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