/// <summary> /// This is used to handle the expansion of the yearly frequency /// </summary> /// <param name="dates">The collection in which to put the dates</param> /// <remarks>The spec is rather vague about how all the rules should interact so I'm making some best /// guesses here based on the examples in the spec itself although not all combinations are shown. /// </remarks> private void ExpandYearly(RecurDateTimeCollection dates) { RecurDateTimeCollection rdtcMonth = null, rdtcMoDay = null, rdtcWeek = null, rdtcYrDay = null, rdtcDay = null; bool isExpanded = false; // We'll expand each rule individually and combine the results before applying the time expansions. // The application of the BYMONTHDAY and BYDAY rules varies based on whatever other rule parts are // present as well. if(byMonth.Count != 0) { // Expand by month isExpanded = true; rdtcMonth = new RecurDateTimeCollection(dates); freqRules.ByMonth(this, rdtcMonth); // If BYMONTHDAY and BYDAY are both specified, we need to expand by month day and then filter by // day. If we expand by day alone, note that we do so only in the months specified in the // BYMONTH rule. if(byMonthDay.Count != 0 && byDay.Count != 0) { Expand.ByMonthDay(this, rdtcMonth); Filter.ByDay(this, rdtcMonth); } else if(Expand.ByMonthDay(this, rdtcMonth) != 0) Expand.ByDayInMonths(this, rdtcMonth); } else { if(byMonthDay.Count != 0) { // Expand by month day if specified without any by month rule part isExpanded = true; rdtcMoDay = new RecurDateTimeCollection(dates); freqRules.ByMonthDay(this, rdtcMoDay); } // As long as by week number isn't specified either, we'll expand the by day rule here too if(byWeekNo.Count == 0) { isExpanded = true; rdtcDay = new RecurDateTimeCollection(dates); freqRules.ByDay(this, rdtcDay); } } if(byWeekNo.Count != 0) { // Expand by week number isExpanded = true; rdtcWeek = new RecurDateTimeCollection(dates); freqRules.ByWeekNo(this, rdtcWeek); // Expand by days of the week in those weeks Expand.ByDayInWeeks(this, rdtcWeek); } if(byYearDay.Count != 0) { // Expand by year day isExpanded = true; rdtcYrDay = new RecurDateTimeCollection(dates); freqRules.ByYearDay(this, rdtcYrDay); } // Combine the various expansions. If nothing was done, leave the original date in the collection. if(isExpanded) { dates.Clear(); if(rdtcMonth != null && rdtcMonth.Count != 0) dates.AddRange(rdtcMonth); if(rdtcMoDay != null && rdtcMoDay.Count != 0) dates.AddRange(rdtcMoDay); if(rdtcWeek != null && rdtcWeek.Count != 0) dates.AddRange(rdtcWeek); if(rdtcYrDay != null && rdtcYrDay.Count != 0) dates.AddRange(rdtcYrDay); if(rdtcDay != null && rdtcDay.Count != 0) dates.AddRange(rdtcDay); } // In any case, the time parts are easy. They always expand the instances if there's anything there. if(dates.Count != 0) { Expand.ByHour(this, dates); Expand.ByMinute(this, dates); Expand.BySecond(this, dates); } }
/// <summary> /// This method is used to return all recurring instances between the two specified date/times based on /// the current settings. /// </summary> /// <param name="fromDate">The minimum date/time on or after which instances should occur.</param> /// <param name="toDate">The maximum date/time on or before which instances should occur.</param> /// <returns>Returns a <see cref="DateTimeCollection"/> of <see cref="DateTime" /> objects that represent /// the instances found between the two specified date/times.</returns> private DateTimeCollection GenerateInstances(DateTime fromDate, DateTime toDate) { RecurDateTimeCollection rdtc; RecurDateTime rdt; int idx, count, lastYear = -1; DateTimeCollection dcDates = new DateTimeCollection(); // If undefined or if the requested range is outside that of the recurrence, don't bother. Just // return an empty collection. Note that for defined recurrences that use a count, we'll always // have to expand it. if(frequency == RecurFrequency.Undefined || startDate > toDate || untilDate < fromDate) return dcDates; RecurDateTime start = new RecurDateTime(startDate), end = new RecurDateTime(untilDate), from = new RecurDateTime(fromDate), to = new RecurDateTime(toDate); RecurDateTime current = freqRules.FindStart(this, start, end, from, to); // If there's nothing to generate, stop now if(current == null) return dcDates; rdtc = new RecurDateTimeCollection(); // Initialize the filtering arrays. These help speed up the filtering process by letting us do one // look up as opposed to comparing all elements in the collection. Array.Clear(isSecondUsed, 0, isSecondUsed.Length); Array.Clear(isMinuteUsed, 0, isMinuteUsed.Length); Array.Clear(isHourUsed, 0, isHourUsed.Length); Array.Clear(isDayUsed, 0, isDayUsed.Length); Array.Clear(isMonthDayUsed, 0, isMonthDayUsed.Length); Array.Clear(isNegMonthDayUsed, 0, isNegMonthDayUsed.Length); Array.Clear(isYearDayUsed, 0, isYearDayUsed.Length); Array.Clear(isNegYearDayUsed, 0, isNegYearDayUsed.Length); Array.Clear(isMonthUsed, 0, isMonthUsed.Length); if(bySecond.Count != 0) foreach(int second in bySecond) isSecondUsed[second] = true; if(byMinute.Count != 0) foreach(int minute in byMinute) isMinuteUsed[minute] = true; if(byHour.Count != 0) foreach(int hour in byHour) isHourUsed[hour] = true; if(byMonth.Count != 0) foreach(int month in byMonth) isMonthUsed[month - 1] = true; // When filtering, the instance is ignored if(byDay.Count != 0) foreach(DayInstance di in byDay) isDayUsed[(int)di.DayOfWeek] = true; // Negative days are from the end of the month if(byMonthDay.Count != 0) foreach(int monthDay in byMonthDay) if(monthDay > 0) isMonthDayUsed[monthDay] = true; else isNegMonthDayUsed[0 - monthDay] = true; // Negative days are from the end of the year if(byYearDay.Count != 0) foreach(int yearDay in byYearDay) if(yearDay > 0) isYearDayUsed[yearDay] = true; else isNegYearDayUsed[0 - yearDay] = true; do { rdtc.Clear(); rdtc.Add(current); // The spec is rather vague about how some of the rules are used together. For example, it says // that rule parts for a period of time less than the frequency generally expand it. However, // an example for the MONTHLY frequency shows that when BYMONTHDAY and BYDAY are used together, // BYDAY acts as a filter for BYMONTHDAY not an expansion of the frequency. When used by // themselves, the rules in question do act as expansions. There are no examples for the yearly // frequency that show how all of the various combinations interact so I'm making some // assumptions based on an evaluation of what makes the most sense. switch(frequency) { case RecurFrequency.Yearly: // This one gets rather messy so it's separate ExpandYearly(rdtc); break; case RecurFrequency.Monthly: if(freqRules.ByMonth(this, rdtc) != 0) if(freqRules.ByYearDay(this, rdtc) != 0) { // If BYMONTHDAY and BYDAY are specified, expand by month day and filter by day. // If one but not the other or neither is specified, handle them in order as // usual. if(byMonthDay.Count != 0 && byDay.Count != 0) { if(Expand.ByMonthDay(this, rdtc) != 0) if(Filter.ByDay(this, rdtc) != 0) { // These always expand if used Expand.ByHour(this, rdtc); Expand.ByMinute(this, rdtc); Expand.BySecond(this, rdtc); } } else if(Expand.ByMonthDay(this, rdtc) != 0) if(freqRules.ByDay(this, rdtc) != 0) { // These always expand if used Expand.ByHour(this, rdtc); Expand.ByMinute(this, rdtc); Expand.BySecond(this, rdtc); } } break; default: // Everything else is fairly straightforward. We just expand or filter based on the // frequency type and what rules are specified. if(freqRules.ByMonth(this, rdtc) != 0) if(freqRules.ByYearDay(this, rdtc) != 0) if(freqRules.ByMonthDay(this, rdtc) != 0) if(freqRules.ByDay(this, rdtc) != 0) if(freqRules.ByHour(this, rdtc) != 0) if(freqRules.ByMinute(this, rdtc) != 0) freqRules.BySecond(this, rdtc); break; } // Sort the dates and remove invalid and duplicate dates rdtc.Sort(); for(idx = 0, count = rdtc.Count; idx < count; idx++) { rdt = rdtc[idx]; // If not valid, discard it. if(!rdt.IsValidDate()) { rdtc.RemoveAt(idx); idx--; count--; continue; } // Discard it if it falls on a holiday if(!canOccurOnHoliday) { // If this is the first call or the year changes, get the holidays in the date's year // and the next year. if(holDates == null || lastYear != rdt.Year) { holDates = new HashSet<DateTime>(holidays.HolidaysBetween(rdt.Year, rdt.Year + 1)); lastYear = rdt.Year; } // Note that we only compare the date part as the holiday's time probably will not match // the recurrence's time. if(holDates.Contains(rdt.ToDateTime().Date)) { rdtc.RemoveAt(idx); idx--; count--; continue; } } // Discard it if it's a duplicate if(idx != 0 && rdt == rdtc[idx - 1]) { rdtc.RemoveAt(idx); idx--; count--; continue; } } if(rdtc.Count != 0) { // Apply the BYSETPOS rule and remove entries prior to the start or past the end of the // ranges. if(bySetPos.Count != 0) { foreach(int nPos in bySetPos) { // Invert negative values. They'll select elements indexed from the end of the // array. if(nPos < 0) idx = nPos + rdtc.Count; else idx = nPos - 1; if(idx >= 0 && idx < rdtc.Count) if(rdtc[idx] >= start && rdtc[idx] <= end && rdtc[idx] >= from && rdtc[idx] <= to) dcDates.Add(rdtc[idx].ToDateTime()); } } else for(idx = 0; idx < rdtc.Count; idx++) if(rdtc[idx] >= start && rdtc[idx] <= end && rdtc[idx] >= from && rdtc[idx] <= to) dcDates.Add(rdtc[idx].ToDateTime()); // Handle MaxOccurrences property. Note that if it's used, it is assumed that the limiting // range starts at the recurrence start. Otherwise, we have no way of knowing how many // occurred between the recurrence start and the limiting range's start date. if(maxOccur != 0 && dcDates.Count > maxOccur) dcDates.RemoveRange(maxOccur, dcDates.Count - maxOccur); } // Loop until the end of the recurrence or the range } while(freqRules.FindNext(this, end, to, current) && (maxOccur == 0 || dcDates.Count < maxOccur)); // Sort the collection one last time. There's no guaranteed order of selection if BYSETPOS was used. dcDates.Sort(true); return dcDates; }