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