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