Skip to content

Commit

Permalink
Add Range-syntax support to foreach and from in
Browse files Browse the repository at this point in the history
  • Loading branch information
FreeApophis committed Sep 10, 2021
1 parent bf624ce commit 35777b1
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 1 deletion.
2 changes: 1 addition & 1 deletion FrameworkFeatureConstants.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.1' Or '$(TargetFramework)' == 'netcoreapp3.1' Or '$(TargetFramework)' == 'net5.0' Or '$(TargetFramework)' == 'net6.0'">
<DefineConstants>$(DefineConstants);INDEX_OF_CHAR_COMPARISONTYPE_SUPPORTED;TIMESPAN_MULTIPLY_SUPPORTED;NULLABLE_ATTRIBUTES_SUPPORTED;SPLIT_ACCEPTS_STRING_SEPARATOR;LAZY_RETURN_CONSTRUCTOR;QUEUE_TRY_OVERLOADS</DefineConstants>
<DefineConstants>$(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>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)' == 'net5.0' Or '$(TargetFramework)' == 'net6.0'">
<DefineConstants>$(DefineConstants);SET_CURRENT_STACK_TRACE_SUPPORTED</DefineConstants>
Expand Down
102 changes: 102 additions & 0 deletions Funcky.Test/Extensions/RangeExtensionTest.cs
Original file line number Diff line number Diff line change
@@ -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<int> { 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<int> { -2, -1, 0, 1 };
var list = new List<int>();

foreach (var index in ^2..2)
{
list.Add(index);
}

Assert.Equal(expected, list);
}

[Fact]
public void ADownToRangeWorksAsExpected()
{
var expected = new List<int> { 7, 6, 5, 4, 3, 2, 1, 0, -1 };
var list = new List<int>();

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<int> ExpectedFromEnumerable(List<int> enumerable)
=> from x in enumerable
from y in enumerable
select x + y;

private static IEnumerable<int> 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
62 changes: 62 additions & 0 deletions Funcky/Extensions/RangeExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#if RANGE_SUPPORTED
using System.Linq;
using Funcky.Internal;

namespace Funcky.Extensions
{
public static class RangeExtensions
{
public static IEnumerator<int> GetEnumerator(this Range range)
=> IterateRange(ToInt(range.Start), ToInt(range.End));

[Pure]
public static IEnumerable<TResult> Select<TResult>(this Range source, Func<int, TResult> selector)
=> source.ToRangeEnumerable().Select(selector);

[Pure]
public static IEnumerable<TResult> SelectMany<TResult>(this Range source, Func<int, IEnumerable<TResult>> selector)
=> source.ToRangeEnumerable().SelectMany(selector);

[Pure]
public static IEnumerable<TResult> SelectMany<TItem, TResult>(this Range source, Func<int, IEnumerable<TItem>> collectionSelector, Func<int, TItem, TResult> resultSelector)
=> source.ToRangeEnumerable().SelectMany(collectionSelector, resultSelector);

[Pure]
public static IEnumerable<TResult> SelectMany<TResult>(this Range source, Func<int, Range> rangeSelector, Func<int, int, TResult> resultSelector)
=> source.ToRangeEnumerable().SelectMany(TransformSelector(rangeSelector), resultSelector);

[Pure]
public static IEnumerable<TResult> SelectMany<TItem, TResult>(this IEnumerable<TItem> source, Func<TItem, Range> rangeSelector, Func<TItem, int, TResult> resultSelector)
=> source.SelectMany(TransformSelector(rangeSelector), resultSelector);

private static IEnumerator<int> 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<TItem, RangeEnumerable> TransformSelector<TItem>(Func<TItem, Range> 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
20 changes: 20 additions & 0 deletions Funcky/Internal/RangeEnumerable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#if RANGE_SUPPORTED
using System.Collections;

namespace Funcky.Internal
{
internal class RangeEnumerable : IEnumerable<int>
{
private readonly Range _range;

public RangeEnumerable(Range range)
=> _range = range;

public IEnumerator<int> GetEnumerator()
=> _range.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}
}
#endif
7 changes: 7 additions & 0 deletions Funcky/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
#nullable enable
Funcky.Extensions.QueueExtensions
Funcky.Extensions.RangeExtensions
static Funcky.Extensions.QueueExtensions.DequeueOrNone<TItem>(this System.Collections.Concurrent.ConcurrentQueue<TItem>! concurrentQueue) -> Funcky.Monads.Option<TItem>
static Funcky.Extensions.QueueExtensions.DequeueOrNone<TItem>(this System.Collections.Generic.Queue<TItem>! queue) -> Funcky.Monads.Option<TItem>
static Funcky.Extensions.QueueExtensions.PeekOrNone<TItem>(this System.Collections.Concurrent.ConcurrentQueue<TItem>! concurrentQueue) -> Funcky.Monads.Option<TItem>
static Funcky.Extensions.QueueExtensions.PeekOrNone<TItem>(this System.Collections.Generic.Queue<TItem>! queue) -> Funcky.Monads.Option<TItem>
static Funcky.Extensions.RangeExtensions.GetEnumerator(this System.Range range) -> System.Collections.Generic.IEnumerator<int>!
static Funcky.Extensions.RangeExtensions.Select<TResult>(this System.Range source, System.Func<int, TResult>! selector) -> System.Collections.Generic.IEnumerable<TResult>!
static Funcky.Extensions.RangeExtensions.SelectMany<TItem, TResult>(this System.Collections.Generic.IEnumerable<TItem>! source, System.Func<TItem, System.Range>! rangeSelector, System.Func<TItem, int, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
static Funcky.Extensions.RangeExtensions.SelectMany<TItem, TResult>(this System.Range source, System.Func<int, System.Collections.Generic.IEnumerable<TItem>!>! collectionSelector, System.Func<int, TItem, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
static Funcky.Extensions.RangeExtensions.SelectMany<TResult>(this System.Range source, System.Func<int, System.Collections.Generic.IEnumerable<TResult>!>! selector) -> System.Collections.Generic.IEnumerable<TResult>!
static Funcky.Extensions.RangeExtensions.SelectMany<TResult>(this System.Range source, System.Func<int, System.Range>! rangeSelector, System.Func<int, int, TResult>! resultSelector) -> System.Collections.Generic.IEnumerable<TResult>!
10 changes: 10 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`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
Expand Down

0 comments on commit 35777b1

Please sign in to comment.