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