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