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