001    /**
002     * Copyright (c) 2000-2012 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    /*
016     * Copyright (c) 2000, Columbia University.  All rights reserved.
017     *
018     * Redistribution and use in source and binary forms, with or without
019     * modification, are permitted provided that the following conditions are met:
020     *
021     * 1. Redistributions of source code must retain the above copyright
022     *        notice, this list of conditions and the following disclaimer.
023     *
024     * 2. Redistributions in binary form must reproduce the above copyright
025     *        notice, this list of conditions and the following disclaimer in the
026     *        documentation and/or other materials provided with the distribution.
027     *
028     * 3. Neither the name of the University nor the names of its contributors
029     *        may be used to endorse or promote products derived from this software
030     *        without specific prior written permission.
031     *
032     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
033     * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
034     * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
035     * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
036     * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
037     * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
038     * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
039     * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
040     * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
041     * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
042     * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
043     */
044    
045    package com.liferay.portal.kernel.cal;
046    
047    import com.liferay.portal.kernel.util.CalendarFactoryUtil;
048    import com.liferay.portal.kernel.util.StringBundler;
049    import com.liferay.portal.kernel.util.StringPool;
050    import com.liferay.portal.kernel.util.TimeZoneUtil;
051    
052    import java.io.Serializable;
053    
054    import java.util.Calendar;
055    import java.util.Date;
056    
057    /**
058     * @author Jonathan Lennox
059     */
060    public class Recurrence implements Serializable {
061    
062            /**
063             * Field DAILY
064             */
065            public static final int DAILY = 3;
066    
067            /**
068             * Field MONTHLY
069             */
070            public static final int MONTHLY = 5;
071    
072            /**
073             * Field NO_RECURRENCE
074             */
075            public static final int NO_RECURRENCE = 7;
076    
077            /**
078             * Field WEEKLY
079             */
080            public static final int WEEKLY = 4;
081    
082            /**
083             * Field YEARLY
084             */
085            public static final int YEARLY = 6;
086    
087            /**
088             * Constructor Recurrence
089             */
090            public Recurrence() {
091                    this(null, new Duration(), NO_RECURRENCE);
092            }
093    
094            /**
095             * Constructor Recurrence
096             */
097            public Recurrence(Calendar start, Duration dur) {
098                    this(start, dur, NO_RECURRENCE);
099            }
100    
101            /**
102             * Constructor Recurrence
103             */
104            public Recurrence(Calendar start, Duration dur, int freq) {
105                    setDtStart(start);
106    
107                    duration = (Duration)dur.clone();
108                    frequency = freq;
109                    interval = 1;
110            }
111    
112            /* Accessors */
113    
114            /**
115             * Method getByDay
116             *
117             * @return DayAndPosition[]
118             */
119            public DayAndPosition[] getByDay() {
120                    if (byDay == null) {
121                            return null;
122                    }
123    
124                    DayAndPosition[] b = new DayAndPosition[byDay.length];
125    
126                    /*
127                     * System.arraycopy isn't good enough -- we want to clone each
128                     * individual element.
129                     */
130                    for (int i = 0; i < byDay.length; i++) {
131                            b[i] = (DayAndPosition)byDay[i].clone();
132                    }
133    
134                    return b;
135            }
136    
137            /**
138             * Method getByMonth
139             *
140             * @return int[]
141             */
142            public int[] getByMonth() {
143                    if (byMonth == null) {
144                            return null;
145                    }
146    
147                    int[] b = new int[byMonth.length];
148    
149                    System.arraycopy(byMonth, 0, b, 0, byMonth.length);
150    
151                    return b;
152            }
153    
154            /**
155             * Method getByMonthDay
156             *
157             * @return int[]
158             */
159            public int[] getByMonthDay() {
160                    if (byMonthDay == null) {
161                            return null;
162                    }
163    
164                    int[] b = new int[byMonthDay.length];
165    
166                    System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
167    
168                    return b;
169            }
170    
171            /**
172             * Method getByWeekNo
173             *
174             * @return int[]
175             */
176            public int[] getByWeekNo() {
177                    if (byWeekNo == null) {
178                            return null;
179                    }
180    
181                    int[] b = new int[byWeekNo.length];
182    
183                    System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
184    
185                    return b;
186            }
187    
188            /**
189             * Method getByYearDay
190             *
191             * @return int[]
192             */
193            public int[] getByYearDay() {
194                    if (byYearDay == null) {
195                            return null;
196                    }
197    
198                    int[] b = new int[byYearDay.length];
199    
200                    System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
201    
202                    return b;
203            }
204    
205            /**
206             * Method getCandidateStartTime
207             *
208             * @return Calendar
209             */
210            public Calendar getCandidateStartTime(Calendar current) {
211                    if (dtStart.getTime().getTime() > current.getTime().getTime()) {
212                            throw new IllegalArgumentException("Current time before DtStart");
213                    }
214    
215                    int minInterval = getMinimumInterval();
216                    Calendar candidate = (Calendar)current.clone();
217    
218                    if (true) {
219    
220                            // This block is only needed while this function is public...
221    
222                            candidate.clear(Calendar.ZONE_OFFSET);
223                            candidate.clear(Calendar.DST_OFFSET);
224                            candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
225                            candidate.setMinimalDaysInFirstWeek(4);
226                            candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
227                    }
228    
229                    if (frequency == NO_RECURRENCE) {
230                            candidate.setTime(dtStart.getTime());
231    
232                            return candidate;
233                    }
234    
235                    reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
236                    reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
237                    reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
238    
239                    switch (minInterval) {
240    
241                            case DAILY :
242    
243                                    /* No more adjustments needed */
244    
245                                    break;
246    
247                            case WEEKLY :
248                                    reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
249                                                                                             candidate);
250                                    break;
251    
252                            case MONTHLY :
253                                    reduce_day_of_month(dtStart, candidate);
254                                    break;
255    
256                            case YEARLY :
257                                    reduce_day_of_year(dtStart, candidate);
258                                    break;
259                    }
260    
261                    return candidate;
262            }
263    
264            /**
265             * Method getDtEnd
266             *
267             * @return Calendar
268             */
269            public Calendar getDtEnd() {
270    
271                    /*
272                     * Make dtEnd a cloned dtStart, so non-time fields of the Calendar
273                     * are accurate.
274                     */
275                    Calendar tempEnd = (Calendar)dtStart.clone();
276    
277                    tempEnd.setTime(new Date(dtStart.getTime().getTime()
278                                                                     + duration.getInterval()));
279    
280                    return tempEnd;
281            }
282    
283            /**
284             * Method getDtStart
285             *
286             * @return Calendar
287             */
288            public Calendar getDtStart() {
289                    return (Calendar)dtStart.clone();
290            }
291    
292            /**
293             * Method getDuration
294             *
295             * @return Duration
296             */
297            public Duration getDuration() {
298                    return (Duration)duration.clone();
299            }
300    
301            /**
302             * Method getFrequency
303             *
304             * @return int
305             */
306            public int getFrequency() {
307                    return frequency;
308            }
309    
310            /**
311             * Method getInterval
312             *
313             * @return int
314             */
315            public int getInterval() {
316                    return interval;
317            }
318    
319            /**
320             * Method getOccurrence
321             *
322             * @return int
323             */
324            public int getOccurrence() {
325                    return occurrence;
326            }
327    
328            /**
329             * Method getUntil
330             *
331             * @return Calendar
332             */
333            public Calendar getUntil() {
334                    return ((until != null) ? (Calendar)until.clone() : null);
335            }
336    
337            /**
338             * Method getWeekStart
339             *
340             * @return int
341             */
342            public int getWeekStart() {
343                    return dtStart.getFirstDayOfWeek();
344            }
345    
346            /**
347             * Method isInRecurrence
348             *
349             * @return boolean
350             */
351            public boolean isInRecurrence(Calendar current) {
352                    return isInRecurrence(current, false);
353            }
354    
355            /**
356             * Method isInRecurrence
357             *
358             * @return boolean
359             */
360            public boolean isInRecurrence(Calendar current, boolean debug) {
361                    Calendar myCurrent = (Calendar)current.clone();
362    
363                    // Do all calculations in GMT.  Keep other parameters consistent.
364    
365                    myCurrent.clear(Calendar.ZONE_OFFSET);
366                    myCurrent.clear(Calendar.DST_OFFSET);
367                    myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
368                    myCurrent.setMinimalDaysInFirstWeek(4);
369                    myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
370    
371                    if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
372    
373                            // The current time is earlier than the start time.
374    
375                            if (debug) {
376                                    System.err.println("current < start");
377                            }
378    
379                            return false;
380                    }
381    
382                    Calendar candidate = getCandidateStartTime(myCurrent);
383    
384                    /* Loop over ranges for the duration. */
385    
386                    while (candidate.getTime().getTime() + duration.getInterval()
387                               > myCurrent.getTime().getTime()) {
388                            if (candidateIsInRecurrence(candidate, debug)) {
389                                    return true;
390                            }
391    
392                            /* Roll back to one second previous, and try again. */
393    
394                            candidate.add(Calendar.SECOND, -1);
395    
396                            /* Make sure we haven't rolled back to before dtStart. */
397    
398                            if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
399                                    if (debug) {
400                                            System.err.println("No candidates after dtStart");
401                                    }
402    
403                                    return false;
404                            }
405    
406                            candidate = getCandidateStartTime(candidate);
407                    }
408    
409                    if (debug) {
410                            System.err.println("No matching candidates");
411                    }
412    
413                    return false;
414            }
415    
416            /**
417             * Method setByDay
418             */
419            public void setByDay(DayAndPosition[] b) {
420                    if (b == null) {
421                            byDay = null;
422    
423                            return;
424                    }
425    
426                    byDay = new DayAndPosition[b.length];
427    
428                    /*
429                     * System.arraycopy isn't good enough -- we want to clone each
430                     * individual element.
431                     */
432                    for (int i = 0; i < b.length; i++) {
433                            byDay[i] = (DayAndPosition)b[i].clone();
434                    }
435            }
436    
437            /**
438             * Method setByMonth
439             */
440            public void setByMonth(int[] b) {
441                    if (b == null) {
442                            byMonth = null;
443    
444                            return;
445                    }
446    
447                    byMonth = new int[b.length];
448    
449                    System.arraycopy(b, 0, byMonth, 0, b.length);
450            }
451    
452            /**
453             * Method setByMonthDay
454             */
455            public void setByMonthDay(int[] b) {
456                    if (b == null) {
457                            byMonthDay = null;
458    
459                            return;
460                    }
461    
462                    byMonthDay = new int[b.length];
463    
464                    System.arraycopy(b, 0, byMonthDay, 0, b.length);
465            }
466    
467            /**
468             * Method setByWeekNo
469             */
470            public void setByWeekNo(int[] b) {
471                    if (b == null) {
472                            byWeekNo = null;
473    
474                            return;
475                    }
476    
477                    byWeekNo = new int[b.length];
478    
479                    System.arraycopy(b, 0, byWeekNo, 0, b.length);
480            }
481    
482            /**
483             * Method setByYearDay
484             */
485            public void setByYearDay(int[] b) {
486                    if (b == null) {
487                            byYearDay = null;
488    
489                            return;
490                    }
491    
492                    byYearDay = new int[b.length];
493    
494                    System.arraycopy(b, 0, byYearDay, 0, b.length);
495            }
496    
497            /**
498             * Method setDtEnd
499             */
500            public void setDtEnd(Calendar end) {
501                    Calendar tempEnd = (Calendar)end.clone();
502    
503                    tempEnd.clear(Calendar.ZONE_OFFSET);
504                    tempEnd.clear(Calendar.DST_OFFSET);
505                    tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
506                    duration.setInterval(tempEnd.getTime().getTime()
507                                                             - dtStart.getTime().getTime());
508            }
509    
510            /**
511             * Method setDtStart
512             */
513            public void setDtStart(Calendar start) {
514                    int oldStart;
515    
516                    if (dtStart != null) {
517                            oldStart = dtStart.getFirstDayOfWeek();
518                    }
519                    else {
520                            oldStart = Calendar.MONDAY;
521                    }
522    
523                    if (start == null) {
524                            dtStart = CalendarFactoryUtil.getCalendar(
525                                    TimeZoneUtil.getTimeZone(StringPool.UTC));
526    
527                            dtStart.setTime(new Date(0L));
528                    }
529                    else {
530                            dtStart = (Calendar)start.clone();
531    
532                            dtStart.clear(Calendar.ZONE_OFFSET);
533                            dtStart.clear(Calendar.DST_OFFSET);
534                            dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
535                    }
536    
537                    dtStart.setMinimalDaysInFirstWeek(4);
538                    dtStart.setFirstDayOfWeek(oldStart);
539            }
540    
541            /**
542             * Method setDuration
543             */
544            public void setDuration(Duration d) {
545                    duration = (Duration)d.clone();
546            }
547    
548            /**
549             * Method setFrequency
550             */
551            public void setFrequency(int freq) {
552                    if ((frequency != DAILY) && (frequency != WEEKLY)
553                            && (frequency != MONTHLY) && (frequency != YEARLY)
554                            && (frequency != NO_RECURRENCE)) {
555                            throw new IllegalArgumentException("Invalid frequency");
556                    }
557    
558                    frequency = freq;
559            }
560    
561            /**
562             * Method setInterval
563             */
564            public void setInterval(int intr) {
565                    interval = (intr > 0) ? intr : 1;
566            }
567    
568            /**
569             * Method setOccurrence
570             */
571            public void setOccurrence(int occur) {
572                    occurrence = occur;
573            }
574    
575            /**
576             * Method setUntil
577             */
578            public void setUntil(Calendar u) {
579                    if (u == null) {
580                            until = null;
581    
582                            return;
583                    }
584    
585                    until = (Calendar)u.clone();
586    
587                    until.clear(Calendar.ZONE_OFFSET);
588                    until.clear(Calendar.DST_OFFSET);
589                    until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
590            }
591    
592            /**
593             * Method setWeekStart
594             */
595            public void setWeekStart(int weekstart) {
596                    dtStart.setFirstDayOfWeek(weekstart);
597            }
598    
599            /**
600             * Method toString
601             *
602             * @return String
603             */
604            @Override
605            public String toString() {
606                    StringBundler sb = new StringBundler();
607    
608                    sb.append(getClass().getName());
609                    sb.append("[dtStart=");
610                    sb.append((dtStart != null) ? dtStart.toString() : "null");
611                    sb.append(",duration=");
612                    sb.append((duration != null) ? duration.toString() : "null");
613                    sb.append(",frequency=");
614                    sb.append(frequency);
615                    sb.append(",interval=");
616                    sb.append(interval);
617                    sb.append(",until=");
618                    sb.append((until != null) ? until.toString() : "null");
619                    sb.append(",byDay=");
620    
621                    if (byDay == null) {
622                            sb.append("null");
623                    }
624                    else {
625                            sb.append("[");
626    
627                            for (int i = 0; i < byDay.length; i++) {
628                                    if (i != 0) {
629                                            sb.append(",");
630                                    }
631    
632                                    if (byDay[i] != null) {
633                                            sb.append(byDay[i].toString());
634                                    }
635                                    else {
636                                            sb.append("null");
637                                    }
638                            }
639    
640                            sb.append("]");
641                    }
642    
643                    sb.append(",byMonthDay=");
644                    sb.append(stringizeIntArray(byMonthDay));
645                    sb.append(",byYearDay=");
646                    sb.append(stringizeIntArray(byYearDay));
647                    sb.append(",byWeekNo=");
648                    sb.append(stringizeIntArray(byWeekNo));
649                    sb.append(",byMonth=");
650                    sb.append(stringizeIntArray(byMonth));
651                    sb.append(']');
652    
653                    return sb.toString();
654            }
655    
656            /**
657             * Method getDayNumber
658             *
659             * @return long
660             */
661            protected static long getDayNumber(Calendar cal) {
662                    Calendar tempCal = (Calendar)cal.clone();
663    
664                    // Set to midnight, GMT
665    
666                    tempCal.set(Calendar.MILLISECOND, 0);
667                    tempCal.set(Calendar.SECOND, 0);
668                    tempCal.set(Calendar.MINUTE, 0);
669                    tempCal.set(Calendar.HOUR_OF_DAY, 0);
670    
671                    return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
672            }
673    
674            /**
675             * Method getMonthNumber
676             *
677             * @return long
678             */
679            protected static long getMonthNumber(Calendar cal) {
680                    return (cal.get(Calendar.YEAR) - 1970) * 12L
681                               + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
682            }
683    
684            /**
685             * Method getWeekNumber
686             *
687             * @return long
688             */
689            protected static long getWeekNumber(Calendar cal) {
690                    Calendar tempCal = (Calendar)cal.clone();
691    
692                    // Set to midnight, GMT
693    
694                    tempCal.set(Calendar.MILLISECOND, 0);
695                    tempCal.set(Calendar.SECOND, 0);
696                    tempCal.set(Calendar.MINUTE, 0);
697                    tempCal.set(Calendar.HOUR_OF_DAY, 0);
698    
699                    // Roll back to the first day of the week
700    
701                    int delta = tempCal.getFirstDayOfWeek()
702                                            - tempCal.get(Calendar.DAY_OF_WEEK);
703    
704                    if (delta > 0) {
705                            delta -= 7;
706                    }
707    
708                    // tempCal now points to the first instant of this week.
709    
710                    // Calculate the "week epoch" -- the weekstart day closest to January 1,
711                    // 1970 (which was a Thursday)
712    
713                    long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L
714                                                     * 60 * 60 * 1000;
715    
716                    return (tempCal.getTime().getTime() - weekEpoch)
717                               / (7 * 24 * 60 * 60 * 1000);
718            }
719    
720            /**
721             * Method reduce_constant_length_field
722             */
723            protected static void reduce_constant_length_field(int field,
724                                                                                                               Calendar start,
725                                                                                                               Calendar candidate) {
726                    if ((start.getMaximum(field) != start.getLeastMaximum(field))
727                            || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
728                            throw new IllegalArgumentException("Not a constant length field");
729                    }
730    
731                    int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
732                                                       + 1);
733                    int delta = start.get(field) - candidate.get(field);
734    
735                    if (delta > 0) {
736                            delta -= fieldLength;
737                    }
738    
739                    candidate.add(field, delta);
740            }
741    
742            /**
743             * Method reduce_day_of_month
744             */
745            protected static void reduce_day_of_month(Calendar start,
746                                                                                              Calendar candidate) {
747                    Calendar tempCal = (Calendar)candidate.clone();
748    
749                    tempCal.add(Calendar.MONTH, -1);
750    
751                    int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
752    
753                    if (delta > 0) {
754                            delta -= tempCal.getActualMaximum(Calendar.DATE);
755                    }
756    
757                    candidate.add(Calendar.DATE, delta);
758    
759                    while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
760                            tempCal.add(Calendar.MONTH, -1);
761                            candidate.add(Calendar.DATE,
762                                                      -tempCal.getActualMaximum(Calendar.DATE));
763                    }
764            }
765    
766            /**
767             * Method reduce_day_of_year
768             */
769            protected static void reduce_day_of_year(Calendar start,
770                                                                                             Calendar candidate) {
771                    if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
772                            || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
773                                    && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
774                            candidate.add(Calendar.YEAR, -1);
775                    }
776    
777                    /* Set the candidate date to the start date. */
778    
779                    candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
780                    candidate.set(Calendar.DATE, start.get(Calendar.DATE));
781    
782                    while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
783                               || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
784                            candidate.add(Calendar.YEAR, -1);
785                            candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
786                            candidate.set(Calendar.DATE, start.get(Calendar.DATE));
787                    }
788            }
789    
790            /**
791             * Method candidateIsInRecurrence
792             *
793             * @return boolean
794             */
795            protected boolean candidateIsInRecurrence(Calendar candidate,
796                                                                                              boolean debug) {
797                    if ((until != null)
798                            && (candidate.getTime().getTime() > until.getTime().getTime())) {
799    
800                            // After "until"
801    
802                            if (debug) {
803                                    System.err.println("after until");
804                            }
805    
806                            return false;
807                    }
808    
809                    if (getRecurrenceCount(candidate) % interval != 0) {
810    
811                            // Not a repetition of the interval
812    
813                            if (debug) {
814                                    System.err.println("not an interval rep");
815                            }
816    
817                            return false;
818                    }
819                    else if ((occurrence > 0) &&
820                                     (getRecurrenceCount(candidate) >= occurrence)) {
821    
822                            return false;
823                    }
824    
825                    if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
826                            ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
827                            ||!matchesByMonth(candidate)) {
828    
829                            // Doesn't match a by* rule
830    
831                            if (debug) {
832                                    System.err.println("doesn't match a by*");
833                            }
834    
835                            return false;
836                    }
837    
838                    if (debug) {
839                            System.err.println("All checks succeeded");
840                    }
841    
842                    return true;
843            }
844    
845            /**
846             * Method getMinimumInterval
847             *
848             * @return int
849             */
850            protected int getMinimumInterval() {
851                    if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
852                            || (byYearDay != null)) {
853                            return DAILY;
854                    }
855                    else if ((frequency == WEEKLY) || (byWeekNo != null)) {
856                            return WEEKLY;
857                    }
858                    else if ((frequency == MONTHLY) || (byMonth != null)) {
859                            return MONTHLY;
860                    }
861                    else if (frequency == YEARLY) {
862                            return YEARLY;
863                    }
864                    else if (frequency == NO_RECURRENCE) {
865                            return NO_RECURRENCE;
866                    }
867                    else {
868    
869                            // Shouldn't happen
870    
871                            throw new IllegalStateException(
872                                    "Internal error: Unknown frequency value");
873                    }
874            }
875    
876            /**
877             * Method getRecurrenceCount
878             *
879             * @return int
880             */
881            protected int getRecurrenceCount(Calendar candidate) {
882                    switch (frequency) {
883    
884                            case NO_RECURRENCE :
885                                    return 0;
886    
887                            case DAILY :
888                                    return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
889    
890                            case WEEKLY :
891                                    Calendar tempCand = (Calendar)candidate.clone();
892    
893                                    tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
894    
895                                    return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
896    
897                            case MONTHLY :
898                                    return (int)(getMonthNumber(candidate)
899                                                             - getMonthNumber(dtStart));
900    
901                            case YEARLY :
902                                    return candidate.get(Calendar.YEAR)
903                                               - dtStart.get(Calendar.YEAR);
904    
905                            default :
906                                    throw new IllegalStateException("bad frequency internally...");
907                    }
908            }
909    
910            /**
911             * Method matchesByDay
912             *
913             * @return boolean
914             */
915            protected boolean matchesByDay(Calendar candidate) {
916                    if ((byDay == null) || (byDay.length == 0)) {
917    
918                            /* No byDay rules, so it matches trivially */
919    
920                            return true;
921                    }
922    
923                    int i;
924    
925                    for (i = 0; i < byDay.length; i++) {
926                            if (matchesIndividualByDay(candidate, byDay[i])) {
927                                    return true;
928                            }
929                    }
930    
931                    return false;
932            }
933    
934            /**
935             * Method matchesByField
936             *
937             * @return boolean
938             */
939            protected boolean matchesByField(int[] array, int field,
940                                                                                            Calendar candidate,
941                                                                                            boolean allowNegative) {
942                    if ((array == null) || (array.length == 0)) {
943    
944                            /* No rules, so it matches trivially */
945    
946                            return true;
947                    }
948    
949                    int i;
950    
951                    for (i = 0; i < array.length; i++) {
952                            int val;
953    
954                            if (allowNegative && (array[i] < 0)) {
955    
956                                    // byMonthDay = -1, in a 31-day month, means 31
957    
958                                    int max = candidate.getActualMaximum(field);
959    
960                                    val = (max + 1) + array[i];
961                            }
962                            else {
963                                    val = array[i];
964                            }
965    
966                            if (val == candidate.get(field)) {
967                                    return true;
968                            }
969                    }
970    
971                    return false;
972            }
973    
974            /**
975             * Method matchesByMonth
976             *
977             * @return boolean
978             */
979            protected boolean matchesByMonth(Calendar candidate) {
980                    return matchesByField(byMonth, Calendar.MONTH, candidate, false);
981            }
982    
983            /**
984             * Method matchesByMonthDay
985             *
986             * @return boolean
987             */
988            protected boolean matchesByMonthDay(Calendar candidate) {
989                    return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
990            }
991    
992            /**
993             * Method matchesByWeekNo
994             *
995             * @return boolean
996             */
997            protected boolean matchesByWeekNo(Calendar candidate) {
998                    return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
999            }
1000    
1001            /**
1002             * Method matchesByYearDay
1003             *
1004             * @return boolean
1005             */
1006            protected boolean matchesByYearDay(Calendar candidate) {
1007                    return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1008            }
1009    
1010            /**
1011             * Method matchesIndividualByDay
1012             *
1013             * @return boolean
1014             */
1015            protected boolean matchesIndividualByDay(Calendar candidate,
1016                                                                                             DayAndPosition pos) {
1017                    if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1018                            return false;
1019                    }
1020    
1021                    int position = pos.getDayPosition();
1022    
1023                    if (position == 0) {
1024                            return true;
1025                    }
1026    
1027                    int field = Calendar.DAY_OF_MONTH;
1028    
1029                    if (position > 0) {
1030                            int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1031    
1032                            return (position == candidatePosition);
1033                    }
1034                    else {
1035    
1036                            /* position < 0 */
1037    
1038                            int negativeCandidatePosition =
1039                                    ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
1040                                    + 1;
1041    
1042                            return (-position == negativeCandidatePosition);
1043                    }
1044            }
1045    
1046            /**
1047             * Method stringizeIntArray
1048             *
1049             * @return String
1050             */
1051            protected String stringizeIntArray(int[] a) {
1052                    if (a == null) {
1053                            return "null";
1054                    }
1055    
1056                    StringBundler sb = new StringBundler(2 * a.length + 1);
1057    
1058                    sb.append("[");
1059    
1060                    for (int i = 0; i < a.length; i++) {
1061                            if (i != 0) {
1062                                    sb.append(",");
1063                            }
1064    
1065                            sb.append(a[i]);
1066                    }
1067    
1068                    sb.append("]");
1069    
1070                    return sb.toString();
1071            }
1072    
1073            /**
1074             * Field byDay
1075             */
1076            protected DayAndPosition[] byDay;
1077    
1078            /**
1079             * Field byMonth
1080             */
1081            protected int[] byMonth;
1082    
1083            /**
1084             * Field byMonthDay
1085             */
1086            protected int[] byMonthDay;
1087    
1088            /**
1089             * Field byWeekNo
1090             */
1091            protected int[] byWeekNo;
1092    
1093            /**
1094             * Field byYearDay
1095             */
1096            protected int[] byYearDay;
1097    
1098            /**
1099             * Field dtStart
1100             */
1101            protected Calendar dtStart;
1102    
1103            /**
1104             * Field duration
1105             */
1106            protected Duration duration;
1107    
1108            /**
1109             * Field frequency
1110             */
1111            protected int frequency;
1112    
1113            /**
1114             * Field interval
1115             */
1116            protected int interval;
1117    
1118            /**
1119             * Field interval
1120             */
1121            protected int occurrence = 0;
1122    
1123            /**
1124             * Field until
1125             */
1126            protected Calendar until;
1127    
1128    }