From aee56d0a7e892d25007046dffa6ad90af50a0ff2 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Sun, 19 Nov 2023 10:50:39 +0100 Subject: [PATCH] Optics for immutable collections --- src/LeviySoft.Visor.Tests/PropertyTests.cs | 58 +++++++++++++++++++ .../Collections/Immutable/Property.cs | 54 +++++++++++++++++ src/LeviySoft.Visor/Internal/Utils.cs | 37 ++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 src/LeviySoft.Visor.Tests/PropertyTests.cs create mode 100644 src/LeviySoft.Visor/Collections/Immutable/Property.cs create mode 100644 src/LeviySoft.Visor/Internal/Utils.cs diff --git a/src/LeviySoft.Visor.Tests/PropertyTests.cs b/src/LeviySoft.Visor.Tests/PropertyTests.cs new file mode 100644 index 0000000..72324c2 --- /dev/null +++ b/src/LeviySoft.Visor.Tests/PropertyTests.cs @@ -0,0 +1,58 @@ +using System.Collections.Immutable; +using Shouldly; +using Xunit; +using LeviySoft.Visor.Collections.Immutable; + +namespace LeviySoft.Visor.Tests; + +public class PropertyTests +{ + [Fact] + public void AtIndexTest() + { + var list = ImmutableList.Create(1, 2, 3); + var sut = Property.IImmutableList.AtIndex(1); + + sut.MaybeGet(list).ShouldBe(2); + sut.MaybeGet(ImmutableList.Empty).ShouldBeNull(); + + sut.Update(i => i * 2)(list).ShouldBe(ImmutableList.Create(1, 4, 3)); + sut.Update(i => i * 2)(ImmutableList.Empty).ShouldBe(ImmutableList.Empty); + } + + [Fact] + public void FirstTest() + { + var list = ImmutableList.Create("a", "bb", "ccc"); + var sut = Property.IImmutableList.First(s => (s?.Length % 2) == 0); + + sut.MaybeGet(list).ShouldBe("bb"); + sut.MaybeGet(ImmutableList.Empty).ShouldBeNull(); + + sut.Update(s => s + s?.Length)(list).ShouldBe(ImmutableList.Create("a", "bb2", "ccc")); + sut.Update(s => s + s?.Length)(ImmutableList.Empty).ShouldBe(ImmutableList.Empty); + } + + [Fact] + public void AtKeyTest() + { + var bld = ImmutableDictionary.CreateBuilder(); + bld.Add("a", 1); + bld.Add("b", 2); + var map = bld.ToImmutable(); + var sut = Property.IImmutableDictionary.AtKey("b"); + var sut2 = Property.IImmutableDictionary.AtKey("b", 42); + + sut.MaybeGet(map).ShouldBe(2); + sut.MaybeGet(ImmutableDictionary.Empty).ShouldBe(0); + + sut2.MaybeGet(map).ShouldBe(2); + sut2.MaybeGet(ImmutableDictionary.Empty).ShouldBe(42); + + sut.Update(v => v * 2)(map)["b"].ShouldBe(4); + sut.Update(v => v * 2)(ImmutableDictionary.Empty).ShouldBe(ImmutableDictionary.Empty); + + sut2.Update(v => v * 2)(map)["b"].ShouldBe(4); + sut2.Update(v => v * 2)(ImmutableDictionary.Empty)["b"].ShouldBe(84); + } +} diff --git a/src/LeviySoft.Visor/Collections/Immutable/Property.cs b/src/LeviySoft.Visor/Collections/Immutable/Property.cs new file mode 100644 index 0000000..aa60b69 --- /dev/null +++ b/src/LeviySoft.Visor/Collections/Immutable/Property.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using LeviySoft.Visor.Internal; + +namespace LeviySoft.Visor.Collections.Immutable; + +/// +/// Optics for System.Collections.Immutable +/// +public static class Property +{ + /// + /// Optics for System.Collections.Immutable.IImmutableList + /// + public static class IImmutableList + { + public static IProperty, T> AtIndex(int index) => + Property, T>.New( + list => index < list.Count ? list[index] : default, + upd => list => index < list.Count ? list.SetItem(index, upd(list[index])) : list + ); + + public static IProperty, T> First() where T : class? => AtIndex(0); + + public static IProperty, T> First(Func predicate) where T : class? => + Property, T>.New( + list => list.FirstOrDefault(predicate), + upd => list => + { + var index = list.FindIndex(predicate); + return index >= 0 ? list.SetItem(index, upd(list[index])) : list; + } + ); + } + + /// + /// Optics for System.Collections.Immutable.IImmutableDictionary + /// + public static class IImmutableDictionary + { + public static IProperty, V> AtKey(K key) => + Property, V>.New( + map => map.ContainsKey(key) ? map[key] : default, + upd => map => map.ContainsKey(key) ? map.SetItem(key, upd(map[key])) : map + ); + + public static IProperty, V> AtKey(K key, V defaultVal) => + Property, V>.New( + map => map.ContainsKey(key) ? map[key] : defaultVal, + upd => map => map.SetItem(key, upd(map.ContainsKey(key) ? map[key] : defaultVal)) + ); + } +} diff --git a/src/LeviySoft.Visor/Internal/Utils.cs b/src/LeviySoft.Visor/Internal/Utils.cs new file mode 100644 index 0000000..70701db --- /dev/null +++ b/src/LeviySoft.Visor/Internal/Utils.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace LeviySoft.Visor.Internal; + +internal static class Utils +{ + public static int FindIndex(this IEnumerable source, Predicate predicate) + { + int index = 0; + foreach (T elem in source) + { + if (predicate(elem)) + { + return index; + } + ++index; + } + + return -1; + } + + public static int FindIndex(this IEnumerable source, Func predicate) + { + int index = 0; + foreach (T elem in source) + { + if (predicate(elem)) + { + return index; + } + ++index; + } + + return -1; + } +}