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