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