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