001    /**
002     * Copyright (c) 2000-2013 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(
249                                            Calendar.DAY_OF_WEEK, dtStart, 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(
278                            new Date(dtStart.getTime().getTime() + 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                    myCurrent.set(Calendar.SECOND, 0);
371                    myCurrent.set(Calendar.MILLISECOND, 0);
372    
373                    if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
374    
375                            // The current time is earlier than the start time.
376    
377                            if (debug) {
378                                    System.err.println("current < start");
379                            }
380    
381                            return false;
382                    }
383    
384                    Calendar candidate = getCandidateStartTime(myCurrent);
385    
386                    /* Loop over ranges for the duration. */
387    
388                    while ((candidate.getTime().getTime() + duration.getInterval()) >
389                                            myCurrent.getTime().getTime()) {
390    
391                            if (candidateIsInRecurrence(candidate, debug)) {
392                                    return true;
393                            }
394    
395                            /* Roll back to one second previous, and try again. */
396    
397                            candidate.add(Calendar.SECOND, -1);
398    
399                            /* Make sure we haven't rolled back to before dtStart. */
400    
401                            if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
402                                    if (debug) {
403                                            System.err.println("No candidates after dtStart");
404                                    }
405    
406                                    return false;
407                            }
408    
409                            candidate = getCandidateStartTime(candidate);
410                    }
411    
412                    if (debug) {
413                            System.err.println("No matching candidates");
414                    }
415    
416                    return false;
417            }
418    
419            /**
420             * Method setByDay
421             */
422            public void setByDay(DayAndPosition[] b) {
423                    if (b == null) {
424                            byDay = null;
425    
426                            return;
427                    }
428    
429                    byDay = new DayAndPosition[b.length];
430    
431                    /*
432                     * System.arraycopy isn't good enough -- we want to clone each
433                     * individual element.
434                     */
435                    for (int i = 0; i < b.length; i++) {
436                            byDay[i] = (DayAndPosition)b[i].clone();
437                    }
438            }
439    
440            /**
441             * Method setByMonth
442             */
443            public void setByMonth(int[] b) {
444                    if (b == null) {
445                            byMonth = null;
446    
447                            return;
448                    }
449    
450                    byMonth = new int[b.length];
451    
452                    System.arraycopy(b, 0, byMonth, 0, b.length);
453            }
454    
455            /**
456             * Method setByMonthDay
457             */
458            public void setByMonthDay(int[] b) {
459                    if (b == null) {
460                            byMonthDay = null;
461    
462                            return;
463                    }
464    
465                    byMonthDay = new int[b.length];
466    
467                    System.arraycopy(b, 0, byMonthDay, 0, b.length);
468            }
469    
470            /**
471             * Method setByWeekNo
472             */
473            public void setByWeekNo(int[] b) {
474                    if (b == null) {
475                            byWeekNo = null;
476    
477                            return;
478                    }
479    
480                    byWeekNo = new int[b.length];
481    
482                    System.arraycopy(b, 0, byWeekNo, 0, b.length);
483            }
484    
485            /**
486             * Method setByYearDay
487             */
488            public void setByYearDay(int[] b) {
489                    if (b == null) {
490                            byYearDay = null;
491    
492                            return;
493                    }
494    
495                    byYearDay = new int[b.length];
496    
497                    System.arraycopy(b, 0, byYearDay, 0, b.length);
498            }
499    
500            /**
501             * Method setDtEnd
502             */
503            public void setDtEnd(Calendar end) {
504                    Calendar tempEnd = (Calendar)end.clone();
505    
506                    tempEnd.clear(Calendar.ZONE_OFFSET);
507                    tempEnd.clear(Calendar.DST_OFFSET);
508                    tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
509                    duration.setInterval(
510                            tempEnd.getTime().getTime() - dtStart.getTime().getTime());
511            }
512    
513            /**
514             * Method setDtStart
515             */
516            public void setDtStart(Calendar start) {
517                    int oldStart;
518    
519                    if (dtStart != null) {
520                            oldStart = dtStart.getFirstDayOfWeek();
521                    }
522                    else {
523                            oldStart = Calendar.MONDAY;
524                    }
525    
526                    if (start == null) {
527                            dtStart = CalendarFactoryUtil.getCalendar(
528                                    TimeZoneUtil.getTimeZone(StringPool.UTC));
529    
530                            dtStart.setTime(new Date(0L));
531                    }
532                    else {
533                            dtStart = (Calendar)start.clone();
534    
535                            dtStart.clear(Calendar.ZONE_OFFSET);
536                            dtStart.clear(Calendar.DST_OFFSET);
537                            dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
538                    }
539    
540                    dtStart.setMinimalDaysInFirstWeek(4);
541                    dtStart.setFirstDayOfWeek(oldStart);
542                    dtStart.set(Calendar.SECOND, 0);
543                    dtStart.set(Calendar.MILLISECOND, 0);
544            }
545    
546            /**
547             * Method setDuration
548             */
549            public void setDuration(Duration d) {
550                    duration = (Duration)d.clone();
551            }
552    
553            /**
554             * Method setFrequency
555             */
556            public void setFrequency(int freq) {
557                    if ((frequency != DAILY) && (frequency != WEEKLY) &&
558                            (frequency != MONTHLY) && (frequency != YEARLY) &&
559                            (frequency != NO_RECURRENCE)) {
560    
561                            throw new IllegalArgumentException("Invalid frequency");
562                    }
563    
564                    frequency = freq;
565            }
566    
567            /**
568             * Method setInterval
569             */
570            public void setInterval(int intr) {
571                    interval = (intr > 0) ? intr : 1;
572            }
573    
574            /**
575             * Method setOccurrence
576             */
577            public void setOccurrence(int occur) {
578                    occurrence = occur;
579            }
580    
581            /**
582             * Method setUntil
583             */
584            public void setUntil(Calendar u) {
585                    if (u == null) {
586                            until = null;
587    
588                            return;
589                    }
590    
591                    until = (Calendar)u.clone();
592    
593                    until.clear(Calendar.ZONE_OFFSET);
594                    until.clear(Calendar.DST_OFFSET);
595                    until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
596            }
597    
598            /**
599             * Method setWeekStart
600             */
601            public void setWeekStart(int weekstart) {
602                    dtStart.setFirstDayOfWeek(weekstart);
603            }
604    
605            /**
606             * Method toString
607             *
608             * @return String
609             */
610            @Override
611            public String toString() {
612                    StringBundler sb = new StringBundler();
613    
614                    sb.append(getClass().getName());
615                    sb.append("[dtStart=");
616                    sb.append((dtStart != null) ? dtStart.toString() : "null");
617                    sb.append(",duration=");
618                    sb.append((duration != null) ? duration.toString() : "null");
619                    sb.append(",frequency=");
620                    sb.append(frequency);
621                    sb.append(",interval=");
622                    sb.append(interval);
623                    sb.append(",until=");
624                    sb.append((until != null) ? until.toString() : "null");
625                    sb.append(",byDay=");
626    
627                    if (byDay == null) {
628                            sb.append("null");
629                    }
630                    else {
631                            sb.append("[");
632    
633                            for (int i = 0; i < byDay.length; i++) {
634                                    if (i != 0) {
635                                            sb.append(",");
636                                    }
637    
638                                    if (byDay[i] != null) {
639                                            sb.append(byDay[i].toString());
640                                    }
641                                    else {
642                                            sb.append("null");
643                                    }
644                            }
645    
646                            sb.append("]");
647                    }
648    
649                    sb.append(",byMonthDay=");
650                    sb.append(stringizeIntArray(byMonthDay));
651                    sb.append(",byYearDay=");
652                    sb.append(stringizeIntArray(byYearDay));
653                    sb.append(",byWeekNo=");
654                    sb.append(stringizeIntArray(byWeekNo));
655                    sb.append(",byMonth=");
656                    sb.append(stringizeIntArray(byMonth));
657                    sb.append(']');
658    
659                    return sb.toString();
660            }
661    
662            /**
663             * Method getDayNumber
664             *
665             * @return long
666             */
667            protected static long getDayNumber(Calendar cal) {
668                    Calendar tempCal = (Calendar)cal.clone();
669    
670                    // Set to midnight, GMT
671    
672                    tempCal.set(Calendar.MILLISECOND, 0);
673                    tempCal.set(Calendar.SECOND, 0);
674                    tempCal.set(Calendar.MINUTE, 0);
675                    tempCal.set(Calendar.HOUR_OF_DAY, 0);
676    
677                    return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
678            }
679    
680            /**
681             * Method getMonthNumber
682             *
683             * @return long
684             */
685            protected static long getMonthNumber(Calendar cal) {
686                    return
687                            ((cal.get(Calendar.YEAR) - 1970) * 12L) +
688                                    ((cal.get(Calendar.MONTH) - Calendar.JANUARY));
689            }
690    
691            /**
692             * Method getWeekNumber
693             *
694             * @return long
695             */
696            protected static long getWeekNumber(Calendar cal) {
697                    Calendar tempCal = (Calendar)cal.clone();
698    
699                    // Set to midnight, GMT
700    
701                    tempCal.set(Calendar.MILLISECOND, 0);
702                    tempCal.set(Calendar.SECOND, 0);
703                    tempCal.set(Calendar.MINUTE, 0);
704                    tempCal.set(Calendar.HOUR_OF_DAY, 0);
705    
706                    // Roll back to the first day of the week
707    
708                    int delta =
709                            tempCal.getFirstDayOfWeek() - tempCal.get(Calendar.DAY_OF_WEEK);
710    
711                    if (delta > 0) {
712                            delta -= 7;
713                    }
714    
715                    // tempCal now points to the first instant of this week.
716    
717                    // Calculate the "week epoch" -- the weekstart day closest to January 1,
718                    // 1970 (which was a Thursday)
719    
720                    long weekEpoch =
721                            (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
722                                    1000;
723    
724                    return
725                            (tempCal.getTime().getTime() - weekEpoch) /
726                                    (7 * 24 * 60 * 60 * 1000);
727            }
728    
729            /**
730             * Method reduce_constant_length_field
731             */
732            protected static void reduce_constant_length_field(
733                    int field, Calendar start, Calendar candidate) {
734    
735                    if ((start.getMaximum(field) != start.getLeastMaximum(field)) ||
736                            (start.getMinimum(field) != start.getGreatestMinimum(field))) {
737    
738                            throw new IllegalArgumentException("Not a constant length field");
739                    }
740    
741                    int fieldLength =
742                            (start.getMaximum(field) - start.getMinimum(field) + 1);
743                    int delta = start.get(field) - candidate.get(field);
744    
745                    if (delta > 0) {
746                            delta -= fieldLength;
747                    }
748    
749                    candidate.add(field, delta);
750            }
751    
752            /**
753             * Method reduce_day_of_month
754             */
755            protected static void reduce_day_of_month(
756                    Calendar start, Calendar candidate) {
757    
758                    Calendar tempCal = (Calendar)candidate.clone();
759    
760                    tempCal.add(Calendar.MONTH, -1);
761    
762                    int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
763    
764                    if (delta > 0) {
765                            delta -= tempCal.getActualMaximum(Calendar.DATE);
766                    }
767    
768                    candidate.add(Calendar.DATE, delta);
769    
770                    while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
771                            tempCal.add(Calendar.MONTH, -1);
772                            candidate.add(
773                                    Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
774                    }
775            }
776    
777            /**
778             * Method reduce_day_of_year
779             */
780            protected static void reduce_day_of_year(
781                    Calendar start, Calendar candidate) {
782    
783                    if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH)) ||
784                            ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH)) &&
785                             (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
786    
787                            candidate.add(Calendar.YEAR, -1);
788                    }
789    
790                    /* Set the candidate date to the start date. */
791    
792                    candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
793                    candidate.set(Calendar.DATE, start.get(Calendar.DATE));
794    
795                    while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
796                               (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
797    
798                            candidate.add(Calendar.YEAR, -1);
799                            candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
800                            candidate.set(Calendar.DATE, start.get(Calendar.DATE));
801                    }
802            }
803    
804            /**
805             * Method candidateIsInRecurrence
806             *
807             * @return boolean
808             */
809            protected boolean candidateIsInRecurrence(
810                    Calendar candidate, boolean debug) {
811    
812                    if ((until != null) &&
813                            (candidate.getTime().getTime() > until.getTime().getTime())) {
814    
815                            // After "until"
816    
817                            if (debug) {
818                                    System.err.println("after until");
819                            }
820    
821                            return false;
822                    }
823    
824                    if ((getRecurrenceCount(candidate) % interval) != 0) {
825    
826                            // Not a repetition of the interval
827    
828                            if (debug) {
829                                    System.err.println("not an interval rep");
830                            }
831    
832                            return false;
833                    }
834                    else if ((occurrence > 0) &&
835                                     (getRecurrenceCount(candidate) >= occurrence)) {
836    
837                            return false;
838                    }
839    
840                    if (!matchesByDay(candidate) || !matchesByMonthDay(candidate) ||
841                            !matchesByYearDay(candidate) || !matchesByWeekNo(candidate) ||
842                            !matchesByMonth(candidate)) {
843    
844                            // Doesn't match a by* rule
845    
846                            if (debug) {
847                                    System.err.println("doesn't match a by*");
848                            }
849    
850                            return false;
851                    }
852    
853                    if (debug) {
854                            System.err.println("All checks succeeded");
855                    }
856    
857                    return true;
858            }
859    
860            /**
861             * Method getMinimumInterval
862             *
863             * @return int
864             */
865            protected int getMinimumInterval() {
866                    if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null) ||
867                            (byYearDay != null)) {
868    
869                            return DAILY;
870                    }
871                    else if ((frequency == WEEKLY) || (byWeekNo != null)) {
872                            return WEEKLY;
873                    }
874                    else if ((frequency == MONTHLY) || (byMonth != null)) {
875                            return MONTHLY;
876                    }
877                    else if (frequency == YEARLY) {
878                            return YEARLY;
879                    }
880                    else if (frequency == NO_RECURRENCE) {
881                            return NO_RECURRENCE;
882                    }
883                    else {
884    
885                            // Shouldn't happen
886    
887                            throw new IllegalStateException(
888                                    "Internal error: Unknown frequency value");
889                    }
890            }
891    
892            /**
893             * Method getRecurrenceCount
894             *
895             * @return int
896             */
897            protected int getRecurrenceCount(Calendar candidate) {
898                    switch (frequency) {
899    
900                            case NO_RECURRENCE :
901                                    return 0;
902    
903                            case DAILY :
904                                    return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
905    
906                            case WEEKLY :
907                                    Calendar tempCand = (Calendar)candidate.clone();
908    
909                                    tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
910    
911                                    return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
912    
913                            case MONTHLY :
914                                    return
915                                            (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
916    
917                            case YEARLY :
918                                    return
919                                            candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
920    
921                            default :
922                                    throw new IllegalStateException("bad frequency internally...");
923                    }
924            }
925    
926            /**
927             * Method matchesByDay
928             *
929             * @return boolean
930             */
931            protected boolean matchesByDay(Calendar candidate) {
932                    if ((byDay == null) || (byDay.length == 0)) {
933    
934                            /* No byDay rules, so it matches trivially */
935    
936                            return true;
937                    }
938    
939                    int i;
940    
941                    for (i = 0; i < byDay.length; i++) {
942                            if (matchesIndividualByDay(candidate, byDay[i])) {
943                                    return true;
944                            }
945                    }
946    
947                    return false;
948            }
949    
950            /**
951             * Method matchesByField
952             *
953             * @return boolean
954             */
955            protected boolean matchesByField(
956                    int[] array, int field, Calendar candidate, boolean allowNegative) {
957    
958                    if ((array == null) || (array.length == 0)) {
959    
960                            /* No rules, so it matches trivially */
961    
962                            return true;
963                    }
964    
965                    int i;
966    
967                    for (i = 0; i < array.length; i++) {
968                            int val;
969    
970                            if (allowNegative && (array[i] < 0)) {
971    
972                                    // byMonthDay = -1, in a 31-day month, means 31
973    
974                                    int max = candidate.getActualMaximum(field);
975    
976                                    val = (max + 1) + array[i];
977                            }
978                            else {
979                                    val = array[i];
980                            }
981    
982                            if (val == candidate.get(field)) {
983                                    return true;
984                            }
985                    }
986    
987                    return false;
988            }
989    
990            /**
991             * Method matchesByMonth
992             *
993             * @return boolean
994             */
995            protected boolean matchesByMonth(Calendar candidate) {
996                    return matchesByField(byMonth, Calendar.MONTH, candidate, false);
997            }
998    
999            /**
1000             * Method matchesByMonthDay
1001             *
1002             * @return boolean
1003             */
1004            protected boolean matchesByMonthDay(Calendar candidate) {
1005                    return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1006            }
1007    
1008            /**
1009             * Method matchesByWeekNo
1010             *
1011             * @return boolean
1012             */
1013            protected boolean matchesByWeekNo(Calendar candidate) {
1014                    return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1015            }
1016    
1017            /**
1018             * Method matchesByYearDay
1019             *
1020             * @return boolean
1021             */
1022            protected boolean matchesByYearDay(Calendar candidate) {
1023                    return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1024            }
1025    
1026            /**
1027             * Method matchesIndividualByDay
1028             *
1029             * @return boolean
1030             */
1031            protected boolean matchesIndividualByDay(
1032                    Calendar candidate, DayAndPosition pos) {
1033    
1034                    if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
1035                            return false;
1036                    }
1037    
1038                    int position = pos.getDayPosition();
1039    
1040                    if (position == 0) {
1041                            return true;
1042                    }
1043    
1044                    int field = Calendar.DAY_OF_MONTH;
1045    
1046                    if (position > 0) {
1047                            int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
1048    
1049                            return (position == candidatePosition);
1050                    }
1051                    else {
1052    
1053                            /* position < 0 */
1054    
1055                            int negativeCandidatePosition =
1056                                    ((candidate.getActualMaximum(field) - candidate.get(field)) /
1057                                            7) + 1;
1058    
1059                            return (-position == negativeCandidatePosition);
1060                    }
1061            }
1062    
1063            /**
1064             * Method stringizeIntArray
1065             *
1066             * @return String
1067             */
1068            protected String stringizeIntArray(int[] a) {
1069                    if (a == null) {
1070                            return "null";
1071                    }
1072    
1073                    StringBundler sb = new StringBundler(2 * a.length + 1);
1074    
1075                    sb.append("[");
1076    
1077                    for (int i = 0; i < a.length; i++) {
1078                            if (i != 0) {
1079                                    sb.append(",");
1080                            }
1081    
1082                            sb.append(a[i]);
1083                    }
1084    
1085                    sb.append("]");
1086    
1087                    return sb.toString();
1088            }
1089    
1090            /**
1091             * Field byDay
1092             */
1093            protected DayAndPosition[] byDay;
1094    
1095            /**
1096             * Field byMonth
1097             */
1098            protected int[] byMonth;
1099    
1100            /**
1101             * Field byMonthDay
1102             */
1103            protected int[] byMonthDay;
1104    
1105            /**
1106             * Field byWeekNo
1107             */
1108            protected int[] byWeekNo;
1109    
1110            /**
1111             * Field byYearDay
1112             */
1113            protected int[] byYearDay;
1114    
1115            /**
1116             * Field dtStart
1117             */
1118            protected Calendar dtStart;
1119    
1120            /**
1121             * Field duration
1122             */
1123            protected Duration duration;
1124    
1125            /**
1126             * Field frequency
1127             */
1128            protected int frequency;
1129    
1130            /**
1131             * Field interval
1132             */
1133            protected int interval;
1134    
1135            /**
1136             * Field interval
1137             */
1138            protected int occurrence = 0;
1139    
1140            /**
1141             * Field until
1142             */
1143            protected Calendar until;
1144    
1145    }