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