001    /**
002     * Copyright (c) 2000-2012 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    /*
016     * Copyright (c) 2000, Columbia University.  All rights reserved.
017     *
018     * Redistribution and use in source and binary forms, with or without
019     * modification, are permitted provided that the following conditions are met:
020     *
021     * 1. Redistributions of source code must retain the above copyright
022     *        notice, this list of conditions and the following disclaimer.
023     *
024     * 2. Redistributions in binary form must reproduce the above copyright
025     *        notice, this list of conditions and the following disclaimer in the
026     *        documentation and/or other materials provided with the distribution.
027     *
028     * 3. Neither the name of the University nor the names of its contributors
029     *        may be used to endorse or promote products derived from this software
030     *        without specific prior written permission.
031     *
032     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
033     * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
034     * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
035     * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
036     * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
037     * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
038     * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
039     * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
040     * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
041     * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
042     * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
043     */
044    
045    package com.liferay.util.cal;
046    
047    import com.liferay.portal.kernel.util.CalendarFactoryUtil;
048    import com.liferay.portal.kernel.util.StringBundler;
049    import com.liferay.portal.kernel.util.StringPool;
050    import com.liferay.portal.kernel.util.TimeZoneUtil;
051    
052    import java.io.Serializable;
053    
054    import java.util.Calendar;
055    import java.util.Date;
056    
057    /**
058     * @author     Jonathan Lennox
059     * @deprecated Moved to {@link com.liferay.portal.kernel.cal.Recurrence}
060     */
061    public class Recurrence implements Serializable {
062    
063            /**
064             * Field DAILY
065             */
066            public static final int DAILY = 3;
067    
068            /**
069             * Field WEEKLY
070             */
071            public static final int WEEKLY = 4;
072    
073            /**
074             * Field MONTHLY
075             */
076            public static final int MONTHLY = 5;
077    
078            /**
079             * Field YEARLY
080             */
081            public static final int YEARLY = 6;
082    
083            /**
084             * Field NO_RECURRENCE
085             */
086            public static final int NO_RECURRENCE = 7;
087    
088            /**
089             * Field dtStart
090             */
091            protected Calendar dtStart;
092    
093            /**
094             * Field duration
095             */
096            protected Duration duration;
097    
098            /**
099             * Field frequency
100             */
101            protected int frequency;
102    
103            /**
104             * Field interval
105             */
106            protected int interval;
107    
108            /**
109             * Field interval
110             */
111            protected int occurrence = 0;
112    
113            /**
114             * Field until
115             */
116            protected Calendar until;
117    
118            /**
119             * Field byDay
120             */
121            protected DayAndPosition[] byDay;
122    
123            /**
124             * Field byMonthDay
125             */
126            protected int[] byMonthDay;
127    
128            /**
129             * Field byYearDay
130             */
131            protected int[] byYearDay;
132    
133            /**
134             * Field byWeekNo
135             */
136            protected int[] byWeekNo;
137    
138            /**
139             * Field byMonth
140             */
141            protected int[] byMonth;
142    
143            /**
144             * Constructor Recurrence
145             */
146            public Recurrence() {
147                    this(null, new Duration(), NO_RECURRENCE);
148            }
149    
150            /**
151             * Constructor Recurrence
152             */
153            public Recurrence(Calendar start, Duration dur) {
154                    this(start, dur, NO_RECURRENCE);
155            }
156    
157            /**
158             * Constructor Recurrence
159             */
160            public Recurrence(Calendar start, Duration dur, int freq) {
161                    setDtStart(start);
162    
163                    duration = (Duration)dur.clone();
164                    frequency = freq;
165                    interval = 1;
166            }
167    
168            /* Accessors */
169    
170            /**
171             * Method getDtStart
172             *
173             * @return Calendar
174             */
175            public Calendar getDtStart() {
176                    return (Calendar)dtStart.clone();
177            }
178    
179            /**
180             * Method setDtStart
181             */
182            public void setDtStart(Calendar start) {
183                    int oldStart;
184    
185                    if (dtStart != null) {
186                            oldStart = dtStart.getFirstDayOfWeek();
187                    }
188                    else {
189                            oldStart = Calendar.MONDAY;
190                    }
191    
192                    if (start == null) {
193                            dtStart = CalendarFactoryUtil.getCalendar(
194                                    TimeZoneUtil.getTimeZone(StringPool.UTC));
195    
196                            dtStart.setTime(new Date(0L));
197                    }
198                    else {
199                            dtStart = (Calendar)start.clone();
200    
201                            dtStart.clear(Calendar.ZONE_OFFSET);
202                            dtStart.clear(Calendar.DST_OFFSET);
203                            dtStart.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
204                    }
205    
206                    dtStart.setMinimalDaysInFirstWeek(4);
207                    dtStart.setFirstDayOfWeek(oldStart);
208            }
209    
210            /**
211             * Method getDuration
212             *
213             * @return Duration
214             */
215            public Duration getDuration() {
216                    return (Duration)duration.clone();
217            }
218    
219            /**
220             * Method setDuration
221             */
222            public void setDuration(Duration d) {
223                    duration = (Duration)d.clone();
224            }
225    
226            /**
227             * Method getDtEnd
228             *
229             * @return Calendar
230             */
231            public Calendar getDtEnd() {
232    
233                    /*
234                     * Make dtEnd a cloned dtStart, so non-time fields of the Calendar
235                     * are accurate.
236                     */
237                    Calendar tempEnd = (Calendar)dtStart.clone();
238    
239                    tempEnd.setTime(new Date(dtStart.getTime().getTime()
240                                                                     + duration.getInterval()));
241    
242                    return tempEnd;
243            }
244    
245            /**
246             * Method setDtEnd
247             */
248            public void setDtEnd(Calendar end) {
249                    Calendar tempEnd = (Calendar)end.clone();
250    
251                    tempEnd.clear(Calendar.ZONE_OFFSET);
252                    tempEnd.clear(Calendar.DST_OFFSET);
253                    tempEnd.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
254                    duration.setInterval(tempEnd.getTime().getTime()
255                                                             - dtStart.getTime().getTime());
256            }
257    
258            /**
259             * Method getFrequency
260             *
261             * @return int
262             */
263            public int getFrequency() {
264                    return frequency;
265            }
266    
267            /**
268             * Method setFrequency
269             */
270            public void setFrequency(int freq) {
271                    if ((frequency != DAILY) && (frequency != WEEKLY)
272                            && (frequency != MONTHLY) && (frequency != YEARLY)
273                            && (frequency != NO_RECURRENCE)) {
274                            throw new IllegalArgumentException("Invalid frequency");
275                    }
276    
277                    frequency = freq;
278            }
279    
280            /**
281             * Method getInterval
282             *
283             * @return int
284             */
285            public int getInterval() {
286                    return interval;
287            }
288    
289            /**
290             * Method setInterval
291             */
292            public void setInterval(int intr) {
293                    interval = (intr > 0) ? intr : 1;
294            }
295    
296            /**
297             * Method getOccurrence
298             *
299             * @return int
300             */
301            public int getOccurrence() {
302                    return occurrence;
303            }
304    
305            /**
306             * Method setOccurrence
307             */
308            public void setOccurrence(int occur) {
309                    occurrence = occur;
310            }
311    
312            /**
313             * Method getUntil
314             *
315             * @return Calendar
316             */
317            public Calendar getUntil() {
318                    return ((until != null) ? (Calendar)until.clone() : null);
319            }
320    
321            /**
322             * Method setUntil
323             */
324            public void setUntil(Calendar u) {
325                    if (u == null) {
326                            until = null;
327    
328                            return;
329                    }
330    
331                    until = (Calendar)u.clone();
332    
333                    until.clear(Calendar.ZONE_OFFSET);
334                    until.clear(Calendar.DST_OFFSET);
335                    until.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
336            }
337    
338            /**
339             * Method getWeekStart
340             *
341             * @return int
342             */
343            public int getWeekStart() {
344                    return dtStart.getFirstDayOfWeek();
345            }
346    
347            /**
348             * Method setWeekStart
349             */
350            public void setWeekStart(int weekstart) {
351                    dtStart.setFirstDayOfWeek(weekstart);
352            }
353    
354            /**
355             * Method getByDay
356             *
357             * @return DayAndPosition[]
358             */
359            public DayAndPosition[] getByDay() {
360                    if (byDay == null) {
361                            return null;
362                    }
363    
364                    DayAndPosition[] b = new DayAndPosition[byDay.length];
365    
366                    /*
367                     * System.arraycopy isn't good enough -- we want to clone each
368                     * individual element.
369                     */
370                    for (int i = 0; i < byDay.length; i++) {
371                            b[i] = (DayAndPosition)byDay[i].clone();
372                    }
373    
374                    return b;
375            }
376    
377            /**
378             * Method setByDay
379             */
380            public void setByDay(DayAndPosition[] b) {
381                    if (b == null) {
382                            byDay = null;
383    
384                            return;
385                    }
386    
387                    byDay = new DayAndPosition[b.length];
388    
389                    /*
390                     * System.arraycopy isn't good enough -- we want to clone each
391                     * individual element.
392                     */
393                    for (int i = 0; i < b.length; i++) {
394                            byDay[i] = (DayAndPosition)b[i].clone();
395                    }
396            }
397    
398            /**
399             * Method getByMonthDay
400             *
401             * @return int[]
402             */
403            public int[] getByMonthDay() {
404                    if (byMonthDay == null) {
405                            return null;
406                    }
407    
408                    int[] b = new int[byMonthDay.length];
409    
410                    System.arraycopy(byMonthDay, 0, b, 0, byMonthDay.length);
411    
412                    return b;
413            }
414    
415            /**
416             * Method setByMonthDay
417             */
418            public void setByMonthDay(int[] b) {
419                    if (b == null) {
420                            byMonthDay = null;
421    
422                            return;
423                    }
424    
425                    byMonthDay = new int[b.length];
426    
427                    System.arraycopy(b, 0, byMonthDay, 0, b.length);
428            }
429    
430            /**
431             * Method getByYearDay
432             *
433             * @return int[]
434             */
435            public int[] getByYearDay() {
436                    if (byYearDay == null) {
437                            return null;
438                    }
439    
440                    int[] b = new int[byYearDay.length];
441    
442                    System.arraycopy(byYearDay, 0, b, 0, byYearDay.length);
443    
444                    return b;
445            }
446    
447            /**
448             * Method setByYearDay
449             */
450            public void setByYearDay(int[] b) {
451                    if (b == null) {
452                            byYearDay = null;
453    
454                            return;
455                    }
456    
457                    byYearDay = new int[b.length];
458    
459                    System.arraycopy(b, 0, byYearDay, 0, b.length);
460            }
461    
462            /**
463             * Method getByWeekNo
464             *
465             * @return int[]
466             */
467            public int[] getByWeekNo() {
468                    if (byWeekNo == null) {
469                            return null;
470                    }
471    
472                    int[] b = new int[byWeekNo.length];
473    
474                    System.arraycopy(byWeekNo, 0, b, 0, byWeekNo.length);
475    
476                    return b;
477            }
478    
479            /**
480             * Method setByWeekNo
481             */
482            public void setByWeekNo(int[] b) {
483                    if (b == null) {
484                            byWeekNo = null;
485    
486                            return;
487                    }
488    
489                    byWeekNo = new int[b.length];
490    
491                    System.arraycopy(b, 0, byWeekNo, 0, b.length);
492            }
493    
494            /**
495             * Method getByMonth
496             *
497             * @return int[]
498             */
499            public int[] getByMonth() {
500                    if (byMonth == null) {
501                            return null;
502                    }
503    
504                    int[] b = new int[byMonth.length];
505    
506                    System.arraycopy(byMonth, 0, b, 0, byMonth.length);
507    
508                    return b;
509            }
510    
511            /**
512             * Method setByMonth
513             */
514            public void setByMonth(int[] b) {
515                    if (b == null) {
516                            byMonth = null;
517    
518                            return;
519                    }
520    
521                    byMonth = new int[b.length];
522    
523                    System.arraycopy(b, 0, byMonth, 0, b.length);
524            }
525    
526            /**
527             * Method isInRecurrence
528             *
529             * @return boolean
530             */
531            public boolean isInRecurrence(Calendar current) {
532                    return isInRecurrence(current, false);
533            }
534    
535            /**
536             * Method isInRecurrence
537             *
538             * @return boolean
539             */
540            public boolean isInRecurrence(Calendar current, boolean debug) {
541                    Calendar myCurrent = (Calendar)current.clone();
542    
543                    // Do all calculations in GMT.  Keep other parameters consistent.
544    
545                    myCurrent.clear(Calendar.ZONE_OFFSET);
546                    myCurrent.clear(Calendar.DST_OFFSET);
547                    myCurrent.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
548                    myCurrent.setMinimalDaysInFirstWeek(4);
549                    myCurrent.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
550    
551                    if (myCurrent.getTime().getTime() < dtStart.getTime().getTime()) {
552    
553                            // The current time is earlier than the start time.
554    
555                            if (debug) {
556                                    System.err.println("current < start");
557                            }
558    
559                            return false;
560                    }
561    
562                    Calendar candidate = getCandidateStartTime(myCurrent);
563    
564                    /* Loop over ranges for the duration. */
565    
566                    while (candidate.getTime().getTime() + duration.getInterval()
567                               > myCurrent.getTime().getTime()) {
568                            if (candidateIsInRecurrence(candidate, debug)) {
569                                    return true;
570                            }
571    
572                            /* Roll back to one second previous, and try again. */
573    
574                            candidate.add(Calendar.SECOND, -1);
575    
576                            /* Make sure we haven't rolled back to before dtStart. */
577    
578                            if (candidate.getTime().getTime() < dtStart.getTime().getTime()) {
579                                    if (debug) {
580                                            System.err.println("No candidates after dtStart");
581                                    }
582    
583                                    return false;
584                            }
585    
586                            candidate = getCandidateStartTime(candidate);
587                    }
588    
589                    if (debug) {
590                            System.err.println("No matching candidates");
591                    }
592    
593                    return false;
594            }
595    
596            /**
597             * Method candidateIsInRecurrence
598             *
599             * @return boolean
600             */
601            protected boolean candidateIsInRecurrence(Calendar candidate,
602                                                                                              boolean debug) {
603                    if ((until != null)
604                            && (candidate.getTime().getTime() > until.getTime().getTime())) {
605    
606                            // After "until"
607    
608                            if (debug) {
609                                    System.err.println("after until");
610                            }
611    
612                            return false;
613                    }
614    
615                    if (getRecurrenceCount(candidate) % interval != 0) {
616    
617                            // Not a repetition of the interval
618    
619                            if (debug) {
620                                    System.err.println("not an interval rep");
621                            }
622    
623                            return false;
624                    }
625                    else if ((occurrence > 0) &&
626                                     (getRecurrenceCount(candidate) >= occurrence)) {
627    
628                            return false;
629                    }
630    
631                    if (!matchesByDay(candidate) ||!matchesByMonthDay(candidate)
632                            ||!matchesByYearDay(candidate) ||!matchesByWeekNo(candidate)
633                            ||!matchesByMonth(candidate)) {
634    
635                            // Doesn't match a by* rule
636    
637                            if (debug) {
638                                    System.err.println("doesn't match a by*");
639                            }
640    
641                            return false;
642                    }
643    
644                    if (debug) {
645                            System.err.println("All checks succeeded");
646                    }
647    
648                    return true;
649            }
650    
651            /**
652             * Method getMinimumInterval
653             *
654             * @return int
655             */
656            protected int getMinimumInterval() {
657                    if ((frequency == DAILY) || (byDay != null) || (byMonthDay != null)
658                            || (byYearDay != null)) {
659                            return DAILY;
660                    }
661                    else if ((frequency == WEEKLY) || (byWeekNo != null)) {
662                            return WEEKLY;
663                    }
664                    else if ((frequency == MONTHLY) || (byMonth != null)) {
665                            return MONTHLY;
666                    }
667                    else if (frequency == YEARLY) {
668                            return YEARLY;
669                    }
670                    else if (frequency == NO_RECURRENCE) {
671                            return NO_RECURRENCE;
672                    }
673                    else {
674    
675                            // Shouldn't happen
676    
677                            throw new IllegalStateException(
678                                    "Internal error: Unknown frequency value");
679                    }
680            }
681    
682            /**
683             * Method getCandidateStartTime
684             *
685             * @return Calendar
686             */
687            public Calendar getCandidateStartTime(Calendar current) {
688                    if (dtStart.getTime().getTime() > current.getTime().getTime()) {
689                            throw new IllegalArgumentException("Current time before DtStart");
690                    }
691    
692                    int minInterval = getMinimumInterval();
693                    Calendar candidate = (Calendar)current.clone();
694    
695                    if (true) {
696    
697                            // This block is only needed while this function is public...
698    
699                            candidate.clear(Calendar.ZONE_OFFSET);
700                            candidate.clear(Calendar.DST_OFFSET);
701                            candidate.setTimeZone(TimeZoneUtil.getTimeZone(StringPool.UTC));
702                            candidate.setMinimalDaysInFirstWeek(4);
703                            candidate.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
704                    }
705    
706                    if (frequency == NO_RECURRENCE) {
707                            candidate.setTime(dtStart.getTime());
708    
709                            return candidate;
710                    }
711    
712                    reduce_constant_length_field(Calendar.SECOND, dtStart, candidate);
713                    reduce_constant_length_field(Calendar.MINUTE, dtStart, candidate);
714                    reduce_constant_length_field(Calendar.HOUR_OF_DAY, dtStart, candidate);
715    
716                    switch (minInterval) {
717    
718                            case DAILY :
719    
720                                    /* No more adjustments needed */
721    
722                                    break;
723    
724                            case WEEKLY :
725                                    reduce_constant_length_field(Calendar.DAY_OF_WEEK, dtStart,
726                                                                                             candidate);
727                                    break;
728    
729                            case MONTHLY :
730                                    reduce_day_of_month(dtStart, candidate);
731                                    break;
732    
733                            case YEARLY :
734                                    reduce_day_of_year(dtStart, candidate);
735                                    break;
736                    }
737    
738                    return candidate;
739            }
740    
741            /**
742             * Method reduce_constant_length_field
743             */
744            protected static void reduce_constant_length_field(int field,
745                                                                                                               Calendar start,
746                                                                                                               Calendar candidate) {
747                    if ((start.getMaximum(field) != start.getLeastMaximum(field))
748                            || (start.getMinimum(field) != start.getGreatestMinimum(field))) {
749                            throw new IllegalArgumentException("Not a constant length field");
750                    }
751    
752                    int fieldLength = (start.getMaximum(field) - start.getMinimum(field)
753                                                       + 1);
754                    int delta = start.get(field) - candidate.get(field);
755    
756                    if (delta > 0) {
757                            delta -= fieldLength;
758                    }
759    
760                    candidate.add(field, delta);
761            }
762    
763            /**
764             * Method reduce_day_of_month
765             */
766            protected static void reduce_day_of_month(Calendar start,
767                                                                                              Calendar candidate) {
768                    Calendar tempCal = (Calendar)candidate.clone();
769    
770                    tempCal.add(Calendar.MONTH, -1);
771    
772                    int delta = start.get(Calendar.DATE) - candidate.get(Calendar.DATE);
773    
774                    if (delta > 0) {
775                            delta -= tempCal.getActualMaximum(Calendar.DATE);
776                    }
777    
778                    candidate.add(Calendar.DATE, delta);
779    
780                    while (start.get(Calendar.DATE) != candidate.get(Calendar.DATE)) {
781                            tempCal.add(Calendar.MONTH, -1);
782                            candidate.add(Calendar.DATE,
783                                                      -tempCal.getActualMaximum(Calendar.DATE));
784                    }
785            }
786    
787            /**
788             * Method reduce_day_of_year
789             */
790            protected static void reduce_day_of_year(Calendar start,
791                                                                                             Calendar candidate) {
792                    if ((start.get(Calendar.MONTH) > candidate.get(Calendar.MONTH))
793                            || ((start.get(Calendar.MONTH) == candidate.get(Calendar.MONTH))
794                                    && (start.get(Calendar.DATE) > candidate.get(Calendar.DATE)))) {
795                            candidate.add(Calendar.YEAR, -1);
796                    }
797    
798                    /* Set the candidate date to the start date. */
799    
800                    candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
801                    candidate.set(Calendar.DATE, start.get(Calendar.DATE));
802    
803                    while ((start.get(Calendar.MONTH) != candidate.get(Calendar.MONTH))
804                               || (start.get(Calendar.DATE) != candidate.get(Calendar.DATE))) {
805                            candidate.add(Calendar.YEAR, -1);
806                            candidate.set(Calendar.MONTH, start.get(Calendar.MONTH));
807                            candidate.set(Calendar.DATE, start.get(Calendar.DATE));
808                    }
809            }
810    
811            /**
812             * Method getRecurrenceCount
813             *
814             * @return int
815             */
816            protected int getRecurrenceCount(Calendar candidate) {
817                    switch (frequency) {
818    
819                            case NO_RECURRENCE :
820                                    return 0;
821    
822                            case DAILY :
823                                    return (int)(getDayNumber(candidate) - getDayNumber(dtStart));
824    
825                            case WEEKLY :
826                                    Calendar tempCand = (Calendar)candidate.clone();
827    
828                                    tempCand.setFirstDayOfWeek(dtStart.getFirstDayOfWeek());
829    
830                                    return (int)(getWeekNumber(tempCand) - getWeekNumber(dtStart));
831    
832                            case MONTHLY :
833                                    return (int)(getMonthNumber(candidate)
834                                                             - getMonthNumber(dtStart));
835    
836                            case YEARLY :
837                                    return candidate.get(Calendar.YEAR)
838                                               - dtStart.get(Calendar.YEAR);
839    
840                            default :
841                                    throw new IllegalStateException("bad frequency internally...");
842                    }
843            }
844    
845            /**
846             * Method getDayNumber
847             *
848             * @return long
849             */
850            protected static long getDayNumber(Calendar cal) {
851                    Calendar tempCal = (Calendar)cal.clone();
852    
853                    // Set to midnight, GMT
854    
855                    tempCal.set(Calendar.MILLISECOND, 0);
856                    tempCal.set(Calendar.SECOND, 0);
857                    tempCal.set(Calendar.MINUTE, 0);
858                    tempCal.set(Calendar.HOUR_OF_DAY, 0);
859    
860                    return tempCal.getTime().getTime() / (24 * 60 * 60 * 1000);
861            }
862    
863            /**
864             * Method getWeekNumber
865             *
866             * @return long
867             */
868            protected static long getWeekNumber(Calendar cal) {
869                    Calendar tempCal = (Calendar)cal.clone();
870    
871                    // Set to midnight, GMT
872    
873                    tempCal.set(Calendar.MILLISECOND, 0);
874                    tempCal.set(Calendar.SECOND, 0);
875                    tempCal.set(Calendar.MINUTE, 0);
876                    tempCal.set(Calendar.HOUR_OF_DAY, 0);
877    
878                    // Roll back to the first day of the week
879    
880                    int delta = tempCal.getFirstDayOfWeek()
881                                            - tempCal.get(Calendar.DAY_OF_WEEK);
882    
883                    if (delta > 0) {
884                            delta -= 7;
885                    }
886    
887                    // tempCal now points to the first instant of this week.
888    
889                    // Calculate the "week epoch" -- the weekstart day closest to January 1,
890                    // 1970 (which was a Thursday)
891    
892                    long weekEpoch = (tempCal.getFirstDayOfWeek() - Calendar.THURSDAY) * 24L
893                                                     * 60 * 60 * 1000L;
894    
895                    return (tempCal.getTime().getTime() - weekEpoch)
896                               / (7 * 24 * 60 * 60 * 1000);
897            }
898    
899            /**
900             * Method getMonthNumber
901             *
902             * @return long
903             */
904            protected static long getMonthNumber(Calendar cal) {
905                    return (cal.get(Calendar.YEAR) - 1970) * 12L
906                               + (cal.get(Calendar.MONTH) - Calendar.JANUARY);
907            }
908    
909            /**
910             * Method matchesByDay
911             *
912             * @return boolean
913             */
914            protected boolean matchesByDay(Calendar candidate) {
915                    if ((byDay == null) || (byDay.length == 0)) {
916    
917                            /* No byDay rules, so it matches trivially */
918    
919                            return true;
920                    }
921    
922                    int i;
923    
924                    for (i = 0; i < byDay.length; i++) {
925                            if (matchesIndividualByDay(candidate, byDay[i])) {
926                                    return true;
927                            }
928                    }
929    
930                    return false;
931            }
932    
933            /**
934             * Method matchesIndividualByDay
935             *
936             * @return boolean
937             */
938            protected boolean matchesIndividualByDay(Calendar candidate,
939                                                                                             DayAndPosition pos) {
940                    if (pos.getDayOfWeek() != candidate.get(Calendar.DAY_OF_WEEK)) {
941                            return false;
942                    }
943    
944                    int position = pos.getDayPosition();
945    
946                    if (position == 0) {
947                            return true;
948                    }
949    
950                    int field = Calendar.DAY_OF_MONTH;
951    
952                    if (position > 0) {
953                            int candidatePosition = ((candidate.get(field) - 1) / 7) + 1;
954    
955                            return (position == candidatePosition);
956                    }
957                    else {
958    
959                            /* position < 0 */
960    
961                            int negativeCandidatePosition =
962                                    ((candidate.getActualMaximum(field) - candidate.get(field)) / 7)
963                                    + 1;
964    
965                            return (-position == negativeCandidatePosition);
966                    }
967            }
968    
969            /**
970             * Method matchesByField
971             *
972             * @return boolean
973             */
974            protected static boolean matchesByField(int[] array, int field,
975                                                                                            Calendar candidate,
976                                                                                            boolean allowNegative) {
977                    if ((array == null) || (array.length == 0)) {
978    
979                            /* No rules, so it matches trivially */
980    
981                            return true;
982                    }
983    
984                    int i;
985    
986                    for (i = 0; i < array.length; i++) {
987                            int val;
988    
989                            if (allowNegative && (array[i] < 0)) {
990    
991                                    // byMonthDay = -1, in a 31-day month, means 31
992    
993                                    int max = candidate.getActualMaximum(field);
994    
995                                    val = (max + 1) + array[i];
996                            }
997                            else {
998                                    val = array[i];
999                            }
1000    
1001                            if (val == candidate.get(field)) {
1002                                    return true;
1003                            }
1004                    }
1005    
1006                    return false;
1007            }
1008    
1009            /**
1010             * Method matchesByMonthDay
1011             *
1012             * @return boolean
1013             */
1014            protected boolean matchesByMonthDay(Calendar candidate) {
1015                    return matchesByField(byMonthDay, Calendar.DATE, candidate, true);
1016            }
1017    
1018            /**
1019             * Method matchesByYearDay
1020             *
1021             * @return boolean
1022             */
1023            protected boolean matchesByYearDay(Calendar candidate) {
1024                    return matchesByField(byYearDay, Calendar.DAY_OF_YEAR, candidate, true);
1025            }
1026    
1027            /**
1028             * Method matchesByWeekNo
1029             *
1030             * @return boolean
1031             */
1032            protected boolean matchesByWeekNo(Calendar candidate) {
1033                    return matchesByField(byWeekNo, Calendar.WEEK_OF_YEAR, candidate, true);
1034            }
1035    
1036            /**
1037             * Method matchesByMonth
1038             *
1039             * @return boolean
1040             */
1041            protected boolean matchesByMonth(Calendar candidate) {
1042                    return matchesByField(byMonth, Calendar.MONTH, candidate, false);
1043            }
1044    
1045            /**
1046             * Method toString
1047             *
1048             * @return String
1049             */
1050            @Override
1051            public String toString() {
1052                    StringBundler sb = new StringBundler();
1053    
1054                    sb.append(getClass().getName());
1055                    sb.append("[dtStart=");
1056                    sb.append((dtStart != null) ? dtStart.toString() : "null");
1057                    sb.append(",duration=");
1058                    sb.append((duration != null) ? duration.toString() : "null");
1059                    sb.append(",frequency=");
1060                    sb.append(frequency);
1061                    sb.append(",interval=");
1062                    sb.append(interval);
1063                    sb.append(",until=");
1064                    sb.append((until != null) ? until.toString() : "null");
1065                    sb.append(",byDay=");
1066    
1067                    if (byDay == null) {
1068                            sb.append("null");
1069                    }
1070                    else {
1071                            sb.append("[");
1072    
1073                            for (int i = 0; i < byDay.length; i++) {
1074                                    if (i != 0) {
1075                                            sb.append(",");
1076                                    }
1077    
1078                                    if (byDay[i] != null) {
1079                                            sb.append(byDay[i].toString());
1080                                    }
1081                                    else {
1082                                            sb.append("null");
1083                                    }
1084                            }
1085    
1086                            sb.append("]");
1087                    }
1088    
1089                    sb.append(",byMonthDay=");
1090                    sb.append(stringizeIntArray(byMonthDay));
1091                    sb.append(",byYearDay=");
1092                    sb.append(stringizeIntArray(byYearDay));
1093                    sb.append(",byWeekNo=");
1094                    sb.append(stringizeIntArray(byWeekNo));
1095                    sb.append(",byMonth=");
1096                    sb.append(stringizeIntArray(byMonth));
1097                    sb.append(']');
1098    
1099                    return sb.toString();
1100            }
1101    
1102            /**
1103             * Method stringizeIntArray
1104             *
1105             * @return String
1106             */
1107            private String stringizeIntArray(int[] a) {
1108                    if (a == null) {
1109                            return "null";
1110                    }
1111    
1112                    StringBundler sb = new StringBundler(2 * a.length + 1);
1113    
1114                    sb.append("[");
1115    
1116                    for (int i = 0; i < a.length; i++) {
1117                            if (i != 0) {
1118                                    sb.append(",");
1119                            }
1120    
1121                            sb.append(a[i]);
1122                    }
1123    
1124                    sb.append("]");
1125    
1126                    return sb.toString();
1127            }
1128    
1129    }