Skip to content

Bugfix #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
-------------------- -------------------- --------------------
Expand All @@ -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.
Expand All @@ -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
```
17 changes: 4 additions & 13 deletions samples/UsingWithFewSubranges/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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} ");
}
while (enumeratorSecondMonth.MoveNext()) Console.Write($"{enumeratorSecondMonth.Current:d} ");
26 changes: 23 additions & 3 deletions src/DateRecurrenceR/Helpers/DateHelper.cs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)
};

Expand Down Expand Up @@ -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;
}
}
2 changes: 1 addition & 1 deletion src/DateRecurrenceR/Helpers/WeekDaysHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
26 changes: 3 additions & 23 deletions src/DateRecurrenceR/Helpers/WeeklyRecurrenceHelper.cs
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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;
}
}
6 changes: 6 additions & 0 deletions src/DateRecurrenceR/Internals/Constant.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace DateRecurrenceR.Internals;

internal struct Constant
{
public const int DaysInWeek = 7;
}
10 changes: 6 additions & 4 deletions src/DateRecurrenceR/Recurrence.Weekly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,19 @@ public static IEnumerator<DateOnly> 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,
out var startDate);

if (!canStart) return EmptyEnumerator;

var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval);

return new WeeklyEnumeratorLimitByCount(startDate, takeCount, patternHash);
}

Expand Down Expand Up @@ -67,9 +68,12 @@ public static IEnumerator<DateOnly> 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,
Expand All @@ -79,8 +83,6 @@ public static IEnumerator<DateOnly> GetWeeklyEnumerator(DateOnly beginDate,

var stopDate = DateOnlyMin(toDate, endDate);

var patternHash = WeeklyRecurrenceHelper.GetPatternHash(weekDays, interval);

return new WeeklyEnumeratorLimitByDate(startDate, stopDate, patternHash);
}
}
1 change: 1 addition & 0 deletions src/DateRecurrenceR/Usings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
global using static DateRecurrenceR.Internals.Constant;
16 changes: 3 additions & 13 deletions src/DateRecurrenceR/WeekDays.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ 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);
}

public WeekDays(params DayOfWeek[] daysArray)
{
MinDay = (DayOfWeek) 7;
MinDay = (DayOfWeek) DaysInWeek;

for (var i = 0; i < daysArray.Length; i++)
{
Expand All @@ -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;
}
}
64 changes: 64 additions & 0 deletions test/DateRecurrenceR.Tests.Unit/Helpers/DateHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading
Loading