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