From d3596fbbb14c5bd77ad55294f7d6a46ee6302413 Mon Sep 17 00:00:00 2001 From: Sergey Olshanskiy Date: Thu, 13 Jun 2024 17:14:39 +0200 Subject: [PATCH] Bugfix - fix bug - add more tests - clean up --- README.md | 19 +++++- samples/UsingWithFewSubranges/Program.cs | 17 ++--- src/DateRecurrenceR/Helpers/DateHelper.cs | 26 +++++++- src/DateRecurrenceR/Helpers/WeekDaysHelper.cs | 2 +- .../Helpers/WeeklyRecurrenceHelper.cs | 26 +------- src/DateRecurrenceR/Internals/Constant.cs | 6 ++ src/DateRecurrenceR/Recurrence.Weekly.cs | 10 +-- src/DateRecurrenceR/Usings.cs | 1 + src/DateRecurrenceR/WeekDays.cs | 16 +---- .../Helpers/DateHelperTests.cs | 64 +++++++++++++++++++ .../Helpers/WeeklyRecurrenceHelperTests.cs | 23 +++---- test/DateRecurrenceR.Tests.Unit/Usings.cs | 3 +- 12 files changed, 139 insertions(+), 74 deletions(-) create mode 100644 src/DateRecurrenceR/Internals/Constant.cs create mode 100644 src/DateRecurrenceR/Usings.cs diff --git a/README.md b/README.md index 656a931..a9a88b4 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,25 @@ # DateRecurrenceR [![Release](https://github.com/Cadabra/DateRecurrenceR/actions/workflows/release.yml/badge.svg)](https://github.com/Cadabra/DateRecurrenceR/actions/workflows/release.yml) [![Nuget](https://img.shields.io/nuget/v/DateRecurrenceR?logo=NuGet)](https://www.nuget.org/packages/DateRecurrenceR) -DateRecurrenceR is a .NET library designed to handle recurrence patterns for dates efficiently. It allows you to generate and manage recurring date sequences without storing every sequences, making it ideal for applications with dynamic date range calculations. +DateRecurrenceR is a .NET library designed to handle recurrence patterns for dates efficiently. It allows you to +generate and manage recurring date sequences without storing every sequence, making it ideal for applications with +dynamic date range calculations. ## Core functional -The primary feature of DateRecurrenceR is its ability to extract specific subranges from a defined recurrence pattern. Instead of storing all dates from a recurrence, it calculates the necessary subset on-the-fly based on a given start date, end date, and recurrence pattern. This approach optimizes performance and memory usage. + +The primary feature of DateRecurrenceR is its ability to extract specific subranges from a defined recurrence pattern. +Instead of storing all dates from a recurrence, it calculates the necessary subset on-the-fly based on a given start +date, end date, and recurrence pattern. This approach optimizes performance and memory usage. #### Example + pattern: `Tuesday` and `Friday` every 2 weeks from `999-01-01`\ Both `X` and `A` are dates for first 3 month.\ `s` is start subrange date `999-02-16`\ `A` are subrange dates\ -see the project: [UsingWithFewSubranges](https://github.com/Cadabra/DateRecurrenceR/tree/main/samples/UsingWithFewSubranges) +see the +project: [UsingWithFewSubranges](https://github.com/Cadabra/DateRecurrenceR/tree/main/samples/UsingWithFewSubranges) + ``` s m t w r f s s m t w r f s s m t w r f s -------------------- -------------------- -------------------- @@ -21,7 +29,9 @@ s m t w r f s s m t w r f s s m t w r f s . . . . . . . . . . . . . . . . . . . . . . . X . . A . . . . A . . A . ``` + ### Key Features + * **Dynamic Subrange Extraction:** Generate only the dates you need within a specified range. * **Flexible Patterns:** Support for various recurrence patterns (e.g., daily, weekly, monthly, yearly). * **Efficient Memory Usage:** Avoids storing large sets of dates, improving performance. @@ -31,10 +41,13 @@ s m t w r f s s m t w r f s s m t w r f s You can install DateRecurrenceR via [NuGet](https://www.nuget.org/packages/DateRecurrenceR): **Package Manager Console:** + ```shell Install-Package DateRecurrenceR ``` + **.NET Core CLI:** + ```shell dotnet add package DateRecurrenceR ``` \ No newline at end of file diff --git a/samples/UsingWithFewSubranges/Program.cs b/samples/UsingWithFewSubranges/Program.cs index ff6a848..a4bc77a 100644 --- a/samples/UsingWithFewSubranges/Program.cs +++ b/samples/UsingWithFewSubranges/Program.cs @@ -21,24 +21,15 @@ var enumeratorSecondMonth = Recurrence.GetWeeklyEnumerator(beginDate, endDate, fromDate2, toDate2, weekDays, firstDayOfWeek, 2); -while (enumeratorFull.MoveNext()) -{ - Console.Write($"{enumeratorFull.Current:d} "); -} +while (enumeratorFull.MoveNext()) Console.Write($"{enumeratorFull.Current:d} "); Console.WriteLine(); Console.ForegroundColor = ConsoleColor.Cyan; -while (enumeratorFirstMonth.MoveNext()) -{ - Console.Write($"{enumeratorFirstMonth.Current:d} "); -} +while (enumeratorFirstMonth.MoveNext()) Console.Write($"{enumeratorFirstMonth.Current:d} "); // just beautify -Console.Write(new string(' ', 29)); +Console.Write(new string(' ', 33)); Console.ForegroundColor = ConsoleColor.Magenta; -while (enumeratorSecondMonth.MoveNext()) -{ - Console.Write($"{enumeratorSecondMonth.Current:d} "); -} \ No newline at end of file +while (enumeratorSecondMonth.MoveNext()) Console.Write($"{enumeratorSecondMonth.Current:d} "); \ No newline at end of file diff --git a/src/DateRecurrenceR/Helpers/DateHelper.cs b/src/DateRecurrenceR/Helpers/DateHelper.cs index 8449f08..afdb73a 100644 --- a/src/DateRecurrenceR/Helpers/DateHelper.cs +++ b/src/DateRecurrenceR/Helpers/DateHelper.cs @@ -1,11 +1,11 @@ +using System.Diagnostics; using System.Diagnostics.Contracts; +using DateRecurrenceR.Internals; namespace DateRecurrenceR.Helpers; internal static class DateHelper { - private const int DaysInWeek = 7; - [Pure] public static bool TryGetDateOfDayOfWeek(DateOnly dateInSameWeek, DayOfWeek dayOfWeek, @@ -38,7 +38,8 @@ public static DateOnly GetDateByDayOfMonth(int year, int month, DayOfWeek dayOfW NumberOfWeek.Second => 1, NumberOfWeek.Third => 2, NumberOfWeek.Fourth => 3, - NumberOfWeek.Last => 4 - (DateTime.DaysInMonth(date.Year, date.Month) % 7 <= diffToFirstDay ? 1 : 0), + NumberOfWeek.Last => 4 - + (DateTime.DaysInMonth(date.Year, date.Month) % DaysInWeek <= diffToFirstDay ? 1 : 0), _ => throw new ArgumentOutOfRangeException(nameof(numberOfWeek), numberOfWeek, null) }; @@ -67,4 +68,23 @@ public static DateOnly GetDateByDayOfYear(int year, int dayOfYear) return new DateOnly(year, 1, 1).AddDays(saveDay - 1); } + + [Pure] + public static int CalculateDaysToNextInterval(int startDay, int fromDay, int interval, WeeklyHash weeklyHash) + { + Debug.Assert(fromDay >= startDay); + + var daysInInterval = DaysInWeek * interval; + var daysDif = fromDay - startDay; + + var modDif = daysDif / daysInInterval * daysInInterval; + + for (var i = 0; (i < DaysInWeek) & (daysDif > modDif); i++) + { + var dayOfWeek = (DayOfWeek) (((uint) startDay + modDif + 1) % DaysInWeek); + modDif += weeklyHash[dayOfWeek]; + } + + return modDif; + } } \ No newline at end of file diff --git a/src/DateRecurrenceR/Helpers/WeekDaysHelper.cs b/src/DateRecurrenceR/Helpers/WeekDaysHelper.cs index c4d1c98..95883e8 100644 --- a/src/DateRecurrenceR/Helpers/WeekDaysHelper.cs +++ b/src/DateRecurrenceR/Helpers/WeekDaysHelper.cs @@ -4,6 +4,6 @@ internal struct WeekDaysHelper { public static int GetDiffToDay(DayOfWeek firstDayOfWeek, DayOfWeek dayOfWeek) { - return (7 + (int) dayOfWeek - (int) firstDayOfWeek) % 7; + return (DaysInWeek + (int) dayOfWeek - (int) firstDayOfWeek) % DaysInWeek; } } \ No newline at end of file diff --git a/src/DateRecurrenceR/Helpers/WeeklyRecurrenceHelper.cs b/src/DateRecurrenceR/Helpers/WeeklyRecurrenceHelper.cs index c0d53ac..4b26b6f 100644 --- a/src/DateRecurrenceR/Helpers/WeeklyRecurrenceHelper.cs +++ b/src/DateRecurrenceR/Helpers/WeeklyRecurrenceHelper.cs @@ -1,12 +1,9 @@ -using System.Diagnostics; using DateRecurrenceR.Internals; namespace DateRecurrenceR.Helpers; internal struct WeeklyRecurrenceHelper { - private const int DaysInWeek = 7; - public static WeeklyHash GetPatternHash(WeekDays weekDays, int interval) { var hash = new WeeklyHash(); @@ -120,6 +117,7 @@ public static WeeklyHash GetPatternHash(WeekDays weekDays, int interval) public static bool TryGetStartDate(DateOnly beginDate, DateOnly fromDate, + WeeklyHash weeklyHash, WeekDays weekDays, DayOfWeek firstDayOfWeek, int interval, @@ -133,7 +131,8 @@ public static bool TryGetStartDate(DateOnly beginDate, if (startDate >= fromDate) return true; - var difDays = CalculateDaysToNextInterval(startDate.DayNumber, fromDate.DayNumber, interval, weekDays); + var difDays = + DateHelper.CalculateDaysToNextInterval(startDate.DayNumber, fromDate.DayNumber, interval, weeklyHash); var startDay = startDate.DayNumber + difDays; if (startDay > DateOnly.MaxValue.DayNumber) @@ -145,23 +144,4 @@ public static bool TryGetStartDate(DateOnly beginDate, startDate = DateOnly.FromDayNumber(startDay); return true; } - - private static int CalculateDaysToNextInterval(int startDay, int fromDay, int interval, WeekDays weekDays) - { - Debug.Assert(fromDay >= startDay); - - var daysInInterval = DaysInWeek * interval; - var daysDif = fromDay - startDay; - - var modDif = daysDif / daysInInterval * daysInInterval; - - if (daysDif > modDif) - { - daysDif += (((daysInInterval - (daysDif - modDif)) / DaysInWeek) * DaysInWeek); - var dayOfWeek = (DayOfWeek) (((uint) (startDay + daysDif) + 1) % 7); - return daysDif + (int)weekDays.GetNextDay(dayOfWeek) + 1; - } - - return modDif; - } } \ No newline at end of file diff --git a/src/DateRecurrenceR/Internals/Constant.cs b/src/DateRecurrenceR/Internals/Constant.cs new file mode 100644 index 0000000..23d073d --- /dev/null +++ b/src/DateRecurrenceR/Internals/Constant.cs @@ -0,0 +1,6 @@ +namespace DateRecurrenceR.Internals; + +internal struct Constant +{ + public const int DaysInWeek = 7; +} \ No newline at end of file diff --git a/src/DateRecurrenceR/Recurrence.Weekly.cs b/src/DateRecurrenceR/Recurrence.Weekly.cs index fa3f327..b18206b 100644 --- a/src/DateRecurrenceR/Recurrence.Weekly.cs +++ b/src/DateRecurrenceR/Recurrence.Weekly.cs @@ -27,9 +27,12 @@ public static IEnumerator GetWeeklyEnumerator(DateOnly beginDate, { if (interval < 1) throw new ArgumentException($"The '{nameof(interval)}' cannot be less than 1."); + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); + var canStart = WeeklyRecurrenceHelper.TryGetStartDate( beginDate, fromDate, + patternHash, weekDays, firstDayOfWeek, interval, @@ -37,8 +40,6 @@ public static IEnumerator GetWeeklyEnumerator(DateOnly beginDate, if (!canStart) return EmptyEnumerator; - var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); - return new WeeklyEnumeratorLimitByCount(startDate, takeCount, patternHash); } @@ -67,9 +68,12 @@ public static IEnumerator GetWeeklyEnumerator(DateOnly beginDate, { if (interval < 1) throw new ArgumentException($"The '{nameof(interval)}' cannot be less than 1."); + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); + var canStart = WeeklyRecurrenceHelper.TryGetStartDate( beginDate, fromDate, + patternHash, weekDays, firstDayOfWeek, interval, @@ -79,8 +83,6 @@ public static IEnumerator GetWeeklyEnumerator(DateOnly beginDate, var stopDate = DateOnlyMin(toDate, endDate); - var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); - return new WeeklyEnumeratorLimitByDate(startDate, stopDate, patternHash); } } \ No newline at end of file diff --git a/src/DateRecurrenceR/Usings.cs b/src/DateRecurrenceR/Usings.cs new file mode 100644 index 0000000..49303cd --- /dev/null +++ b/src/DateRecurrenceR/Usings.cs @@ -0,0 +1 @@ +global using static DateRecurrenceR.Internals.Constant; \ No newline at end of file diff --git a/src/DateRecurrenceR/WeekDays.cs b/src/DateRecurrenceR/WeekDays.cs index c861687..48eebce 100644 --- a/src/DateRecurrenceR/WeekDays.cs +++ b/src/DateRecurrenceR/WeekDays.cs @@ -2,11 +2,11 @@ namespace DateRecurrenceR; public sealed class WeekDays { - private readonly bool[] _ds = new bool[7]; + private readonly bool[] _ds = new bool[DaysInWeek]; public WeekDays(DayOfWeek dayOfWeek) { - MinDay = (DayOfWeek) 7; + MinDay = (DayOfWeek) DaysInWeek; _ds[(int) dayOfWeek] = true; UpdateMinDay(dayOfWeek); @@ -14,7 +14,7 @@ public WeekDays(DayOfWeek dayOfWeek) public WeekDays(params DayOfWeek[] daysArray) { - MinDay = (DayOfWeek) 7; + MinDay = (DayOfWeek) DaysInWeek; for (var i = 0; i < daysArray.Length; i++) { @@ -31,14 +31,4 @@ private void UpdateMinDay(DayOfWeek dayOfWeek) { if (MinDay > dayOfWeek) MinDay = dayOfWeek; } - - public DayOfWeek GetNextDay(DayOfWeek dayOfWeek) - { - while (!_ds[(int) dayOfWeek]) - { - dayOfWeek = (DayOfWeek)(((int)dayOfWeek + 1) % 7); - } - - return dayOfWeek; - } } \ No newline at end of file diff --git a/test/DateRecurrenceR.Tests.Unit/Helpers/DateHelperTests.cs b/test/DateRecurrenceR.Tests.Unit/Helpers/DateHelperTests.cs index 4974baf..3327f61 100644 --- a/test/DateRecurrenceR.Tests.Unit/Helpers/DateHelperTests.cs +++ b/test/DateRecurrenceR.Tests.Unit/Helpers/DateHelperTests.cs @@ -235,4 +235,68 @@ public void Method_GetDateByDayOfYear_returns_max_366() //Assert date.Should().Be(expectedDate); } + + + [Fact] + public void Method_CalculateDaysToNextInterval_with_singleDay() + { + // Arrange + var interval = 50; + var weekDays = new WeekDays(DayOfWeek.Monday); + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); + + // Act + var res = DateHelper.CalculateDaysToNextInterval( + DateOnly.MinValue.DayNumber, + DateOnly.MinValue.AddDays(1).DayNumber, + interval, + patternHash); + + //Assert + res.Should().Be(interval * DaysInWeek); + } + + [Fact] + public void Method_CalculateDaysToNextInterval_with_multipleDays_with_start_after_lastDay() + { + // Arrange + var interval = 2; + var weekDays = new WeekDays(DayOfWeek.Tuesday, DayOfWeek.Friday); + var startDate = new DateOnly(999, 1, 1); + var fromDate = new DateOnly(999, 2, 16); // right after 999-02-15 Friday + var expectedDate = new DateOnly(999, 2, 26); + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); + + // Act + var res = DateHelper.CalculateDaysToNextInterval( + startDate.DayNumber, + fromDate.DayNumber, + interval, + patternHash); + + //Assert + res.Should().Be(expectedDate.DayNumber - startDate.DayNumber); + } + + [Fact] + public void Method_CalculateDaysToNextInterval_with_multipleDays_with_start_in_middleDay() + { + // Arrange + var interval = 2; + var weekDays = new WeekDays(DayOfWeek.Tuesday, DayOfWeek.Friday); + var startDate = new DateOnly(999, 1, 1); + var fromDate = new DateOnly(999, 2, 14); // before 999-02-15 Friday + var expectedDate = new DateOnly(999, 2, 15); + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); + + // Act + var res = DateHelper.CalculateDaysToNextInterval( + startDate.DayNumber, + fromDate.DayNumber, + interval, + patternHash); + + //Assert + res.Should().Be(expectedDate.DayNumber - startDate.DayNumber); + } } \ No newline at end of file diff --git a/test/DateRecurrenceR.Tests.Unit/Helpers/WeeklyRecurrenceHelperTests.cs b/test/DateRecurrenceR.Tests.Unit/Helpers/WeeklyRecurrenceHelperTests.cs index 2531e4b..2b44125 100644 --- a/test/DateRecurrenceR.Tests.Unit/Helpers/WeeklyRecurrenceHelperTests.cs +++ b/test/DateRecurrenceR.Tests.Unit/Helpers/WeeklyRecurrenceHelperTests.cs @@ -14,11 +14,13 @@ public void Method_TryGetStartDate_returns_True_when_FromDate_eq_BeginDate() var weekDays = new WeekDays(DayOfWeek.Monday); var firstDayOfWeek = DayOfWeek.Monday; var interval = 1; + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); // Act var canStart = WeeklyRecurrenceHelper.TryGetStartDate( beginDate, fromDate, + patternHash, weekDays, firstDayOfWeek, interval, @@ -37,11 +39,13 @@ public void Method_TryGetStartDate_returns_True_when_FromDate_gt_BeginDate() var weekDays = new WeekDays(DayOfWeek.Monday, DayOfWeek.Friday); var firstDayOfWeek = DayOfWeek.Monday; var interval = 1; + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); // Act var canStart = WeeklyRecurrenceHelper.TryGetStartDate( beginDate, fromDate, + patternHash, weekDays, firstDayOfWeek, interval, @@ -60,11 +64,13 @@ public void Method_TryGetStartDate_returns_False_when_FromDate_eq_BeginDate_and_ var weekDays = new WeekDays(DayOfWeek.Saturday); var firstDayOfWeek = DayOfWeek.Monday; var interval = 1; + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); // Act var canStart = WeeklyRecurrenceHelper.TryGetStartDate( beginDate, fromDate, + patternHash, weekDays, firstDayOfWeek, interval, @@ -83,11 +89,13 @@ public void Method_TryGetStartDate_returns_False_when_FromDate_gt_BeginDate_and_ var weekDays = new WeekDays(DayOfWeek.Monday, DayOfWeek.Saturday); var firstDayOfWeek = DayOfWeek.Monday; var interval = 1; + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); // Act var canStart = WeeklyRecurrenceHelper.TryGetStartDate( beginDate, fromDate, + patternHash, weekDays, firstDayOfWeek, interval, @@ -106,11 +114,13 @@ public void Method_TryGetStartDate_returns_Result_eq_FromDate() var fromDate = beginDate.AddDays(7 * interval * 2); var weekDays = new WeekDays(DayOfWeek.Monday); var firstDayOfWeek = DayOfWeek.Monday; + var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval); // Act WeeklyRecurrenceHelper.TryGetStartDate( beginDate, fromDate, + patternHash, weekDays, firstDayOfWeek, interval, @@ -184,17 +194,4 @@ public void Method_GetPatternHash_returns_correct_hash_for_seven_days() hash[DayOfWeek.Friday].Should().Be(1); hash[DayOfWeek.Saturday].Should().Be(1); } - - // [Fact] - // public void Method_GetPatternHash_throws_Exception_if_dayOfWeek_outOfRange() - // { - // // Arrange - // var action = () => new WeekDays((DayOfWeek)7); - // - // // Act - // // var hash = WeeklyRecurrenceHelper.GetPatternHash(DayOfWeek.Friday, weekDays, 1); - // - // //Assert - // action.Should().Throw(); - // } } \ No newline at end of file diff --git a/test/DateRecurrenceR.Tests.Unit/Usings.cs b/test/DateRecurrenceR.Tests.Unit/Usings.cs index 8c927eb..2c042d5 100644 --- a/test/DateRecurrenceR.Tests.Unit/Usings.cs +++ b/test/DateRecurrenceR.Tests.Unit/Usings.cs @@ -1 +1,2 @@ -global using Xunit; \ No newline at end of file +global using Xunit; +global using static DateRecurrenceR.Internals.Constant; \ No newline at end of file