diff --git a/Funcky.Test/EnumerableExtensionTest.cs b/Funcky.Test/EnumerableExtensionTest.cs index 5ca71db8..90dd5b4d 100644 --- a/Funcky.Test/EnumerableExtensionTest.cs +++ b/Funcky.Test/EnumerableExtensionTest.cs @@ -1,9 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Funcky.Extensions; using Funcky.Monads; using Xunit; +using static Funcky.Functional; + namespace Funcky.Test { public class EnumerableExtensionTest @@ -77,12 +80,39 @@ public void WhereSelectFiltersEmptyValues() Assert.Equal(expectedResult, result); } + [Theory] + [MemberData(nameof(ValueReferenceEnumerables))] + public void GivenAnValueEnumerableFirstLastOrNoneGivesTheCorrectOption(List valueEnumerable, List referenceEnumerable) + { + Assert.Equal(ExpectedOptionValue(valueEnumerable), valueEnumerable.FirstOrNone().Match(false, True)); + Assert.Equal(ExpectedOptionValue(referenceEnumerable), referenceEnumerable.FirstOrNone().Match(false, True)); + + Assert.Equal(ExpectedOptionValue(valueEnumerable), valueEnumerable.LastOrNone().Match(false, True)); + Assert.Equal(ExpectedOptionValue(referenceEnumerable), referenceEnumerable.LastOrNone().Match(false, True)); + } + + [Theory] + [MemberData(nameof(ValueReferenceEnumerables))] + public void GivenAnEnumerableSingleOrNoneGivesTheCorrectOption(List valueEnumerable, List referenceEnumerable) + { + ExpectedSingleOrNoneBehaviour(valueEnumerable, () => valueEnumerable.SingleOrNone().Match(false, True)); + ExpectedSingleOrNoneBehaviour(valueEnumerable, () => referenceEnumerable.SingleOrNone().Match(false, True)); + } + + public static TheoryData, List> ValueReferenceEnumerables() + => new TheoryData, List> + { + { new List(), new List() }, + { new List { 1 }, new List { "a" } }, + { new List { 1, 2, 3 }, new List { "a", "b", "c" } }, + }; + private static Option SquareEvenNumbers(int number) => IsEven(number) ? Option.Some(number * number) : Option.None(); private static bool IsEven(int number) => number % 2 == 0; - private void AcceptIntegers(IEnumerable values) + private static void AcceptIntegers(IEnumerable values) { foreach (var value in values) { @@ -90,12 +120,35 @@ private void AcceptIntegers(IEnumerable values) } } - private void AcceptUnits(IEnumerable units) + private static void AcceptUnits(IEnumerable units) { foreach (var unit in units) { Assert.Equal(default, unit); } } + + private static bool ExpectedOptionValue(List valueEnumerable) => + valueEnumerable.Count switch + { + 0 => false, + _ => true, + }; + + private static void ExpectedSingleOrNoneBehaviour(List list, Func singleOrNone) + { + switch (list.Count) + { + case 0: + Assert.False(singleOrNone()); + break; + case 1: + Assert.True(singleOrNone()); + break; + default: + Assert.Throws(() => singleOrNone()); + break; + } + } } } diff --git a/Funcky/Extensions/EnumerableExtensions.cs b/Funcky/Extensions/EnumerableExtensions.cs index 2f662fec..965eb673 100644 --- a/Funcky/Extensions/EnumerableExtensions.cs +++ b/Funcky/Extensions/EnumerableExtensions.cs @@ -74,5 +74,68 @@ public static void Each(this IEnumerable elements, Action action) action(element); } } + + /// + /// Returns the first element of a sequence as an , or a value if the sequence contains no elements. + /// + /// the inner type of the enumerable. + public static Option FirstOrNone(this IEnumerable source) + where TSource : notnull => + source + .Select(Option.Some) + .FirstOrDefault(); + + /// + /// Returns the first element of the sequence as an that satisfies a condition or a value if no such element is found. + /// + /// the inner type of the enumerable. + public static Option FirstOrNone(this IEnumerable source, Func predicate) + where TSource : notnull => + source + .Where(predicate) + .Select(Option.Some) + .FirstOrDefault(); + + /// + /// Returns the last element of a sequence as an , or a value if the sequence contains no elements. + /// + /// the inner type of the enumerable. + public static Option LastOrNone(this IEnumerable source) + where TSource : notnull => + source + .Select(Option.Some) + .LastOrDefault(); + + /// + /// Returns the last element of a sequence that satisfies a condition as an or a value if no such element is found. + /// + /// the inner type of the enumerable. + public static Option LastOrNone(this IEnumerable source, Func predicate) + where TSource : notnull => + source + .Where(predicate) + .Select(Option.Some) + .LastOrDefault(); + + /// + /// Returns the only element of a sequence as an , or a value if the sequence is empty; this method throws an exception if there is more than one element in the sequence. + /// + /// the inner type of the enumerable. + public static Option SingleOrNone(this IEnumerable source) + where TSource : notnull => + source + .Select(Option.Some) + .SingleOrDefault(); + + /// + /// Returns the only element of a sequence that satisfies a specified condition as an or a value if no such element exists; this method throws an exception if more than one element satisfies the condition. + /// + /// the inner type of the enumerable. + public static Option SingleOrNone(this IEnumerable source, Func predicate) + where TSource : notnull => + source + .Where(predicate) + .Select(Option.Some) + .SingleOrDefault(); } } diff --git a/changelog.md b/changelog.md index d5f86ea3..173a4beb 100644 --- a/changelog.md +++ b/changelog.md @@ -9,3 +9,6 @@ * Add nullability annotations to everything except for `Monads.Reader`. * Add a function for creating an `Option` from a nullable value: `Option.From`. * `Either.Match` now throws when called on an `Either` value created using `default(Either)`. +* Add `True` and `False` functions to public API +* Match of `Result` Monad accepts actions +* Add `FirstOrNone`, `LastOrNone` and `SingleOrNone` extension functions