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