diff --git a/RunningStatistics.Tests/CountMap/TestCountMap.cs b/RunningStatistics.Tests/CountMap/TestCountMap.cs index c7fc5e3..5db8950 100644 --- a/RunningStatistics.Tests/CountMap/TestCountMap.cs +++ b/RunningStatistics.Tests/CountMap/TestCountMap.cs @@ -1,8 +1,161 @@ using System; +using System.Collections.Generic; +using Xunit; namespace RunningStatistics.Tests.CountMap; public partial class TestCountMap() : AbstractRunningStatsTest>( - () => Random.Shared.Next(0, 100), - () => new CountMap()); \ No newline at end of file + () => Random.Shared.Next(0, 100), + () => new CountMap()) +{ + [Fact] + public void MinKey_ThrowsIfEmpty() + { + var countMap = new CountMap(); + Assert.Throws(() => countMap.MinKey()); + } + + [Fact] + public void MaxKey_ThrowsIfEmpty() + { + var countMap = new CountMap(); + Assert.Throws(() => countMap.MaxKey()); + } + + [Fact] + public void MinKey_ThrowsIfObservationIsNotComparable() + { + var countMap = new CountMap(); + countMap.Fit(new object(), 1); + countMap.Fit(new object(), 2); + Assert.Throws(() => countMap.MinKey()); + } + + [Fact] + public void MaxKey_ThrowsIfObservationIsNotComparable() + { + var countMap = new CountMap(); + countMap.Fit(new object(), 1); + countMap.Fit(new object(), 2); + Assert.Throws(() => countMap.MaxKey()); + } + + [Fact] + public void MinKey_ReturnsMinimumKeyForNumerics() + { + var countMap = new CountMap(); + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + + Assert.Equal(1, countMap.MinKey()); + } + + [Fact] + public void MaxKey_ReturnsMaximumKeyForNumerics() + { + var countMap = new CountMap(); + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + + Assert.Equal(3, countMap.MaxKey()); + } + + [Fact] + public void MinKey_ReturnsMinimumKeyForStrings() + { + var countMap = new CountMap(); + countMap.Fit("a", 2); + countMap.Fit("b", 3); + countMap.Fit("c", 1); + + Assert.Equal("a", countMap.MinKey()); + } + + [Fact] + public void MaxKey_ReturnsMaximumKeyForStrings() + { + var countMap = new CountMap(); + countMap.Fit("a", 2); + countMap.Fit("b", 3); + countMap.Fit("c", 1); + + Assert.Equal("c", countMap.MaxKey()); + } + + [Fact] + public void MinKey_UsesCustomComparer() + { + var comparer = Comparer.Create((x, y) => Comparer.Default.Compare(-x, -y)); + var countMap = new CountMap(); + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + Assert.Equal(3, countMap.MinKey(comparer)); + } + + [Fact] + public void MaxKey_UsesCustomComparer() + { + var comparer = Comparer.Create((x, y) => Comparer.Default.Compare(-x, -y)); + var countMap = new CountMap(); + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + Assert.Equal(1, countMap.MaxKey(comparer)); + } + + [Fact] + public void CountMap_WithCustomComparer_MinKey_ReturnsCorrectValue() + { + var comparer = Comparer.Create((x, y) => Comparer.Default.Compare(-x, -y)); + var countMap = new CountMap(comparer); + + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + + Assert.Equal(3, countMap.MinKey()); + } + + [Fact] + public void CountMap_WithCustomComparer_MaxKey_ReturnsCorrectValue() + { + var comparer = Comparer.Create((x, y) => Comparer.Default.Compare(-x, -y)); + var countMap = new CountMap(comparer); + + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + + Assert.Equal(1, countMap.MaxKey()); + } + + [Fact] + public void CountMap_MinKey_NullComparer_UsesDefaultComparer() + { + var comparer = Comparer.Create((x, y) => Comparer.Default.Compare(-x, -y)); + var countMap = new CountMap(comparer); + + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + + Assert.Equal(1, countMap.MinKey(null)); + } + + [Fact] + public void CountMap_MaxKey_NullComparer_UsesDefaultComparer() + { + var comparer = Comparer.Create((x, y) => Comparer.Default.Compare(-x, -y)); + var countMap = new CountMap(comparer); + + countMap.Fit(1, 2); + countMap.Fit(2, 3); + countMap.Fit(3, 1); + + Assert.Equal(3, countMap.MaxKey(null)); + } +} \ No newline at end of file diff --git a/RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs b/RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs index 6163cb8..e8025a0 100644 --- a/RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs +++ b/RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs @@ -4,50 +4,6 @@ namespace RunningStatistics.Tests.CountMap; public partial class TestCountMap { - [Fact] - public void MinKey_ReturnsMinimumKeyForNumerics() - { - var countMap = new CountMap(); - countMap.Fit(1, 2); - countMap.Fit(2, 3); - countMap.Fit(3, 1); - - Assert.Equal(1, countMap.MinKey()); - } - - [Fact] - public void MaxKey_ReturnsMaximumKeyForNumerics() - { - var countMap = new CountMap(); - countMap.Fit(1, 2); - countMap.Fit(2, 3); - countMap.Fit(3, 1); - - Assert.Equal(3, countMap.MaxKey()); - } - - [Fact] - public void MinKey_ReturnsMinimumKeyForStrings() - { - var countMap = new CountMap(); - countMap.Fit("a", 2); - countMap.Fit("b", 3); - countMap.Fit("c", 1); - - Assert.Equal("a", countMap.MinKey()); - } - - [Fact] - public void MaxKey_ReturnsMaximumKeyForStrings() - { - var countMap = new CountMap(); - countMap.Fit("a", 2); - countMap.Fit("b", 3); - countMap.Fit("c", 1); - - Assert.Equal("c", countMap.MaxKey()); - } - [Fact] public void Sum_ReturnsCorrectSum() { diff --git a/RunningStatistics/Extensions/CountMapExtensions.cs b/RunningStatistics/Extensions/CountMapExtensions.cs index 3bb1409..da3d38e 100644 --- a/RunningStatistics/Extensions/CountMapExtensions.cs +++ b/RunningStatistics/Extensions/CountMapExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; // ReSharper disable MemberCanBePrivate.Global @@ -10,24 +11,6 @@ namespace RunningStatistics; public static class CountMapExtensions { - /// - /// Find the minimum key in a CountMap. - /// - public static T MinKey(this CountMap countMap) where T : notnull - { - var minKey = countMap.Keys.Min(); - return minKey ?? throw new NullReferenceException(); - } - - /// - /// Find the maximum key in a CountMap. - /// - public static T MaxKey(this CountMap countMap) where T : notnull - { - var maxKey = countMap.Keys.Max(); - return maxKey ?? throw new NullReferenceException(); - } - /// /// Find the sum of all observations in a CountMap of integers. /// diff --git a/RunningStatistics/Statistics/CountMap.cs b/RunningStatistics/Statistics/CountMap.cs index 117aa5f..efbf772 100644 --- a/RunningStatistics/Statistics/CountMap.cs +++ b/RunningStatistics/Statistics/CountMap.cs @@ -17,6 +17,25 @@ public sealed class CountMap : RunningStatisticBase>, { private readonly Dictionary _dict = new(); private long _nobs; + + + /// + /// Creates a CountMap object with the default observation comparer. + /// + public CountMap() + { + // leave Comparer as null to indicate default comparer is to be used, + // or to indicate that no ordering is possible + Comparer = null; + } + + /// + /// Creates a CountMap object with the given observation comparer. + /// + public CountMap(IComparer? comparer) + { + Comparer = comparer; + } /// @@ -42,7 +61,12 @@ public sealed class CountMap : RunningStatisticBase>, /// public IEnumerable Values => _dict.Values; + /// + /// Gets the value comparer used for ordering observations. + /// + public IComparer? Comparer { get; } + protected override long GetNobs() => _nobs; public override void Fit(TObs value, long count) @@ -85,6 +109,86 @@ public override void Reset() public bool ContainsKey(TObs key) => _dict.ContainsKey(key); public bool TryGetValue(TObs key, out long value) => _dict.TryGetValue(key, out value); + + /// + /// Finds the minimum key in the count map. + /// + /// The comparer to use for finding the minimum key. + /// If null, the default comparer is used. + /// The minimum key in the count map, if any exist. + /// + /// Thrown if the count map is empty and no minimum key exists. + /// + public TObs MinKey(IComparer? comparer) + { + comparer ??= Comparer.Default; + + using var enumerator = _dict.Keys.GetEnumerator(); + if (!enumerator.MoveNext()) + { + throw new KeyNotFoundException("The minimum key does not exist."); + } + + var min = enumerator.Current; + while (enumerator.MoveNext()) + { + if (comparer.Compare(enumerator.Current, min) < 0) + { + min = enumerator.Current; + } + } + + return min!; + } + + /// + /// Finds the minimum key in the count map. + /// + /// The minimum key in the count map, if any exist. + /// + /// Thrown if the count map is empty and no minimum key exists. + /// + public TObs MinKey() => MinKey(Comparer); + + /// + /// Finds the maximum key in the count map. + /// + /// The comparer to use for finding the maximum key. + /// If null, the default comparer is used. + /// The maximum key in the count map, if any exist. + /// + /// Thrown if the count map is empty and no maximum key exists. + /// + public TObs MaxKey(IComparer? comparer) + { + comparer ??= Comparer.Default; + + using var enumerator = _dict.Keys.GetEnumerator(); + if (!enumerator.MoveNext()) + { + throw new KeyNotFoundException("The maximum key does not exist."); + } + + var max = enumerator.Current; + while (enumerator.MoveNext()) + { + if (comparer.Compare(enumerator.Current, max) > 0) + { + max = enumerator.Current; + } + } + + return max!; + } + + /// + /// Finds the maximum key in the count map. + /// + /// The maximum key in the count map, if any exist. + /// + /// Thrown if the count map is empty and no maximum key exists. + /// + public TObs MaxKey() => MaxKey(Comparer); public IEnumerator> GetEnumerator() => _dict.GetEnumerator();