diff --git a/Ical.Net/Evaluation/EvaluationException.cs b/Ical.Net/Evaluation/EvaluationException.cs index 78eaccdb..ee96c2a5 100644 --- a/Ical.Net/Evaluation/EvaluationException.cs +++ b/Ical.Net/Evaluation/EvaluationException.cs @@ -12,4 +12,9 @@ namespace Ical.Net.Evaluation; /// Represents an exception that may occur during calendar evaluation. /// public class EvaluationException : Exception -{ } +{ + public EvaluationException() { } + + public EvaluationException(string message) : base(message) + { } +} diff --git a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs index dd2cc0f4..f3d9f23b 100644 --- a/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs +++ b/Ical.Net/Evaluation/RecurrencePatternEvaluator.cs @@ -6,6 +6,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Ical.Net.DataTypes; @@ -97,6 +98,13 @@ private RecurrencePattern ProcessRecurrencePattern(CalDateTime referenceDate) } } + // Pre-order those BY values that don't allow for negative values. Those with negative values can only + // be ordered once the individual position is known. + if (r.BySecond?.Count > 0) r.BySecond = r.BySecond.OrderBy(x => x).ToList(); + if (r.ByMinute?.Count > 0) r.ByMinute = r.ByMinute.OrderBy(x => x).ToList(); + if (r.ByHour?.Count > 0) r.ByHour = r.ByHour.OrderBy(x => x).ToList(); + if (r.ByMonth?.Count > 0) r.ByMonth = r.ByMonth.OrderBy(x => x).ToList(); + return r; } @@ -177,14 +185,10 @@ private IEnumerable EnumerateDates(CalDateTime originalDate, CalDat // // exclude candidates that start at the same moment as periodEnd if the period is a range but keep them if targeting a specific moment if (dateCount >= pattern.Count) - { break; - } if ((candidate >= periodEnd && periodStart != periodEnd) || candidate > periodEnd && periodStart == periodEnd) - { continue; - } // UNTIL is applied outside of this method, after TZ conversion has been applied. @@ -251,11 +255,11 @@ private struct ExpandContext /// /// /// A list of possible dates. - private ISet GetCandidates(CalDateTime date, RecurrencePattern pattern, bool?[] expandBehaviors) + private IEnumerable GetCandidates(CalDateTime date, RecurrencePattern pattern, bool?[] expandBehaviors) { var expandContext = new ExpandContext() { DatesFullyExpanded = false }; - var dates = new List { date }; + IEnumerable dates = [ date ]; dates = GetMonthVariants(dates, pattern, expandBehaviors[0]); dates = GetWeekNoVariants(dates, pattern, expandBehaviors[1], ref expandContext); dates = GetYearDayVariants(dates, pattern, expandBehaviors[2], ref expandContext); @@ -265,7 +269,8 @@ private ISet GetCandidates(CalDateTime date, RecurrencePattern patt dates = GetMinuteVariants(dates, pattern, expandBehaviors[6]); dates = GetSecondVariants(dates, pattern, expandBehaviors[7]); dates = ApplySetPosRules(dates, pattern); - return new SortedSet(dates); + + return dates; } /// @@ -274,25 +279,26 @@ private ISet GetCandidates(CalDateTime date, RecurrencePattern patt /// /// The list of dates to which the BYSETPOS rules will be applied. /// - private List ApplySetPosRules(List dates, RecurrencePattern pattern) + private static IEnumerable ApplySetPosRules(IEnumerable dates, RecurrencePattern pattern) { // return if no SETPOS rules specified.. if (pattern.BySetPosition.Count == 0) - { return dates; - } - // sort the list before processing.. - dates.Sort(); + ISet bySetPos; - var size = dates.Count; - var setPosDates = pattern.BySetPosition - .Where(p => p > 0 && p <= size || p < 0 && p >= -size) //Protect against out of range access - .Select(p => p > 0 && p <= size - ? dates[p - 1] - : dates[size + p]) - .ToList(); - return setPosDates; + if (pattern.BySetPosition.Any(p => p < 0)) { + var tmp = dates.ToList(); + var count = tmp.Count; + dates = tmp; + bySetPos = new HashSet( + pattern.BySetPosition + .Select(p => (p < 0) ? count + p + 1 : p)); + } else { + bySetPos = new HashSet(pattern.BySetPosition); + } + + return dates.Where((d, i) => bySetPos.Contains(i + 1)); } /// @@ -303,25 +309,20 @@ private List ApplySetPosRules(List dates, RecurrencePa /// /// /// The modified list of dates after applying the BYMONTH rules. - private List GetMonthVariants(List dates, RecurrencePattern pattern, bool? expand) + private static IEnumerable GetMonthVariants(IEnumerable dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByMonth.Count == 0) - { return dates; - } if (expand.Value) { // Expand behavior return dates - .SelectMany(d => pattern.ByMonth.Select(month => d.AddMonths(month - d.Month))) - .ToList(); + .SelectMany(d => pattern.ByMonth.Select(month => d.AddMonths(month - d.Month))); } // Limit behavior - var dateSet = new HashSet(dates); - dateSet.ExceptWith(dates.Where(date => pattern.ByMonth.All(t => t != date.Month))); - return dateSet.ToList(); + return dates.Where(date => pattern.ByMonth.Contains(date.Month)); } /// @@ -330,20 +331,26 @@ private List GetMonthVariants(List dates, RecurrencePa /// /// The list of dates to which the BYWEEKNO rules will be applied. /// The modified list of dates after applying the BYWEEKNO rules. - private List GetWeekNoVariants(List dates, RecurrencePattern pattern, bool? expand, ref ExpandContext expandContext) + private static IEnumerable GetWeekNoVariants(IEnumerable dates, RecurrencePattern pattern, bool? expand, ref ExpandContext expandContext) { if (expand == null || pattern.ByWeekNo.Count == 0) - { return dates; - } - if (!expand.Value) - { - return new List(); - } + Debug.Assert(expand.Value); // Expand behavior - var weekNoDates = new List(); + var weekNoDates = GetWeekNoVariantsExpanded(dates, pattern); + + // subsequent parts should only limit, not expand + expandContext.DatesFullyExpanded = true; + + // Apply BYMONTH limit behavior, as we might have expanded over month/year boundaries + // in this method and BYMONTH has already been applied before, so wouldn't be again. + return GetMonthVariants(weekNoDates, pattern, expand: false); + } + + private static IEnumerable GetWeekNoVariantsExpanded(IEnumerable dates, RecurrencePattern pattern) + { foreach ((var t, var weekNo) in dates.SelectMany(t => GetByWeekNoForYearNormalized(pattern, t.Year), (t, weekNo) => (t, weekNo))) { var date = t; @@ -369,17 +376,9 @@ private List GetWeekNoVariants(List dates, RecurrenceP // Step backward single days until we're at the correct DayOfWeek date = GetFirstDayOfWeekDate(date, pattern.FirstDayOfWeek); - weekNoDates.AddRange(Enumerable.Range(0, 7).Select(i => date.AddDays(i))); + foreach (var d in Enumerable.Range(0, 7).Select(i => date.AddDays(i))) + yield return d; } - - // subsequent parts should only limit, not expand - expandContext.DatesFullyExpanded = true; - - // Apply BYMONTH limit behavior, as we might have expanded over month/year boundaries - // in this method and BYMONTH has already been applied before, so wouldn't be again. - weekNoDates = GetMonthVariants(weekNoDates, pattern, expand: false); - - return weekNoDates; } private static CalDateTime GetFirstDayOfWeekDate(CalDateTime date, DayOfWeek firstDayOfWeek) @@ -388,11 +387,12 @@ private static CalDateTime GetFirstDayOfWeekDate(CalDateTime date, DayOfWeek fir /// /// Normalize the BYWEEKNO values to be positive integers. /// - private List GetByWeekNoForYearNormalized(RecurrencePattern pattern, int year) + private static List GetByWeekNoForYearNormalized(RecurrencePattern pattern, int year) { var weeksInYear = new Lazy(() => Calendar.GetIso8601WeeksInYear(year, pattern.FirstDayOfWeek)); return pattern.ByWeekNo .Select(weekNo => weekNo >= 0 ? weekNo : weeksInYear.Value + weekNo + 1) + .OrderBy(x => x) .ToList(); } @@ -402,56 +402,52 @@ private List GetByWeekNoForYearNormalized(RecurrencePattern pattern, int ye /// /// The list of dates to which the BYYEARDAY rules will be applied. /// The modified list of dates after applying the BYYEARDAY rules. - private static List GetYearDayVariants(List dates, RecurrencePattern pattern, bool? expand, ref ExpandContext expandContext) + private static IEnumerable GetYearDayVariants(IEnumerable dates, RecurrencePattern pattern, bool? expand, ref ExpandContext expandContext) { - if (expand == null || pattern.ByYearDay.Count == 0) - { + if (expand is null || pattern.ByYearDay.Count == 0) return dates; - } - if (expand.Value && !expandContext.DatesFullyExpanded) + if ((expand == true) && !expandContext.DatesFullyExpanded) { - var yearDayDates = new List(dates.Count); - foreach (var date in dates) - { - var date1 = date; - yearDayDates.AddRange(pattern.ByYearDay.Select(yearDay => yearDay > 0 - ? date1.AddDays(-date1.DayOfYear + yearDay) - : date1.AddDays(-date1.DayOfYear + 1).AddYears(1).AddDays(yearDay)) - // Ignore the BY values that don't fit into the current year (i.e. +-366 in non-leap-years). - .Where(d => d.Year == date1.Year)); - } - expandContext.DatesFullyExpanded = true; - return yearDayDates; + return GetYearDayVariantsExpanded(dates, pattern); } + // Limit behavior - for (var i = dates.Count - 1; i >= 0; i--) + return GetYearDayVariantsLimited(dates, pattern); + } + + private static IEnumerable GetYearDayVariantsExpanded(IEnumerable dates, RecurrencePattern pattern) + { + foreach (var date in dates) { - var date = dates[i]; - var keepDate = false; - for (var j = 0; j < pattern.ByYearDay.Count; j++) - { - var yearDay = pattern.ByYearDay[j]; + var date1 = date; + var yearDayDates = new SortedSet( + pattern.ByYearDay.Select(yearDay => yearDay > 0 + ? date1.AddDays(-date1.DayOfYear + yearDay) + : date1.AddDays(-date1.DayOfYear + 1).AddYears(1).AddDays(yearDay)) + // Ignore the BY values that don't fit into the current year (i.e. +-366 in non-leap-years). + .Where(d => d.Year == date1.Year)); - var newDate = yearDay > 0 - ? date.AddDays(-date.DayOfYear + yearDay) - : date.AddDays(-date.DayOfYear + 1).AddYears(1).AddDays(yearDay); + foreach (var d in yearDayDates) + yield return d; + } + } - if (newDate.Date == date.Date) - { - keepDate = true; - break; - } - } + private static IEnumerable GetYearDayVariantsLimited(IEnumerable dates, RecurrencePattern pattern) + { + foreach (var date in dates) + { + var candidates = + from yearDay in pattern.ByYearDay + let newDate = yearDay > 0 + ? date.AddDays(-date.DayOfYear + yearDay) + : date.AddDays(-date.DayOfYear + 1).AddYears(1).AddDays(yearDay) + select newDate; - if (!keepDate) - { - dates.RemoveAt(i); - } + if (candidates.Contains(date)) + yield return date; } - - return dates; } /// @@ -460,66 +456,52 @@ private static List GetYearDayVariants(List dates, Rec /// /// The list of dates to which the BYMONTHDAY rules will be applied. /// The modified list of dates after applying the BYMONTHDAY rules. - private List GetMonthDayVariants(List dates, RecurrencePattern pattern, bool? expand, ref ExpandContext expandContext) + private static IEnumerable GetMonthDayVariants(IEnumerable dates, RecurrencePattern pattern, bool? expand, ref ExpandContext expandContext) { if (expand == null || pattern.ByMonthDay.Count == 0) - { return dates; - } if (expand.Value && !expandContext.DatesFullyExpanded) { - var monthDayDates = new List(); - foreach (var date in dates) - { - monthDayDates.AddRange( - from monthDay in pattern.ByMonthDay - let daysInMonth = Calendar.GetDaysInMonth(date.Year, date.Month) - where Math.Abs(monthDay) <= daysInMonth - select monthDay > 0 - ? date.AddDays(-date.Day + monthDay) - : date.AddDays(-date.Day + 1).AddMonths(1).AddDays(monthDay) - ); - } - expandContext.DatesFullyExpanded = true; - return monthDayDates; + return GetMonthDayVariantsExpanded(dates, pattern); } - // Limit behavior - for (var i = dates.Count - 1; i >= 0; i--) + // limit behavior + return GetMonthDayVariantsLimited(dates, pattern); + } + + private static IEnumerable GetMonthDayVariantsLimited(IEnumerable dates, RecurrencePattern pattern) + { + foreach (var date in dates) { - var date = dates[i]; - var keepDate = false; - for (var j = 0; j < pattern.ByMonthDay.Count; j++) + var daysInMonth = Calendar.GetDaysInMonth(date.Year, date.Month); + foreach (var monthDay in pattern.ByMonthDay) { - var monthDay = pattern.ByMonthDay[j]; - - var daysInMonth = Calendar.GetDaysInMonth(date.Year, date.Month); - if (Math.Abs(monthDay) > daysInMonth) + var byMonthDay = (monthDay > 0) ? monthDay : (daysInMonth + monthDay + 1); + if (date.Day == byMonthDay) { - throw new ArgumentException("Invalid day of month: " + date + " (day " + monthDay + ")"); - } - - // Account for positive or negative numbers - var newDate = monthDay > 0 - ? date.AddDays(-date.Day + monthDay) - : date.AddDays(-date.Day + 1).AddMonths(1).AddDays(monthDay); - - if (newDate.Day.Equals(date.Day)) - { - keepDate = true; + yield return date; break; } } - - if (!keepDate) - { - dates.RemoveAt(i); - } } + } - return dates; + private static IEnumerable GetMonthDayVariantsExpanded(IEnumerable dates, RecurrencePattern pattern) + { + foreach (var date in dates) + { + var monthDayDates = new SortedSet( + from monthDay in pattern.ByMonthDay + let daysInMonth = Calendar.GetDaysInMonth(date.Year, date.Month) + let monthDayAbs = (monthDay > 0) ? monthDay : (daysInMonth + monthDay + 1) + where (monthDayAbs > 0) && (monthDayAbs <= daysInMonth) + select date.AddDays(-date.Day + monthDayAbs)); + + foreach (var d in monthDayDates) + yield return d; + } } /// @@ -528,56 +510,40 @@ select monthDay > 0 /// /// The list of dates to which BYDAY rules will be applied. /// The modified list of dates after applying BYDAY rules, or the original list if no BYDAY rules are specified. - private List GetDayVariants(List dates, RecurrencePattern pattern, bool? expand, ref ExpandContext expandContext) + private static IEnumerable GetDayVariants(IEnumerable dates, RecurrencePattern pattern, bool? expand, ref ExpandContext expandContext) { if (expand == null || pattern.ByDay.Count == 0) - { return dates; - } if (expand.Value && !expandContext.DatesFullyExpanded) { // Expand behavior - var weekDayDates = new List(); - foreach (var date in dates) - { - foreach (var day in pattern.ByDay) - { - weekDayDates.AddRange(GetAbsWeekDays(date, day, pattern)); - } - } - expandContext.DatesFullyExpanded = true; - return weekDayDates; + return GetDayVariantsExpanded(dates, pattern); } // Limit behavior - for (var i = dates.Count - 1; i >= 0; i--) + return GetDayVariantsLimited(dates, pattern); + } + + private static IEnumerable GetDayVariantsLimited(IEnumerable dates, RecurrencePattern pattern) + => + // If no offset is specified, simply test the day of week! + // FIXME: test with offset... + dates.Where(date => pattern.ByDay.Any(weekDay => weekDay.DayOfWeek.Equals(date.DayOfWeek))); + + private static IEnumerable GetDayVariantsExpanded(IEnumerable dates, RecurrencePattern pattern) + { + foreach (var date in dates) { - var date = dates[i]; - var keepDate = false; - for (var j = 0; j < pattern.ByDay.Count; j++) - { - var weekDay = pattern.ByDay[j]; - if (weekDay.DayOfWeek.Equals(date.DayOfWeek)) - { - // If no offset is specified, simply test the day of week! - // FIXME: test with offset... - if (date.DayOfWeek.Equals(weekDay.DayOfWeek)) - { - keepDate = true; - break; - } - } - } + var weekDayDates = new SortedSet(); + foreach (var day in pattern.ByDay) + foreach (var d in GetAbsWeekDays(date, day, pattern)) + weekDayDates.Add(d); - if (!keepDate) - { - dates.RemoveAt(i); - } + foreach (var d in weekDayDates) + yield return d; } - - return dates; } /// @@ -587,92 +553,97 @@ private List GetDayVariants(List dates, RecurrencePatt /// The date to start the evaluation from. /// The week day to evaluate. /// A list of applicable dates. - private List GetAbsWeekDays(CalDateTime date, WeekDay weekDay, RecurrencePattern pattern) + private static IEnumerable GetAbsWeekDays(CalDateTime date, WeekDay weekDay, RecurrencePattern pattern) { - var days = new List(); + var dates = pattern switch + { + { Frequency: FrequencyType.Daily } => GetAbsWeekDaysDaily(date, weekDay), + { Frequency: FrequencyType.Weekly } or { ByWeekNo: { Count: > 0 } } => GetAbsWeekDaysWeekly(date, pattern, weekDay), + { Frequency: FrequencyType.Monthly } or { ByMonth: { Count: > 0 } } => GetAbsWeekDaysMonthly(date, pattern, weekDay), + { Frequency: FrequencyType.Yearly } => GetAbsWeekDaysYearly(date, weekDay), + _ => [] + }; - var dayOfWeek = weekDay.DayOfWeek; - if (pattern.Frequency == FrequencyType.Daily) + return GetOffsetDates(dates, weekDay.Offset); + } + + private static IEnumerable GetAbsWeekDaysDaily(CalDateTime date, WeekDay weekDay) + => (date.DayOfWeek == weekDay.DayOfWeek) ? [date] : []; + + private static IEnumerable GetAbsWeekDaysYearly(CalDateTime date, WeekDay weekDay) + { + var year = date.Year; + + // construct a list of possible year days.. + date = date.AddDays(-date.DayOfYear + 1); + while (date.DayOfWeek != weekDay.DayOfWeek) { - if (date.DayOfWeek == dayOfWeek) - { - days.Add(date); - } + date = date.AddDays(1); } - else if (pattern.Frequency == FrequencyType.Weekly || pattern.ByWeekNo.Count > 0) + + while (date.Year == year) { - var weekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); + yield return date; + date = date.AddDays(7); + } + } - // Go to the first day of the week - date = date.AddDays(-GetWeekDayOffset(date, pattern.FirstDayOfWeek)); + private static IEnumerable GetAbsWeekDaysMonthly(CalDateTime date, RecurrencePattern pattern, WeekDay weekDay) + { + var month = date.Month; - // construct a list of possible week days.. - while (date.DayOfWeek != dayOfWeek) - { - date = date.AddDays(1); - } + // construct a list of possible month days.. + date = date.AddDays(-date.Day + 1); + while (date.DayOfWeek != weekDay.DayOfWeek) + { + date = date.AddDays(1); + } - var nextWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); + var byWeekNoNormalized = GetByWeekNoForYearNormalized(pattern, Calendar.GetIso8601YearOfWeek(date, pattern.FirstDayOfWeek)); + while (date.Month == month) + { var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); - var byWeekNoNormalized = GetByWeekNoForYearNormalized(pattern, Calendar.GetIso8601YearOfWeek(date, pattern.FirstDayOfWeek)); - //When we manage weekly recurring pattern and we have boundary case: - //Weekdays: Dec 31, Jan 1, Feb 1, Mar 1, Apr 1, May 1, June 1, Dec 31 - It's the 53th week of the year, but all another are 1st week number. - //So we need an EXRULE for this situation, but only for weekly events - while (currentWeekNo == weekNo || (nextWeekNo < weekNo && currentWeekNo == nextWeekNo && pattern.Frequency == FrequencyType.Weekly)) + if ((byWeekNoNormalized.Count == 0 || byWeekNoNormalized.Contains(currentWeekNo)) + && (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month))) { - if ((byWeekNoNormalized.Count == 0 || byWeekNoNormalized.Contains(currentWeekNo)) - && (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month))) - { - days.Add(date); - } - - date = date.AddDays(7); - currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); + yield return date; } + date = date.AddDays(7); } - else if (pattern.Frequency == FrequencyType.Monthly || pattern.ByMonth.Count > 0) - { - var month = date.Month; + } - // construct a list of possible month days.. - date = date.AddDays(-date.Day + 1); - while (date.DayOfWeek != dayOfWeek) - { - date = date.AddDays(1); - } + private static IEnumerable GetAbsWeekDaysWeekly(CalDateTime date, RecurrencePattern pattern, WeekDay weekDay) + { + var weekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); - var byWeekNoNormalized = GetByWeekNoForYearNormalized(pattern, Calendar.GetIso8601YearOfWeek(date, pattern.FirstDayOfWeek)); - while (date.Month == month) - { - var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); + // Go to the first day of the week + date = date.AddDays(-GetWeekDayOffset(date, pattern.FirstDayOfWeek)); - if ((byWeekNoNormalized.Count == 0 || byWeekNoNormalized.Contains(currentWeekNo)) - && (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month))) - { - days.Add(date); - } - date = date.AddDays(7); - } - } - else if (pattern.Frequency == FrequencyType.Yearly) + // construct a list of possible week days.. + while (date.DayOfWeek != weekDay.DayOfWeek) { - var year = date.Year; + date = date.AddDays(1); + } - // construct a list of possible year days.. - date = date.AddDays(-date.DayOfYear + 1); - while (date.DayOfWeek != dayOfWeek) - { - date = date.AddDays(1); - } + var nextWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); + var currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); + var byWeekNoNormalized = GetByWeekNoForYearNormalized(pattern, Calendar.GetIso8601YearOfWeek(date, pattern.FirstDayOfWeek)); - while (date.Year == year) + //When we manage weekly recurring pattern and we have boundary case: + //Weekdays: Dec 31, Jan 1, Feb 1, Mar 1, Apr 1, May 1, June 1, Dec 31 - It's the 53th week of the year, but all another are 1st week number. + //So we need an EXRULE for this situation, but only for weekly events + while (currentWeekNo == weekNo || (nextWeekNo < weekNo && currentWeekNo == nextWeekNo && pattern.Frequency == FrequencyType.Weekly)) + { + if ((byWeekNoNormalized.Count == 0 || byWeekNoNormalized.Contains(currentWeekNo)) + && (pattern.ByMonth.Count == 0 || pattern.ByMonth.Contains(date.Month))) { - days.Add(date); - date = date.AddDays(7); + yield return date; } + + date = date.AddDays(7); + currentWeekNo = Calendar.GetIso8601WeekOfYear(date, pattern.FirstDayOfWeek); } - return GetOffsetDates(days, weekDay.Offset); } /// @@ -688,24 +659,20 @@ private static int GetWeekDayOffset(CalDateTime date, DayOfWeek startOfWeek) /// /// The list from which to extract the element. /// The position of the element to extract. - private List GetOffsetDates(List dates, int? offset) + private static IEnumerable GetOffsetDates(IEnumerable dates, int? offset) { if (offset is null) - { return dates; - } - var offsetDates = new List(); - var size = dates.Count; - if (offset < 0 && offset >= -size) - { - offsetDates.Add(dates[size + offset.Value]); - } - else if (offset > 0 && offset <= size) - { - offsetDates.Add(dates[offset.Value - 1]); + if (offset == 0) + throw new EvaluationException("Encountered a day offset of 0 which is not allowed."); + + if (offset < 0) { + offset = -offset; + dates = dates.Reverse(); } - return offsetDates; + + return dates.Skip(offset.Value - 1).Take(1); } /// @@ -716,50 +683,19 @@ private List GetOffsetDates(List dates, int? offset) /// /// /// The modified list of dates after applying the BYHOUR rules. - private List GetHourVariants(List dates, RecurrencePattern pattern, bool? expand) + private static IEnumerable GetHourVariants(IEnumerable dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByHour.Count == 0) - { return dates; - } if (expand.Value) { // Expand behavior - var hourlyDates = new List(); - for (var i = 0; i < dates.Count; i++) - { - var date = dates[i]; - for (var j = 0; j < pattern.ByHour.Count; j++) - { - var hour = pattern.ByHour[j]; - date = date.AddHours(-date.Hour + hour); - hourlyDates.Add(date); - } - } - return hourlyDates; + return dates.SelectMany(date => pattern.ByHour.Select(hour => date.AddHours(-date.Hour + hour))); } + // Limit behavior - for (var i = dates.Count - 1; i >= 0; i--) - { - var date = dates[i]; - var keepDate = false; - for (var j = 0; j < pattern.ByHour.Count; j++) - { - var hour = pattern.ByHour[j]; - if (date.Hour == hour) - { - keepDate = true; - break; - } - } - // Remove unmatched dates - if (!keepDate) - { - dates.RemoveAt(i); - } - } - return dates; + return dates.Where(date => pattern.ByHour.Contains(date.Hour)); } /// @@ -770,49 +706,19 @@ private List GetHourVariants(List dates, RecurrencePat /// /// /// The modified list of dates after applying the BYMINUTE rules. - private List GetMinuteVariants(List dates, RecurrencePattern pattern, bool? expand) + private static IEnumerable GetMinuteVariants(IEnumerable dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.ByMinute.Count == 0) - { return dates; - } if (expand.Value) { // Expand behavior - var minutelyDates = new List(); - for (var i = 0; i < dates.Count; i++) - { - var date = dates[i]; - for (var j = 0; j < pattern.ByMinute.Count; j++) - { - var minute = pattern.ByMinute[j]; - date = date.AddMinutes(-date.Minute + minute); - minutelyDates.Add(date); - } - } - return minutelyDates; + return dates.SelectMany(date => pattern.ByMinute.Select(minute => date.AddMinutes(-date.Minute + minute))); } + // Limit behavior - for (var i = dates.Count - 1; i >= 0; i--) - { - var date = dates[i]; - var keepDate = false; - for (var j = 0; j < pattern.ByMinute.Count; j++) - { - var minute = pattern.ByMinute[j]; - if (date.Minute == minute) - { - keepDate = true; - } - } - // Remove unmatched dates - if (!keepDate) - { - dates.RemoveAt(i); - } - } - return dates; + return dates.Where(date => pattern.ByMinute.Contains(date.Minute)); } /// @@ -823,52 +729,19 @@ private List GetMinuteVariants(List dates, RecurrenceP /// /// /// The modified list of dates after applying the BYSECOND rules. - private List GetSecondVariants(List dates, RecurrencePattern pattern, bool? expand) + private static IEnumerable GetSecondVariants(IEnumerable dates, RecurrencePattern pattern, bool? expand) { if (expand == null || pattern.BySecond.Count == 0) - { return dates; - } if (expand.Value) { // Expand behavior - var secondlyDates = new List(); - for (var i = 0; i < dates.Count; i++) - { - var date = dates[i]; - for (var j = 0; j < pattern.BySecond.Count; j++) - { - var second = pattern.BySecond[j]; - date = date.AddSeconds(-date.Second + second); - secondlyDates.Add(date); - } - } - return secondlyDates; - } - // Limit behavior - for (var i = dates.Count - 1; i >= 0; i--) - { - var date = dates[i]; - var keepDate = false; - for (var j = 0; j < pattern.BySecond.Count; j++) - { - var second = pattern.BySecond[j]; - if (date.Second == second) - { - keepDate = true; - break; - } - } - - // Remove unmatched dates - if (!keepDate) - { - dates.RemoveAt(i); - } + return dates.SelectMany(date => pattern.BySecond.Select(second => date.AddSeconds(-date.Second + second))); } - return dates; + // Limit behavior + return dates.Where(date => pattern.BySecond.Contains(date.Second)); } ///