diff --git a/ScoutHelper/Utils/CollectionExtensions.cs b/ScoutHelper/Utils/CollectionExtensions.cs index aa2e633..c66a859 100644 --- a/ScoutHelper/Utils/CollectionExtensions.cs +++ b/ScoutHelper/Utils/CollectionExtensions.cs @@ -27,6 +27,26 @@ public static IDictionary ToDict(this IEnumerable<(K, V)> source) wh .DistinctBy(entry => entry.Item1) .ToImmutableDictionary(entry => entry.Item1, entry => entry.Item2); + public static IDictionary ToMutableDict(this IEnumerable> source) where K : notnull => + source.Select(entry => (entry.Key, entry.Value)).ToMutableDict(); + + public static IDictionary ToMutableDict(this IEnumerable<(K, V)> source) where K : notnull => + source + .DistinctBy(entry => entry.Item1) + .ToDictionary(entry => entry.Item1, entry => entry.Item2); + + public static IDictionary With(this IDictionary source, params (K, V)[] entries) where K : notnull { + var dict = source.IsReadOnly ? source.ToMutableDict() : source; + entries.ForEach(entry => dict[entry.Item1] = entry.Item2); + return source.IsReadOnly ? dict.ToDict() : source; + } + + public static IDictionary Without(this IDictionary source, params K[] keys) where K : notnull { + var dict = source.IsReadOnly ? source.ToMutableDict() : source; + keys.ForEach(key => dict.Remove(key)); + return source.IsReadOnly ? dict.ToDict() : source; + } + public static IEnumerable SelectWhere(this IEnumerable source, Func filteredSelector) => source .Select(filteredSelector) diff --git a/ScoutHelper/Utils/Functional/FunctionalExtensions.cs b/ScoutHelper/Utils/Functional/FunctionalExtensions.cs index 54dceb3..15e8fca 100644 --- a/ScoutHelper/Utils/Functional/FunctionalExtensions.cs +++ b/ScoutHelper/Utils/Functional/FunctionalExtensions.cs @@ -12,8 +12,10 @@ namespace ScoutHelper.Utils.Functional; public static class FunctionalExtensions { #region maybe - public static IEnumerable SelectMaybe(this IEnumerable source, Func> selector) - where U : struct => + public static IEnumerable SelectMaybe(this IEnumerable> source) => + source.SelectMaybe(value => value); + + public static IEnumerable SelectMaybe(this IEnumerable source, Func> selector) => source.SelectWhere( value => { var result = selector.Invoke(value); diff --git a/ScoutHelper/Utils/Utils.cs b/ScoutHelper/Utils/Utils.cs index cd0738c..1870957 100644 --- a/ScoutHelper/Utils/Utils.cs +++ b/ScoutHelper/Utils/Utils.cs @@ -2,20 +2,13 @@ using System.Numerics; using System.Text; using System.Text.RegularExpressions; +using CSharpFunctionalExtensions; using ImGuiNET; using ScoutHelper.Models; namespace ScoutHelper.Utils; public static partial class Utils { - // visible for testing - public static readonly IDictionary PatchMaxMarks = new Dictionary() { - { Patch.ARR, 17 }, - { Patch.HW, 12 }, - { Patch.SB, 12 }, - { Patch.SHB, 12 }, - { Patch.EW, 16 }, - }.VerifyEnumDictionary(); public static void CreateTooltip(string text, float width = 12f) { ImGui.BeginTooltip(); @@ -46,7 +39,7 @@ string link var variables = new Dictionary() { { "#", trainList.Count.ToString() }, - { "#max", PatchMaxMarks[highestPatch].ToString() }, + { "#max", highestPatch.MaxMarks().ToString() }, { "link", link }, { "patch", highestPatch.ToString() }, { "tracker", tracker }, @@ -124,3 +117,15 @@ private static IEnumerable Tokenize(string s) { #endregion } + +public static class UtilExtensions { + private static readonly IDictionary PatchMaxMarks = new Dictionary() { + { Patch.ARR, 17 }, + { Patch.HW, 12 }, + { Patch.SB, 12 }, + { Patch.SHB, 12 }, + { Patch.EW, 16 }, + }.VerifyEnumDictionary(); + + public static uint MaxMarks(this Patch patch) => PatchMaxMarks[patch]; +} diff --git a/ScoutHelperTests/TestUtils/FsCheck/Arbs.cs b/ScoutHelperTests/TestUtils/FsCheck/Arbs.cs index 7404bff..50d645b 100644 --- a/ScoutHelperTests/TestUtils/FsCheck/Arbs.cs +++ b/ScoutHelperTests/TestUtils/FsCheck/Arbs.cs @@ -1,32 +1,39 @@ -using FsCheck; +using CSharpFunctionalExtensions; +using FsCheck; using ScoutHelper; using ScoutHelper.Models; +using ScoutHelper.Utils; using static ScoutHelper.Utils.Utils; namespace ScoutHelperTests.TestUtils.FsCheck; public static class Arbs { - public static Arbitrary NonEmptyString() => - Arb.Default.NonEmptyString() + public static Arbitrary String() => + Arb.Default.UnicodeString() .Generator - .Select(nes => nes.ToString()) + .Select(s => s.ToString()) .ToArbitrary(); - public static Arbitrary OfEnum() where T : struct, Enum => Gen.Elements(Enum.GetValues()).ToArbitrary(); - - public static Arbitrary> ListOf(Gen gen) => - Gen.ListOf(gen) - .Select(list => list.ToList()) + public static Arbitrary NonEmptyString() => + String() + .Generator + .Where(s => !string.IsNullOrEmpty(s)) .ToArbitrary(); - public static Arbitrary> NonEmptyList() => - Arb.Default - .List() - .Filter(list => 0 < list.Count) - .Generator - .Select(list => (ICollection)list) + public static Arbitrary> DictOf(Gen keyGen, Gen valueGen) where K : notnull => + FsCheckUtils.Zip(keyGen, valueGen) + .ListOf() + .Select(entryList => entryList.ToDict()) .ToArbitrary(); + public static Arbitrary WithNulls(Gen gen) => + RandomFreq( + gen.Select(value => (T?)value), + Gen.Constant((T?)default) + ); + + public static Arbitrary OfEnum() where T : struct, Enum => Gen.Elements(Enum.GetValues()).ToArbitrary(); + public static Arbitrary> EnumDict() where K : struct, Enum where V : notnull { var enumValues = Enum.GetValues(); return Gen.ListOf(enumValues.Length, Arb.Generate()) @@ -48,7 +55,7 @@ public static Arbitrary> PartialEnumDict() where K : str .ToArbitrary(); public static Arbitrary CopyTemplate() { - var trainList = Arbs.ListOf(Gen.Constant(new TrainMob())); + var trainList = Gen.Constant(new TrainMob()).ListOf().ToArbitrary(); var tracker = Arb.Default.String(); var worldName = Arb.Default.String(); var highestPatch = OfEnum(); @@ -59,7 +66,7 @@ public static Arbitrary CopyTemplate() { .Select( arbs => (arbs, new List<(string?, string?)>() { ("{#}", arbs.a.Count.ToString()), - ("{#max}", PatchMaxMarks[arbs.d].ToString()), + ("{#max}", arbs.d.MaxMarks().ToString()), ("{tracker}", arbs.b), ("{world}", arbs.c), ("{patch}", arbs.d.ToString()), @@ -68,20 +75,14 @@ public static Arbitrary CopyTemplate() { ) .SelectMany( acc => - Gen.Choose(0, 10) - .SelectMany( - f => - Gen.Frequency( - Tuple.Create(f, Gen.Elements<(string?, string?)>(acc.Item2)), - Tuple.Create( - 10 - f, - Arb.Generate() - .Select(s => s?.ToString()?.TrimEnd('\\')) - .Select(s => (s, s)) - ) - ) + RandomFreq( + Gen.Elements<(string?, string?)>(acc.Item2), + String().Generator + .Select(s => ((string?)s)?.TrimEnd('\\')) + .Select(s => (s, s)) ) .ListOf() + .Generator .Select( chunks => ( string.Join(null, chunks.Select(chunk => chunk.Item1)), @@ -104,7 +105,7 @@ public static Arbitrary CopyTemplate() { } public record struct CopyTemplateArb( - List TrainList, + IList TrainList, string Tracker, string WorldName, Patch HighestPatch, @@ -112,4 +113,49 @@ public record struct CopyTemplateArb( string Template, string Expected ); + + public static Arbitrary> MaybeArb(Gen gen, bool includeNulls = false) => + (includeNulls ? WithNulls(gen).Generator : gen.Select(value => (T?)value!)) + .Select( + value => + Maybe + .From(value) + .Select(maybeValue => (T)maybeValue!) + ) + .ToArbitrary(); + + public static Arbitrary RandomFreq(params Gen[] gens) => + Gen.Choose(0, 100) + .ListOf(gens.Length) + .SelectMany( + freqs => + Gen.Frequency(freqs.Zip(gens).Select(f => Tuple.Create(f.First, f.Second))) + ) + .ToArbitrary(); +} + +public static class ArbExtensions { + public static Arbitrary> ListOf(this Arbitrary arb) => arb.Generator.ListOf().ToArbitrary(); + + public static Arbitrary> NonEmptyListOf(this Arbitrary arb) => + arb.Generator.NonEmptyListOf().ToArbitrary(); + + public static Arbitrary> DictWith( + this Arbitrary keyArb, + Arbitrary valueArb, + params K[] excluding + ) where K : notnull => + Arbs.DictOf(keyArb.Generator, valueArb.Generator) + .Generator + .Select(dict => dict.ToMutableDict()) + .Select( + dict => { + excluding.ForEach(key => dict.Remove(key)); + return dict.ToDict(); + } + ) + .ToArbitrary(); + + public static Arbitrary> ToMaybeArb(this Arbitrary arb, bool includeNulls = false) => + Arbs.MaybeArb(arb.Generator, includeNulls); } diff --git a/ScoutHelperTests/TestUtils/FsCheck/FsCheckUtils.cs b/ScoutHelperTests/TestUtils/FsCheck/FsCheckUtils.cs index a375159..cd04613 100644 --- a/ScoutHelperTests/TestUtils/FsCheck/FsCheckUtils.cs +++ b/ScoutHelperTests/TestUtils/FsCheck/FsCheckUtils.cs @@ -96,8 +96,6 @@ public static Property ForAll( Action body ) => Prop.ForAll(Zip(a, b, c, d, e, f, g, h), x => body.Invoke(x.a, x.b, x.c, x.d, x.e, x.f, x.g, x.h)); - public static Arbitrary ToArbitrary(this Gen gen) => Arb.From(gen); - public static void Replay(this Property prop, int a, int b) { var config = Configuration.QuickThrowOnFailure; config.Replay = Random.StdGen.NewStdGen(a, b); diff --git a/ScoutHelperTests/Utils/CollectionExtensionsTest.cs b/ScoutHelperTests/Utils/CollectionExtensionsTest.cs index ea5bfd6..650cf15 100644 --- a/ScoutHelperTests/Utils/CollectionExtensionsTest.cs +++ b/ScoutHelperTests/Utils/CollectionExtensionsTest.cs @@ -39,11 +39,11 @@ public void ForEach_WithIndex_EmptyEnumerable() { [Property] public Property ForEach_NonEmptyEnumerable() => FsCheckUtils.ForAll( - Arbs.NonEmptyList(), + Arbs.String().NonEmptyListOf(), testEnumerable => { // DATA - int F(string? s) => s?.Length ?? 0; - var expected = testEnumerable.Select((Func)F).ToImmutableArray(); + int F(string s) => s.Length; + var expected = testEnumerable.Select(F).ToImmutableArray(); // WHEN var actual = new List(); @@ -56,11 +56,11 @@ public Property ForEach_NonEmptyEnumerable() => FsCheckUtils.ForAll( [Property] public Property ForEach_WithIndex_NonEmptyEnumerable() => FsCheckUtils.ForAll( - Arbs.NonEmptyList(), + Arbs.String().NonEmptyListOf(), testEnumerable => { // DATA - int F(string? s, int i) => (s?.Length ?? 0) + i; - var expected = testEnumerable.Select((Func)F).ToImmutableArray(); + int F(string s, int i) => s.Length + i; + var expected = testEnumerable.Select(F).ToImmutableArray(); // WHEN var actual = new List(); @@ -73,7 +73,7 @@ public Property ForEach_WithIndex_NonEmptyEnumerable() => FsCheckUtils.ForAll( [Property] public Property ForEach_ReturnsOriginalEnumerable() => FsCheckUtils.ForAll( - Arbs.NonEmptyList(), + Arbs.String().NonEmptyListOf(), testEnumerable => { // WHEN var actualNumItems = 0; @@ -87,7 +87,7 @@ public Property ForEach_ReturnsOriginalEnumerable() => FsCheckUtils.ForAll( [Property] public Property ForEach_WithIndex_ReturnsOriginalEnumerable() => FsCheckUtils.ForAll( - Arbs.NonEmptyList(), + Arbs.String().NonEmptyListOf(), testEnumerable => { // WHEN var actualNumItems = 0; diff --git a/ScoutHelperTests/Utils/Functional/FunctionalExtensionsTest.cs b/ScoutHelperTests/Utils/Functional/FunctionalExtensionsTest.cs new file mode 100644 index 0000000..31c1e17 --- /dev/null +++ b/ScoutHelperTests/Utils/Functional/FunctionalExtensionsTest.cs @@ -0,0 +1,91 @@ +using CSharpFunctionalExtensions; +using FluentAssertions; +using FsCheck; +using FsCheck.Xunit; +using JetBrains.Annotations; +using ScoutHelper; +using ScoutHelper.Utils.Functional; +using ScoutHelperTests.TestUtils.FsCheck; +using static ScoutHelperTests.TestUtils.FsCheck.FsCheckUtils; + +namespace ScoutHelperTests.Utils.Functional; + +[TestSubject(typeof(FunctionalExtensions))] +public class FunctionalExtensionsTest { + [Property] + public Property MaybeGet_MissingKey() => ForAll( + Arbs.String().DictWith(Arbs.String()), + Arbs.String(), + (arbitraryDict, lookupKey) => { + // DATA + var dict = arbitraryDict.Without(lookupKey); + + // WHEN + var actual = dict.MaybeGet(lookupKey); + + // THEN + actual.Should().Be(Maybe.None); + } + ); + + [Property] + public Property MaybeGet_ContainsKey() => ForAll( + Arbs.String().DictWith(Arbs.String()), + Arbs.String(), + Arbs.String(), + (arbitraryDict, lookupKey, lookupValue) => { + // DATA + var dict = arbitraryDict.With((lookupKey, lookupValue)); + + // WHEN + var actual = dict.MaybeGet(lookupKey); + + // THEN + actual.Should().Be(Maybe.From(lookupValue)); + } + ); + + [Property] + public Property MaybeGet_EmptyDict() => ForAll( + Arbs.String(), + lookupKey => { + // DATA + var dict = new Dictionary(); + + // WHEN + var actual = dict.MaybeGet(lookupKey); + + // THEN + actual.Should().Be(Maybe.None); + } + ); + + [Property] + public Property SelectMaybe_ExistingList() => ForAll( + Arbs.String().ToMaybeArb().NonEmptyListOf(), + (list) => { + // DATA + var expectedValues = list + .Where(maybe => !Maybe.None.Equals(maybe)) + .Select(value => value.Value); + + // WHEN + var actual = list.SelectMaybe(); + + // THEN + actual.Should().BeEquivalentTo(expectedValues); + } + ); + + // select maybe + // reduce + + // == result == + // as pair + // for each error + // join + // select results + // select values + // to accresult + // with value +}