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