1   /**
2    * Copyright (c) 2000-2010 Liferay, Inc. All rights reserved.
3    *
4    * The contents of this file are subject to the terms of the Liferay Enterprise
5    * Subscription License ("License"). You may not use this file except in
6    * compliance with the License. You can obtain a copy of the License by
7    * contacting Liferay, Inc. See the License for the specific language governing
8    * permissions and limitations under the License, including but not limited to
9    * distribution rights of the Software.
10   *
11   *
12   *
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  import com.liferay.portal.kernel.util.TimeZoneUtil;
51  
52  import java.io.Serializable;
53  
54  import java.util.Calendar;
55  import java.util.Date;
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 static final int DAILY = 3;
70  
71      /**
72       * Field WEEKLY
73       */
74      public static final int WEEKLY = 4;
75  
76      /**
77       * Field MONTHLY
78       */
79      public static final int MONTHLY = 5;
80  
81      /**
82       * Field YEARLY
83       */
84      public static final int YEARLY = 6;
85  
86      /**
87       * Field NO_RECURRENCE
88       */
89      public static final 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                 TimeZoneUtil.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(TimeZoneUtil.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(TimeZoneUtil.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(TimeZoneUtil.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(TimeZoneUtil.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(TimeZoneUtil.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 = Calendar.DAY_OF_MONTH;
966 
967         if (position > 0) {
968             int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
969 
970             return (position == candidatePosition);
971         }
972         else {
973 
974             /* position < 0 */
975 
976             int negativeCandidatePosition =
977                 ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
978                 + 1;
979 
980             return (-position == negativeCandidatePosition);
981         }
982     }
983 
984     /**
985      * Method matchesByField
986      *
987      * @return boolean
988      */
989     protected static boolean matchesByField(int[] array, int field,
990                                             Calendar candidate,
991                                             boolean allowNegative) {
992         if ((array == null) || (array.length == 0)) {
993 
994             /* No rules, so it matches trivially */
995 
996             return true;
997         }
998 
999         int i;
1000
1001        for (i = 0; i < array.length; i++) {
1002            int val;
1003
1004            if (allowNegative && (array[i] < 0)) {
1005
1006                // byMonthDay = -1, in a 31-day month, means 31
1007
1008                int max = candidate.getActualMaximum(field);
1009
1010                val = (max + 1) + array[i];
1011            }
1012            else {
1013                val = array[i];
1014            }
1015
1016            if (val == candidate.get(field)) {
1017                return true;
1018            }
1019        }
1020
1021        return false;
1022    }
1023
1024    /**
1025     * Method matchesByMonthDay
1026     *
1027     * @return boolean
1028     */
1029    protected boolean matchesByMonthDay(Calendar candidate) {
1030        return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1031    }
1032
1033    /**
1034     * Method matchesByYearDay
1035     *
1036     * @return boolean
1037     */
1038    protected boolean matchesByYearDay(Calendar candidate) {
1039        return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1040    }
1041
1042    /**
1043     * Method matchesByWeekNo
1044     *
1045     * @return boolean
1046     */
1047    protected boolean matchesByWeekNo(Calendar candidate) {
1048        return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1049    }
1050
1051    /**
1052     * Method matchesByMonth
1053     *
1054     * @return boolean
1055     */
1056    protected boolean matchesByMonth(Calendar candidate) {
1057        return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1058    }
1059
1060    /**
1061     * Method toString
1062     *
1063     * @return String
1064     */
1065    public String toString() {
1066        StringBundler sb = new StringBundler();
1067
1068        sb.append(getClass().getName());
1069        sb.append("[dtStart=");
1070        sb.append((dtStart != null) ? dtStart.toString() : "null");
1071        sb.append(",duration=");
1072        sb.append((duration != null) ? duration.toString() : "null");
1073        sb.append(",frequency=");
1074        sb.append(frequency);
1075        sb.append(",interval=");
1076        sb.append(interval);
1077        sb.append(",until=");
1078        sb.append((until != null) ? until.toString() : "null");
1079        sb.append(",byDay=");
1080
1081        if (byDay == null) {
1082            sb.append("null");
1083        }
1084        else {
1085            sb.append("[");
1086
1087            for (int i = 0; i < byDay.length; i++) {
1088                if (i != 0) {
1089                    sb.append(",");
1090                }
1091
1092                if (byDay[i] != null) {
1093                    sb.append(byDay[i].toString());
1094                }
1095                else {
1096                    sb.append("null");
1097                }
1098            }
1099
1100            sb.append("]");
1101        }
1102
1103        sb.append(",byMonthDay=");
1104        sb.append(stringizeIntArray(byMonthDay));
1105        sb.append(",byYearDay=");
1106        sb.append(stringizeIntArray(byYearDay));
1107        sb.append(",byWeekNo=");
1108        sb.append(stringizeIntArray(byWeekNo));
1109        sb.append(",byMonth=");
1110        sb.append(stringizeIntArray(byMonth));
1111        sb.append(']');
1112
1113        return sb.toString();
1114    }
1115
1116    /**
1117     * Method stringizeIntArray
1118     *
1119     * @return String
1120     */
1121    private String stringizeIntArray(int[] a) {
1122        if (a == null) {
1123            return "null";
1124        }
1125
1126        StringBundler sb = new StringBundler(2 * a.length + 1);
1127
1128        sb.append("[");
1129
1130        for (int i = 0; i < a.length; i++) {
1131            if (i != 0) {
1132                sb.append(",");
1133            }
1134
1135            sb.append(a[i]);
1136        }
1137
1138        sb.append("]");
1139
1140        return sb.toString();
1141    }
1142
1143}