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             * @param  current the current time
209             * @return Calendar
210             */
211            public Calendar getCandidateStartTime(Calendar current) {
212                    if (dtStart.getTime().getTime() > current.getTime().getTime()) {
213                            throw new IllegalArgumentException("Current time before DtStart");
214                    }
215    
216                    int minInterval = getMinimumInterval();
217                    Calendar candidate = (Calendar)current.clone();
218    
219                    if (true) {
220    
221                            // This block is only needed while this function is public...
222    
223                            candidate.clear(Calendar.ZONE_OFFSET);
224                            candidate.clear(Calendar.DST_OFFSET);
225                            candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
226                            candidate.setMinimalDaysInFirstWeek(4);
227                            candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
228                    }
229    
230                    if (frequency == NO_RECURRENCE) {
231                            candidate.setTime(dtStart.getTime());
232    
233                            return candidate;
234                    }
235    
236                    reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
237                    reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
238                    reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
239    
240                    switch (minInterval) {
241    
242                            case DAILY :
243    
244                                    /* No more adjustments needed */
245    
246                                    break;
247    
248                            case WEEKLY :
249                                    reduce_constant_length_field(
250                                            Calendar.DAY_OF_WEEK, dtStart, candidate);
251                                    break;
252    
253                            case MONTHLY :
254                                    reduce_day_of_month(dtStart, candidate);
255                                    break;
256    
257                            case YEARLY :
258                                    reduce_day_of_year(dtStart, candidate);
259                                    break;
260                    }
261    
262                    return candidate;
263            }
264    
265            /**
266             * Method getDtEnd
267             *
268             * @return Calendar
269             */
270            public Calendar getDtEnd() {
271    
272                    /*
273                     * Make dtEnd a cloned dtStart, so non-time fields of the Calendar
274                     * are accurate.
275                     */
276                    Calendar tempEnd = (Calendar)dtStart.clone();
277    
278                    tempEnd.setTime(
279                            new Date(dtStart.getTime().getTime() + duration.getInterval()));
280    
281                    return tempEnd;
282            }
283    
284            /**
285             * Method getDtStart
286             *
287             * @return Calendar
288             */
289            public Calendar getDtStart() {
290                    return (Calendar)dtStart.clone();
291            }
292    
293            /**
294             * Method getDuration
295             *
296             * @return Duration
297             */
298            public Duration getDuration() {
299                    return (Duration)duration.clone();
300            }
301    
302            /**
303             * Method getFrequency
304             *
305             * @return int
306             */
307            public int getFrequency() {
308                    return frequency;
309            }
310    
311            /**
312             * Method getInterval
313             *
314             * @return int
315             */
316            public int getInterval() {
317                    return interval;
318            }
319    
320            /**
321             * Method getOccurrence
322             *
323             * @return int
324             */
325            public int getOccurrence() {
326                    return occurrence;
327            }
328    
329            /**
330             * Method getUntil
331             *
332             * @return Calendar
333             */
334            public Calendar getUntil() {
335                    return ((until != null) ? (Calendar)until.clone() : null);
336            }
337    
338            /**
339             * Method getWeekStart
340             *
341             * @return int
342             */
343            public int getWeekStart() {
344                    return dtStart.getFirstDayOfWeek();
345            }
346    
347            /**
348             * Method isInRecurrence
349             *
350             * @param  current the current time
351             * @return boolean
352             */
353            public boolean isInRecurrence(Calendar current) {
354                    return isInRecurrence(current, false);
355            }
356    
357            /**
358             * Method isInRecurrence
359             *
360             * @param  current the current time
361             * @param  debug whether to print debug messages
362             * @return boolean
363             */
364            public boolean isInRecurrence(Calendar current, boolean debug) {
365                    Calendar myCurrent = (Calendar)current.clone();
366    
367                    // Do all calculations in GMT.  Keep other parameters consistent.
368    
369                    myCurrent.clear(Calendar.ZONE_OFFSET);
370                    myCurrent.clear(Calendar.DST_OFFSET);
371                    myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
372                    myCurrent.setMinimalDaysInFirstWeek(4);
373                    myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
374                    myCurrent.set(Calendar.SECOND, 0);
375                    myCurrent.set(Calendar.MILLISECOND, 0);
376    
377                    if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
378    
379                            // The current time is earlier than the start time.
380    
381                            if (debug) {
382                                    System.err.println("current < start");
383                            }
384    
385                            return false;
386                    }
387    
388                    Calendar candidate = getCandidateStartTime(myCurrent);
389    
390                    /* Loop over ranges for the duration. */
391    
392                    while ((candidate.getTime().getTime() + duration.getInterval()) >
393                                            myCurrent.getTime().getTime()) {
394    
395                            if (candidateIsInRecurrence(candidate, debug)) {
396                                    return true;
397                            }
398    
399                            /* Roll back to one second previous, and try again. */
400    
401                            candidate.add(Calendar.SECOND, -1);
402    
403                            /* Make sure we haven't rolled back to before dtStart. */
404    
405                            if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
406                                    if (debug) {
407                                            System.err.println("No candidates after dtStart");
408                                    }
409    
410                                    return false;
411                            }
412    
413                            candidate = getCandidateStartTime(candidate);
414                    }
415    
416                    if (debug) {
417                            System.err.println("No matching candidates");
418                    }
419    
420                    return false;
421            }
422    
423            /**
424             * Method setByDay
425             */
426            public void setByDay(DayAndPosition[] b) {
427                    if (b == null) {
428                            byDay = null;
429    
430                            return;
431                    }
432    
433                    byDay = new DayAndPosition[b.length];
434    
435                    /*
436                     * System.arraycopy isn't good enough -- we want to clone each
437                     * individual element.
438                     */
439                    for (int i = 0; i < b.length; i++) {
440                            byDay[i] = (DayAndPosition)b[i].clone();
441                    }
442            }
443    
444            /**
445             * Method setByMonth
446             */
447            public void setByMonth(int[] b) {
448                    if (b == null) {
449                            byMonth = null;
450    
451                            return;
452                    }
453    
454                    byMonth = new int[b.length];
455    
456                    System.arraycopy(b, 0, byMonth, 0, b.length);
457            }
458    
459            /**
460             * Method setByMonthDay
461             */
462            public void setByMonthDay(int[] b) {
463                    if (b == null) {
464                            byMonthDay = null;
465    
466                            return;
467                    }
468    
469                    byMonthDay = new int[b.length];
470    
471                    System.arraycopy(b, 0, byMonthDay, 0, b.length);
472            }
473    
474            /**
475             * Method setByWeekNo
476             */
477            public void setByWeekNo(int[] b) {
478                    if (b == null) {
479                            byWeekNo = null;
480    
481                            return;
482                    }
483    
484                    byWeekNo = new int[b.length];
485    
486                    System.arraycopy(b, 0, byWeekNo, 0, b.length);
487            }
488    
489            /**
490             * Method setByYearDay
491             */
492            public void setByYearDay(int[] b) {
493                    if (b == null) {
494                            byYearDay = null;
495    
496                            return;
497                    }
498    
499                    byYearDay = new int[b.length];
500    
501                    System.arraycopy(b, 0, byYearDay, 0, b.length);
502            }
503    
504            /**
505             * Method setDtEnd
506             */
507            public void setDtEnd(Calendar end) {
508                    Calendar tempEnd = (Calendar)end.clone();
509    
510                    tempEnd.clear(Calendar.ZONE_OFFSET);
511                    tempEnd.clear(Calendar.DST_OFFSET);
512                    tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
513                    duration.setInterval(
514                            tempEnd.getTime().getTime() - dtStart.getTime().getTime());
515            }
516    
517            /**
518             * Method setDtStart
519             */
520            public void setDtStart(Calendar start) {
521                    int oldStart;
522    
523                    if (dtStart != null) {
524                            oldStart = dtStart.getFirstDayOfWeek();
525                    }
526                    else {
527                            oldStart = Calendar.MONDAY;
528                    }
529    
530                    if (start == null) {
531                            dtStart = CalendarFactoryUtil.getCalendar(
532                                    TimeZoneUtil.getTimeZone(StringPool.UTC));
533    
534                            dtStart.setTime(new Date(0L));
535                    }
536                    else {
537                            dtStart = (Calendar)start.clone();
538    
539                            dtStart.clear(Calendar.ZONE_OFFSET);
540                            dtStart.clear(Calendar.DST_OFFSET);
541                            dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
542                    }
543    
544                    dtStart.setMinimalDaysInFirstWeek(4);
545                    dtStart.setFirstDayOfWeek(oldStart);
546                    dtStart.set(Calendar.SECOND, 0);
547                    dtStart.set(Calendar.MILLISECOND, 0);
548            }
549    
550            /**
551             * Method setDuration
552             */
553            public void setDuration(Duration d) {
554                    duration = (Duration)d.clone();
555            }
556    
557            /**
558             * Method setFrequency
559             */
560            public void setFrequency(int freq) {
561                    if ((frequency != DAILY) && (frequency != WEEKLY) &&
562                            (frequency != MONTHLY) && (frequency != YEARLY) &&
563                            (frequency != NO_RECURRENCE)) {
564    
565                            throw new IllegalArgumentException("Invalid frequency");
566                    }
567    
568                    frequency = freq;
569            }
570    
571            /**
572             * Method setInterval
573             */
574            public void setInterval(int intr) {
575                    interval = (intr > 0) ? intr : 1;
576            }
577    
578            /**
579             * Method setOccurrence
580             */
581            public void setOccurrence(int occur) {
582                    occurrence = occur;
583            }
584    
585            /**
586             * Method setUntil
587             */
588            public void setUntil(Calendar u) {
589                    if (u == null) {
590                            until = null;
591    
592                            return;
593                    }
594    
595                    until = (Calendar)u.clone();
596    
597                    until.clear(Calendar.ZONE_OFFSET);
598                    until.clear(Calendar.DST_OFFSET);
599                    until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
600            }
601    
602            /**
603             * Method setWeekStart
604             */
605            public void setWeekStart(int weekstart) {
606                    dtStart.setFirstDayOfWeek(weekstart);
607            }
608    
609            /**
610             * Method toString
611             *
612             * @return String
613             */
614            @Override
615            public String toString() {
616                    StringBundler sb = new StringBundler();
617    
618                    sb.append(getClass().getName());
619                    sb.append("[dtStart=");
620                    sb.append((dtStart != null) ? dtStart.toString() : "null");
621                    sb.append(",duration=");
622                    sb.append((duration != null) ? duration.toString() : "null");
623                    sb.append(",frequency=");
624                    sb.append(frequency);
625                    sb.append(",interval=");
626                    sb.append(interval);
627                    sb.append(",until=");
628                    sb.append((until != null) ? until.toString() : "null");
629                    sb.append(",byDay=");
630    
631                    if (byDay == null) {
632                            sb.append("null");
633                    }
634                    else {
635                            sb.append("[");
636    
637                            for (int i = 0; i < byDay.length; i++) {
638                                    if (i != 0) {
639                                            sb.append(",");
640                                    }
641    
642                                    if (byDay[i] != null) {
643                                            sb.append(byDay[i].toString());
644                                    }
645                                    else {
646                                            sb.append("null");
647                                    }
648                            }
649    
650                            sb.append("]");
651                    }
652    
653                    sb.append(",byMonthDay=");
654                    sb.append(stringizeIntArray(byMonthDay));
655                    sb.append(",byYearDay=");
656                    sb.append(stringizeIntArray(byYearDay));
657                    sb.append(",byWeekNo=");
658                    sb.append(stringizeIntArray(byWeekNo));
659                    sb.append(",byMonth=");
660                    sb.append(stringizeIntArray(byMonth));
661                    sb.append(']');
662    
663                    return sb.toString();
664            }
665    
666            /**
667             * Method getDayNumber
668             *
669             * @return long
670             */
671            protected static long getDayNumber(Calendar cal) {
672                    Calendar tempCal = (Calendar)cal.clone();
673    
674                    // Set to midnight, GMT
675    
676                    tempCal.set(Calendar.MILLISECOND, 0);
677                    tempCal.set(Calendar.SECOND, 0);
678                    tempCal.set(Calendar.MINUTE, 0);
679                    tempCal.set(Calendar.HOUR_OF_DAY, 0);
680    
681                    return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
682            }
683    
684            /**
685             * Method getMonthNumber
686             *
687             * @return long
688             */
689            protected static long getMonthNumber(Calendar cal) {
690                    return
691                            ((cal.get(Calendar.YEAR) - 1970) * 12L) +
692                                    ((cal.get(Calendar.MONTH) - Calendar.JANUARY));
693            }
694    
695            /**
696             * Method getWeekNumber
697             *
698             * @return long
699             */
700            protected static long getWeekNumber(Calendar cal) {
701                    Calendar tempCal = (Calendar)cal.clone();
702    
703                    // Set to midnight, GMT
704    
705                    tempCal.set(Calendar.MILLISECOND, 0);
706                    tempCal.set(Calendar.SECOND, 0);
707                    tempCal.set(Calendar.MINUTE, 0);
708                    tempCal.set(Calendar.HOUR_OF_DAY, 0);
709    
710                    // Roll back to the first day of the week
711    
712                    int delta =
713                            tempCal.getFirstDayOfWeek() - tempCal.get(Calendar.DAY_OF_WEEK);
714    
715                    if (delta > 0) {
716                            delta -= 7;
717                    }
718    
719                    // tempCal now points to the first instant of this week.
720    
721                    // Calculate the "week epoch" -- the weekstart day closest to January 1,
722                    // 1970 (which was a Thursday)
723    
724                    long weekEpoch =
725                            (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L * 60 * 60 *
726                                    1000;
727    
728                    return
729                            (tempCal.getTime().getTime() - weekEpoch) /
730                                    (7 * 24 * 60 * 60 * 1000);
731            }
732    
733            /**
734             * Method reduce_constant_length_field
735             */
736            protected static void reduce_constant_length_field(
737                    int field, Calendar start, Calendar candidate) {
738    
739                    if ((start.getMaximum(field) != start.getLeastMaximum(field)) ||
740                            (start.getMinimum(field) != start.getGreatestMinimum(field))) {
741    
742                            throw new IllegalArgumentException("Not a constant length field");
743                    }
744    
745                    int fieldLength =
746                            (start.getMaximum(field) - start.getMinimum(field) + 1);
747                    int delta = start.get(field) - candidate.get(field);
748    
749                    if (delta > 0) {
750                            delta -= fieldLength;
751                    }
752    
753                    candidate.add(field, delta);
754            }
755    
756            /**
757             * Method reduce_day_of_month
758             */
759            protected static void reduce_day_of_month(
760                    Calendar start, Calendar candidate) {
761    
762                    Calendar tempCal = (Calendar)candidate.clone();
763    
764                    tempCal.add(Calendar.MONTH, -1);
765    
766                    int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
767    
768                    if (delta > 0) {
769                            delta -= tempCal.getActualMaximum(Calendar.DATE);
770                    }
771    
772                    candidate.add(Calendar.DATE, delta);
773    
774                    while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
775                            tempCal.add(Calendar.MONTH, -1);
776                            candidate.add(
777                                    Calendar.DATE, -tempCal.getActualMaximum(Calendar.DATE));
778                    }
779            }
780    
781            /**
782             * Method reduce_day_of_year
783             */
784            protected static void reduce_day_of_year(
785                    Calendar start, Calendar candidate) {
786    
787                    if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH)) ||
788                            ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH)) &&
789                             (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
790    
791                            candidate.add(Calendar.YEAR, -1);
792                    }
793    
794                    /* Set the candidate date to the start date. */
795    
796                    candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
797                    candidate.set(Calendar.DATE, start.get(Calendar.DATE));
798    
799                    while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH)) ||
800                               (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
801    
802                            candidate.add(Calendar.YEAR, -1);
803                            candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
804                            candidate.set(Calendar.DATE, start.get(Calendar.DATE));
805                    }
806            }
807    
808            /**
809             * Method candidateIsInRecurrence
810             *
811             * @return boolean
812             */
813            protected boolean candidateIsInRecurrence(
814                    Calendar candidate, boolean debug) {
815    
816                    if ((until != null) &&
817                            (candidate.getTime().getTime() > until.getTime().getTime())) {
818    
819                            // After "until"
820    
821                            if (debug) {
822                                    System.err.println("after until");
823                            }
824    
825                            return false;
826                    }
827    
828                    if ((getRecurrenceCount(candidate) % interval) != 0) {
829    
830                            // Not a repetition of the interval
831    
832                            if (debug) {
833                                    System.err.println("not an interval rep");
834                            }
835    
836                            return false;
837                    }
838                    else if ((occurrence > 0) &&
839                                     (getRecurrenceCount(candidate) >= occurrence)) {
840    
841                            return false;
842                    }
843    
844                    if (!matchesByDay(candidate) || !matchesByMonthDay(candidate) ||
845                            !matchesByYearDay(candidate) || !matchesByWeekNo(candidate) ||
846                            !matchesByMonth(candidate)) {
847    
848                            // Doesn't match a by* rule
849    
850                            if (debug) {
851                                    System.err.println("doesn't match a by*");
852                            }
853    
854                            return false;
855                    }
856    
857                    if (debug) {
858                            System.err.println("All checks succeeded");
859                    }
860    
861                    return true;
862            }
863    
864            /**
865             * Method getMinimumInterval
866             *
867             * @return int
868             */
869            protected int getMinimumInterval() {
870                    if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null) ||
871                            (byYearDay != null)) {
872    
873                            return DAILY;
874                    }
875                    else if ((frequency == WEEKLY) || (byWeekNo != null)) {
876                            return WEEKLY;
877                    }
878                    else if ((frequency == MONTHLY) || (byMonth != null)) {
879                            return MONTHLY;
880                    }
881                    else if (frequency == YEARLY) {
882                            return YEARLY;
883                    }
884                    else if (frequency == NO_RECURRENCE) {
885                            return NO_RECURRENCE;
886                    }
887                    else {
888    
889                            // Shouldn't happen
890    
891                            throw new IllegalStateException(
892                                    "Internal error: Unknown frequency value");
893                    }
894            }
895    
896            /**
897             * Method getRecurrenceCount
898             *
899             * @return int
900             */
901            protected int getRecurrenceCount(Calendar candidate) {
902                    switch (frequency) {
903    
904                            case NO_RECURRENCE :
905                                    return 0;
906    
907                            case DAILY :
908                                    return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
909    
910                            case WEEKLY :
911                                    Calendar tempCand = (Calendar)candidate.clone();
912    
913                                    tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
914    
915                                    return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
916    
917                            case MONTHLY :
918                                    return
919                                            (int)(getMonthNumber(candidate) - getMonthNumber(dtStart));
920    
921                            case YEARLY :
922                                    return
923                                            candidate.get(Calendar.YEAR) - dtStart.get(Calendar.YEAR);
924    
925                            default :
926                                    throw new IllegalStateException("bad frequency internally...");
927                    }
928            }
929    
930            /**
931             * Method matchesByDay
932             *
933             * @return boolean
934             */
935            protected boolean matchesByDay(Calendar candidate) {
936                    if ((byDay == null) || (byDay.length == 0)) {
937    
938                            /* No byDay rules, so it matches trivially */
939    
940                            return true;
941                    }
942    
943                    int i;
944    
945                    for (i = 0; i < byDay.length; i++) {
946                            if (matchesIndividualByDay(candidate, byDay[i])) {
947                                    return true;
948                            }
949                    }
950    
951                    return false;
952            }
953    
954            /**
955             * Method matchesByField
956             *
957             * @return boolean
958             */
959            protected boolean matchesByField(
960                    int[] array, int field, Calendar candidate, boolean allowNegative) {
961    
962                    if ((array == null) || (array.length == 0)) {
963    
964                            /* No rules, so it matches trivially */
965    
966                            return true;
967                    }
968    
969                    int i;
970    
971                    for (i = 0; i < array.length; i++) {
972                            int val;
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                            return (position == candidatePosition);
1054                    }
1055                    else {
1056    
1057                            /* position < 0 */
1058    
1059                            int negativeCandidatePosition =
1060                                    ((candidate.getActualMaximum(field) - candidate.get(field)) /
1061                                            7) + 1;
1062    
1063                            return (-position == negativeCandidatePosition);
1064                    }
1065            }
1066    
1067            /**
1068             * Method stringizeIntArray
1069             *
1070             * @return String
1071             */
1072            protected String stringizeIntArray(int[] a) {
1073                    if (a == null) {
1074                            return "null";
1075                    }
1076    
1077                    StringBundler sb = new StringBundler(2 * a.length + 1);
1078    
1079                    sb.append("[");
1080    
1081                    for (int i = 0; i < a.length; i++) {
1082                            if (i != 0) {
1083                                    sb.append(",");
1084                            }
1085    
1086                            sb.append(a[i]);
1087                    }
1088    
1089                    sb.append("]");
1090    
1091                    return sb.toString();
1092            }
1093    
1094            /**
1095             * Field byDay
1096             */
1097            protected DayAndPosition[] byDay;
1098    
1099            /**
1100             * Field byMonth
1101             */
1102            protected int[] byMonth;
1103    
1104            /**
1105             * Field byMonthDay
1106             */
1107            protected int[] byMonthDay;
1108    
1109            /**
1110             * Field byWeekNo
1111             */
1112            protected int[] byWeekNo;
1113    
1114            /**
1115             * Field byYearDay
1116             */
1117            protected int[] byYearDay;
1118    
1119            /**
1120             * Field dtStart
1121             */
1122            protected Calendar dtStart;
1123    
1124            /**
1125             * Field duration
1126             */
1127            protected Duration duration;
1128    
1129            /**
1130             * Field frequency
1131             */
1132            protected int frequency;
1133    
1134            /**
1135             * Field interval
1136             */
1137            protected int interval;
1138    
1139            /**
1140             * Field interval
1141             */
1142            protected int occurrence = 0;
1143    
1144            /**
1145             * Field until
1146             */
1147            protected Calendar until;
1148    
1149    }