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