From 093b1e87ee74177fd5e4baa3c2158d05f295e616 Mon Sep 17 00:00:00 2001
From: Sergey Olshanskiy <sergey.olshanskiy@gmail.com>
Date: Fri, 29 Nov 2024 12:26:30 +0100
Subject: [PATCH] Add Union method; up dotnet to 9

---
 samples/Benchmarks/Benchmarks.csproj          |   2 +-
 samples/SampleUsing/SampleUsing.csproj        |   2 +-
 .../UsingWithFewSubranges.csproj              |   2 +-
 .../UsingWithSubrange.csproj                  |   2 +-
 .../Collections/UnionEnumerator.cs            | 115 ++++++++++++++++++
 src/DateRecurrenceR/Core/IInt32Based.cs       |   2 +-
 src/DateRecurrenceR/DateRecurrenceR.csproj    |   2 +-
 src/DateRecurrenceR/Recurrence.cs             |  10 ++
 src/DateRecurrenceR/WeekDays.cs               |   4 +-
 src/DateRecurrenceR/WeekDaysArray.cs          |   2 +-
 .../Collections/UnionEnumeratorTest.cs        | 101 +++++++++++++++
 .../DateRecurrenceR.Tests.Unit.csproj         |   2 +-
 12 files changed, 236 insertions(+), 10 deletions(-)
 create mode 100644 src/DateRecurrenceR/Collections/UnionEnumerator.cs
 create mode 100644 test/DateRecurrenceR.Tests.Unit/Collections/UnionEnumeratorTest.cs

diff --git a/samples/Benchmarks/Benchmarks.csproj b/samples/Benchmarks/Benchmarks.csproj
index 8a1ff02..c0a1081 100644
--- a/samples/Benchmarks/Benchmarks.csproj
+++ b/samples/Benchmarks/Benchmarks.csproj
@@ -2,7 +2,7 @@
 
     <PropertyGroup>
         <OutputType>Exe</OutputType>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
 
diff --git a/samples/SampleUsing/SampleUsing.csproj b/samples/SampleUsing/SampleUsing.csproj
index 48346f5..8b63013 100644
--- a/samples/SampleUsing/SampleUsing.csproj
+++ b/samples/SampleUsing/SampleUsing.csproj
@@ -2,7 +2,7 @@
 
     <PropertyGroup>
         <OutputType>Exe</OutputType>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
 
diff --git a/samples/UsingWithFewSubranges/UsingWithFewSubranges.csproj b/samples/UsingWithFewSubranges/UsingWithFewSubranges.csproj
index 48346f5..8b63013 100644
--- a/samples/UsingWithFewSubranges/UsingWithFewSubranges.csproj
+++ b/samples/UsingWithFewSubranges/UsingWithFewSubranges.csproj
@@ -2,7 +2,7 @@
 
     <PropertyGroup>
         <OutputType>Exe</OutputType>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
 
diff --git a/samples/UsingWithSubrange/UsingWithSubrange.csproj b/samples/UsingWithSubrange/UsingWithSubrange.csproj
index 48346f5..8b63013 100644
--- a/samples/UsingWithSubrange/UsingWithSubrange.csproj
+++ b/samples/UsingWithSubrange/UsingWithSubrange.csproj
@@ -2,7 +2,7 @@
 
     <PropertyGroup>
         <OutputType>Exe</OutputType>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
 
diff --git a/src/DateRecurrenceR/Collections/UnionEnumerator.cs b/src/DateRecurrenceR/Collections/UnionEnumerator.cs
new file mode 100644
index 0000000..c704c65
--- /dev/null
+++ b/src/DateRecurrenceR/Collections/UnionEnumerator.cs
@@ -0,0 +1,115 @@
+using System.Collections;
+
+namespace DateRecurrenceR.Collections;
+
+internal struct UnionEnumerator : IEnumerator<DateOnly>
+{
+    private readonly EWrapper[] _enumerators;
+    private DateOnly? _current = null;
+
+    public UnionEnumerator(IReadOnlyList<IEnumerator<DateOnly>> enumerators)
+    {
+        var hash = new HashSet<EWrapper>();
+
+        for (var i = 0; i < enumerators.Count; i++)
+        {
+            if (enumerators[i] is UnionEnumerator ue)
+            {
+                for (var j = 0; j < ue._enumerators.Length; j++)
+                {
+                    hash.Add(ue._enumerators[j]);
+                }
+            }
+            else
+            {
+                hash.Add(new EWrapper(enumerators[i]));
+            }
+        }
+
+        _enumerators = hash.ToArray();
+
+        Current = default;
+    }
+
+    public bool MoveNext()
+    {
+        var nextIndex = -1;
+
+        for (var i = 0; i < _enumerators.Length; i++)
+        {
+            if (_current.HasValue)
+            {
+                while (_enumerators[i].CanMoveNext && _enumerators[i].Enum.Current <= _current.Value)
+                {
+                    _enumerators[i].MoveNext();
+                }
+            }
+            else
+            {
+                _enumerators[i].MoveNext();
+            }
+        }
+
+        for (var i = 0; i < _enumerators.Length; i++)
+        {
+            if (!_enumerators[i].CanMoveNext) continue;
+
+            _current = _enumerators[i].Enum.Current;
+            nextIndex = i;
+            break;
+        }
+
+        for (var i = 0; i < _enumerators.Length; i++)
+        {
+            if (!_enumerators[i].CanMoveNext || !(_current > _enumerators[i].Enum.Current)) continue;
+
+            _current = _enumerators[i].Enum.Current;
+            nextIndex = i;
+        }
+
+        if (nextIndex < 0)
+        {
+            return false;
+        }
+
+        Current = _enumerators[nextIndex].Enum.Current;
+
+        return true;
+    }
+
+    public void Reset()
+    {
+        throw new NotSupportedException();
+    }
+
+    public DateOnly Current { get; private set; }
+
+    object IEnumerator.Current => Current;
+
+    public void Dispose()
+    {
+    }
+
+    private struct EWrapper
+    {
+        public EWrapper(IEnumerator<DateOnly> @enum)
+        {
+            Enum = @enum;
+            CanMoveNext = true;
+        }
+
+        public IEnumerator<DateOnly> Enum { get; }
+        public bool CanMoveNext { get; private set; }
+
+        public bool MoveNext()
+        {
+            CanMoveNext = Enum.MoveNext();
+            return CanMoveNext;
+        }
+
+        public override int GetHashCode()
+        {
+            return Enum.GetHashCode();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/DateRecurrenceR/Core/IInt32Based.cs b/src/DateRecurrenceR/Core/IInt32Based.cs
index 48f949e..7c5eada 100644
--- a/src/DateRecurrenceR/Core/IInt32Based.cs
+++ b/src/DateRecurrenceR/Core/IInt32Based.cs
@@ -6,7 +6,7 @@ namespace DateRecurrenceR.Core;
 /// 
 /// </summary>
 /// <typeparam name="TSelf"></typeparam>
-#if NET8_0
+#if NET8_0_OR_GREATER
 public interface IInt32Based<TSelf> : IEquatable<TSelf>, IEqualityOperators<TSelf, TSelf, bool>, IMinMaxValue<TSelf>
     where TSelf : IInt32Based<TSelf>;
 #endif
\ No newline at end of file
diff --git a/src/DateRecurrenceR/DateRecurrenceR.csproj b/src/DateRecurrenceR/DateRecurrenceR.csproj
index 7834c61..855f70b 100644
--- a/src/DateRecurrenceR/DateRecurrenceR.csproj
+++ b/src/DateRecurrenceR/DateRecurrenceR.csproj
@@ -9,7 +9,7 @@
         <Version>0.5.3-beta.2</Version>
         <PublishRepositoryUrl>true</PublishRepositoryUrl>
         <ImplicitUsings>enable</ImplicitUsings>
-        <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
+        <TargetFrameworks>net6.0;net8.0;net9.0</TargetFrameworks>
         <Nullable>enable</Nullable>
         <WarningsAsErrors>Nullable</WarningsAsErrors>
         <Deterministic>true</Deterministic>
diff --git a/src/DateRecurrenceR/Recurrence.cs b/src/DateRecurrenceR/Recurrence.cs
index 7c91a40..63e1278 100644
--- a/src/DateRecurrenceR/Recurrence.cs
+++ b/src/DateRecurrenceR/Recurrence.cs
@@ -6,6 +6,16 @@ public readonly partial struct Recurrence
 {
     private static readonly EmptyEnumerator EmptyEnumerator = new();
 
+    public static IEnumerator<DateOnly> Union(IEnumerator<DateOnly> d1, IEnumerator<DateOnly> d2)
+    {
+        return new UnionEnumerator(new[] {d1, d2});
+    }
+
+    public static IEnumerator<DateOnly> Union(params IEnumerator<DateOnly>[] enumerators)
+    {
+        return new UnionEnumerator(enumerators);
+    }
+
     private static DateOnly DateOnlyMin(DateOnly val1, DateOnly val2)
     {
         return val1 <= val2 ? val1 : val2;
diff --git a/src/DateRecurrenceR/WeekDays.cs b/src/DateRecurrenceR/WeekDays.cs
index db932b1..f5f477c 100644
--- a/src/DateRecurrenceR/WeekDays.cs
+++ b/src/DateRecurrenceR/WeekDays.cs
@@ -2,7 +2,7 @@ namespace DateRecurrenceR;
 
 public sealed class WeekDays
 {
-#if NET8_0
+#if NET8_0_OR_GREATER
     private readonly WeekDaysArray _ds = new WeekDaysArray();
 #else
     private readonly bool[] _ds = new bool[DaysInWeek];
@@ -38,7 +38,7 @@ public WeekDays(DayOfWeek day1, DayOfWeek day2, DayOfWeek day3, DayOfWeek day4,
     {
     }
 
-#if NET8_0
+#if NET8_0_OR_GREATER
     public WeekDays(WeekDaysArray daysArray)
     {
         MinDay = (DayOfWeek) DaysInWeek;
diff --git a/src/DateRecurrenceR/WeekDaysArray.cs b/src/DateRecurrenceR/WeekDaysArray.cs
index e0df7ff..0eff699 100644
--- a/src/DateRecurrenceR/WeekDaysArray.cs
+++ b/src/DateRecurrenceR/WeekDaysArray.cs
@@ -1,4 +1,4 @@
-#if NET8_0
+#if NET8_0_OR_GREATER
 namespace DateRecurrenceR;
 
 [System.Runtime.CompilerServices.InlineArray(7)]
diff --git a/test/DateRecurrenceR.Tests.Unit/Collections/UnionEnumeratorTest.cs b/test/DateRecurrenceR.Tests.Unit/Collections/UnionEnumeratorTest.cs
new file mode 100644
index 0000000..0c985eb
--- /dev/null
+++ b/test/DateRecurrenceR.Tests.Unit/Collections/UnionEnumeratorTest.cs
@@ -0,0 +1,101 @@
+using DateRecurrenceR.Collections;
+using DateRecurrenceR.Core;
+using FluentAssertions;
+using JetBrains.Annotations;
+
+namespace DateRecurrenceR.Tests.Unit.Collections;
+
+[TestSubject(typeof(UnionEnumerator))]
+public class UnionEnumeratorTest
+{
+    [Fact]
+    public void Union_two_recurrences()
+    {
+        // Arrange
+        const int takeCount = 5;
+        var interval = new Interval(1);
+        var beginDate = DateOnly.MinValue;
+        var fromDate = beginDate;
+
+        var enumerator1 = Recurrence.Yearly(beginDate, fromDate, takeCount, new DayOfYear(1), interval);
+        var enumerator2 = Recurrence.Yearly(beginDate, fromDate, takeCount, new DayOfYear(2), interval);
+
+        var res = Recurrence.Union(enumerator1, enumerator2);
+
+        // Act
+        var list = new List<DateOnly>();
+
+        while (res.MoveNext())
+        {
+            list.Add(res.Current);
+        }
+
+        //Assert
+        list.Count.Should().Be(takeCount * 2);
+    }
+
+    [Fact]
+    public void Union_one_recurrence_multiple_times()
+    {
+        // Arrange
+        const int takeCount = 5;
+        var dayOfYear = new DayOfYear(256);
+        var interval = new Interval(1);
+        var beginDate = DateOnly.MinValue;
+        var fromDate = beginDate;
+
+        var equivalentEnumerator = Recurrence.Yearly(beginDate, fromDate, takeCount, dayOfYear, interval);
+        var enumerator = Recurrence.Yearly(beginDate, fromDate, takeCount, dayOfYear, interval);
+
+        var res = Recurrence.Union(enumerator, enumerator, enumerator, enumerator);
+        res = Recurrence.Union(res, enumerator, enumerator, enumerator);
+        res = Recurrence.Union(res, enumerator, enumerator);
+        res = Recurrence.Union(res, enumerator);
+
+        // Act
+        var equivalentList = new List<DateOnly>();
+        while (equivalentEnumerator.MoveNext())
+        {
+            equivalentList.Add(equivalentEnumerator.Current);
+        }
+
+        var list = new List<DateOnly>();
+        while (res.MoveNext())
+        {
+            list.Add(res.Current);
+        }
+
+        //Assert
+        list.Count.Should().Be(takeCount);
+        list.Should().BeEquivalentTo(equivalentList);
+    }
+
+    [Fact]
+    public void Union_two_recurrence_multiple_times()
+    {
+        // Arrange
+        const int takeCount = 5;
+        var interval = new Interval(1);
+        var beginDate = DateOnly.MinValue;
+        var fromDate = beginDate;
+
+        var enumerator1 = Recurrence.Yearly(beginDate, fromDate, takeCount, new DayOfYear(1), interval);
+        var enumerator2 = Recurrence.Yearly(beginDate, fromDate, takeCount, new DayOfYear(2), interval);
+
+        var res1 = Recurrence.Union(enumerator1, enumerator1, enumerator1, enumerator1);
+        var res2 = Recurrence.Union(enumerator2, enumerator2, enumerator2, enumerator2);
+        var res = Recurrence.Union(res1, res2);
+        res = Recurrence.Union(res, res1, enumerator2, enumerator1);
+        res = Recurrence.Union(res, res2, enumerator1, enumerator2);
+
+        // Act
+        var list = new List<DateOnly>();
+        while (res.MoveNext())
+        {
+            list.Add(res.Current);
+        }
+
+        //Assert
+        list.Count.Should().Be(takeCount * 2);
+    }
+}
\ No newline at end of file
diff --git a/test/DateRecurrenceR.Tests.Unit/DateRecurrenceR.Tests.Unit.csproj b/test/DateRecurrenceR.Tests.Unit/DateRecurrenceR.Tests.Unit.csproj
index 5f0bd56..05c412d 100644
--- a/test/DateRecurrenceR.Tests.Unit/DateRecurrenceR.Tests.Unit.csproj
+++ b/test/DateRecurrenceR.Tests.Unit/DateRecurrenceR.Tests.Unit.csproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
     <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net9.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <WarningsAsErrors>Nullable</WarningsAsErrors>