diff --git a/FrameworkFeatureConstants.props b/FrameworkFeatureConstants.props index 6f376771..dbb1903c 100644 --- a/FrameworkFeatureConstants.props +++ b/FrameworkFeatureConstants.props @@ -1,7 +1,7 @@ - $(DefineConstants);INDEX_OF_CHAR_COMPARISONTYPE_SUPPORTED;TIMESPAN_MULTIPLY_SUPPORTED;NULLABLE_ATTRIBUTES_SUPPORTED;SPLIT_ACCEPTS_STRING_SEPARATOR;LAZY_RETURN_CONSTRUCTOR;QUEUE_TRY_OVERLOADS + $(DefineConstants);INDEX_OF_CHAR_COMPARISONTYPE_SUPPORTED;TIMESPAN_MULTIPLY_SUPPORTED;NULLABLE_ATTRIBUTES_SUPPORTED;SPLIT_ACCEPTS_STRING_SEPARATOR;LAZY_RETURN_CONSTRUCTOR;QUEUE_TRY_OVERLOADS;RANGE_SUPPORTED; $(DefineConstants);SET_CURRENT_STACK_TRACE_SUPPORTED diff --git a/Funcky.Test/Extensions/RangeExtensionTest.cs b/Funcky.Test/Extensions/RangeExtensionTest.cs new file mode 100644 index 00000000..81ca7d8a --- /dev/null +++ b/Funcky.Test/Extensions/RangeExtensionTest.cs @@ -0,0 +1,102 @@ +#if RANGE_SUPPORTED +using System; +using System.Collections.Generic; +using System.Linq; +using FsCheck; +using FsCheck.Xunit; +using Funcky.Extensions; +using Xunit; + +namespace Funcky.Test.Extensions +{ + public class RangeExtensionTest + { + [Fact] + public void ThereIsAMatchingSelectManyForAllCombinationsOfRangeAndEnumerable() + { + var enumerable = new List { 0, 1, 2 }; + var expected = ExpectedFromEnumerable(enumerable).ToList(); + + var rangesOnly = from x in 0..3 + from y in 0..3 + select x + y; + + var firstRange = from x in 0..3 + from y in enumerable + select x + y; + + var lastRange = from x in enumerable + from y in 0..3 + select x + y; + + Assert.Equal(expected, rangesOnly); + Assert.Equal(expected, firstRange); + Assert.Equal(expected, lastRange); + } + + [Fact] + public void YouCanUseARangeInForeachSyntax() + { + var expected = new List { -2, -1, 0, 1 }; + var list = new List(); + + foreach (var index in ^2..2) + { + list.Add(index); + } + + Assert.Equal(expected, list); + } + + [Fact] + public void ADownToRangeWorksAsExpected() + { + var expected = new List { 7, 6, 5, 4, 3, 2, 1, 0, -1 }; + var list = new List(); + + foreach (var index in 7..^2) + { + list.Add(index); + } + + Assert.Equal(expected, list); + } + + [Property] + public Property AnyRangeProducedAValidEnumeration(NonNegativeInt start, bool signStart, NonNegativeInt end, bool signEnd) + { + var range = new Range(new Index(start.Get, signStart), new Index(end.Get, signEnd)); + + var fromRange = from index in range select index; + + return fromRange.SequenceEqual(CreateRange(range)).ToProperty(); + } + + private static IEnumerable ExpectedFromEnumerable(List enumerable) + => from x in enumerable + from y in enumerable + select x + y; + + private static IEnumerable CreateRange(Range range) + => ToInt(range.Start) < ToInt(range.End) + ? Enumerable.Range(ToInt(range.Start), GetDistance(range)) + : Enumerable.Range(ToInt(range.End) + 1, GetDistance(range)).Reverse(); + + private static int GetDistance(Range range) + => GetDistance(range.Start, range.End); + + private static int GetDistance(Index start, Index end) + => GetDistance(ToInt(start), ToInt(end)); + + private static int GetDistance(int start, int end) + => start > end + ? start - end + : end - start; + + private static int ToInt(Index index) + => index.IsFromEnd + ? -index.Value + : index.Value; + } +} +#endif diff --git a/Funcky/Extensions/RangeExtensions.cs b/Funcky/Extensions/RangeExtensions.cs new file mode 100644 index 00000000..93690a03 --- /dev/null +++ b/Funcky/Extensions/RangeExtensions.cs @@ -0,0 +1,62 @@ +#if RANGE_SUPPORTED +using System.Linq; +using Funcky.Internal; + +namespace Funcky.Extensions +{ + public static class RangeExtensions + { + public static IEnumerator GetEnumerator(this Range range) + => IterateRange(ToInt(range.Start), ToInt(range.End)); + + [Pure] + public static IEnumerable Select(this Range source, Func selector) + => source.ToRangeEnumerable().Select(selector); + + [Pure] + public static IEnumerable SelectMany(this Range source, Func> selector) + => source.ToRangeEnumerable().SelectMany(selector); + + [Pure] + public static IEnumerable SelectMany(this Range source, Func> collectionSelector, Func resultSelector) + => source.ToRangeEnumerable().SelectMany(collectionSelector, resultSelector); + + [Pure] + public static IEnumerable SelectMany(this Range source, Func rangeSelector, Func resultSelector) + => source.ToRangeEnumerable().SelectMany(TransformSelector(rangeSelector), resultSelector); + + [Pure] + public static IEnumerable SelectMany(this IEnumerable source, Func rangeSelector, Func resultSelector) + => source.SelectMany(TransformSelector(rangeSelector), resultSelector); + + private static IEnumerator IterateRange(int start, int end) + { + for (var index = start; CanAdvance(index, end); index = Advance(index, GetDirection(start, end))) + { + yield return index; + } + } + + private static RangeEnumerable ToRangeEnumerable(this Range source) + => new(source); + + private static Func TransformSelector(Func rangeSelector) + => value + => rangeSelector(value).ToRangeEnumerable(); + + private static int Advance(int index, int direction) + => index + direction; + + private static bool CanAdvance(int index, int end) + => index != end; + + private static int GetDirection(int start, int end) + => end.CompareTo(start); + + private static int ToInt(Index index) + => index.IsFromEnd + ? -index.Value + : index.Value; + } +} +#endif diff --git a/Funcky/Internal/RangeEnumerable.cs b/Funcky/Internal/RangeEnumerable.cs new file mode 100644 index 00000000..55190bec --- /dev/null +++ b/Funcky/Internal/RangeEnumerable.cs @@ -0,0 +1,20 @@ +#if RANGE_SUPPORTED +using System.Collections; + +namespace Funcky.Internal +{ + internal class RangeEnumerable : IEnumerable + { + private readonly Range _range; + + public RangeEnumerable(Range range) + => _range = range; + + public IEnumerator GetEnumerator() + => _range.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + } +} +#endif diff --git a/Funcky/PublicAPI.Unshipped.txt b/Funcky/PublicAPI.Unshipped.txt index 8a69b235..0d54f95e 100644 --- a/Funcky/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI.Unshipped.txt @@ -1,6 +1,13 @@ #nullable enable Funcky.Extensions.QueueExtensions +Funcky.Extensions.RangeExtensions static Funcky.Extensions.QueueExtensions.DequeueOrNone(this System.Collections.Concurrent.ConcurrentQueue! concurrentQueue) -> Funcky.Monads.Option static Funcky.Extensions.QueueExtensions.DequeueOrNone(this System.Collections.Generic.Queue! queue) -> Funcky.Monads.Option static Funcky.Extensions.QueueExtensions.PeekOrNone(this System.Collections.Concurrent.ConcurrentQueue! concurrentQueue) -> Funcky.Monads.Option static Funcky.Extensions.QueueExtensions.PeekOrNone(this System.Collections.Generic.Queue! queue) -> Funcky.Monads.Option +static Funcky.Extensions.RangeExtensions.GetEnumerator(this System.Range range) -> System.Collections.Generic.IEnumerator! +static Funcky.Extensions.RangeExtensions.Select(this System.Range source, System.Func! selector) -> System.Collections.Generic.IEnumerable! +static Funcky.Extensions.RangeExtensions.SelectMany(this System.Collections.Generic.IEnumerable! source, System.Func! rangeSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static Funcky.Extensions.RangeExtensions.SelectMany(this System.Range source, System.Func!>! collectionSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! +static Funcky.Extensions.RangeExtensions.SelectMany(this System.Range source, System.Func!>! selector) -> System.Collections.Generic.IEnumerable! +static Funcky.Extensions.RangeExtensions.SelectMany(this System.Range source, System.Func! rangeSelector, System.Func! resultSelector) -> System.Collections.Generic.IEnumerable! diff --git a/changelog.md b/changelog.md index 6b0fbfa5..714c2c52 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,16 @@ Funcky adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). with an already set stack trace. The original stack trace is now preserved. Previously this resulted in the stacktrace being replaced (.NET < 5.0) or an error (.NET ≥ 5.0). * Added extensions `DequeueOrNone` and `PeekOrNone` on `Queue` and `ConcurrentQueue` +* Extension functions for `System.Range` to allow the generations of `IEnumerable`s from Range-Syntax: + +```cs + foreach(var i in 1..5) { } + + // ^ must be used to indicate negative numbers + from x in 5..^2 + from y in 1..3 + select (x,y) +``` ## Funcky 2.5.0 ### Reader Monad