Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 155 additions & 2 deletions RunningStatistics.Tests/CountMap/TestCountMap.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,161 @@
using System;
using System.Collections.Generic;
using Xunit;

namespace RunningStatistics.Tests.CountMap;

public partial class TestCountMap()
: AbstractRunningStatsTest<int, CountMap<int>>(
() => Random.Shared.Next(0, 100),
() => new CountMap<int>());
() => Random.Shared.Next(0, 100),
() => new CountMap<int>())
{
[Fact]
public void MinKey_ThrowsIfEmpty()
{
var countMap = new CountMap<int>();
Assert.Throws<KeyNotFoundException>(() => countMap.MinKey());
}

[Fact]
public void MaxKey_ThrowsIfEmpty()
{
var countMap = new CountMap<int>();
Assert.Throws<KeyNotFoundException>(() => countMap.MaxKey());
}

[Fact]
public void MinKey_ThrowsIfObservationIsNotComparable()
{
var countMap = new CountMap<object>();
countMap.Fit(new object(), 1);
countMap.Fit(new object(), 2);
Assert.Throws<ArgumentException>(() => countMap.MinKey());
}

[Fact]
public void MaxKey_ThrowsIfObservationIsNotComparable()
{
var countMap = new CountMap<object>();
countMap.Fit(new object(), 1);
countMap.Fit(new object(), 2);
Assert.Throws<ArgumentException>(() => countMap.MaxKey());
}

[Fact]
public void MinKey_ReturnsMinimumKeyForNumerics()
{
var countMap = new CountMap<int>();
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<int>();
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<string>();
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<string>();
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<int>.Create((x, y) => Comparer<int>.Default.Compare(-x, -y));
var countMap = new CountMap<int>();
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<int>.Create((x, y) => Comparer<int>.Default.Compare(-x, -y));
var countMap = new CountMap<int>();
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<int>.Create((x, y) => Comparer<int>.Default.Compare(-x, -y));
var countMap = new CountMap<int>(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<int>.Create((x, y) => Comparer<int>.Default.Compare(-x, -y));
var countMap = new CountMap<int>(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<int>.Create((x, y) => Comparer<int>.Default.Compare(-x, -y));
var countMap = new CountMap<int>(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<int>.Create((x, y) => Comparer<int>.Default.Compare(-x, -y));
var countMap = new CountMap<int>(comparer);

countMap.Fit(1, 2);
countMap.Fit(2, 3);
countMap.Fit(3, 1);

Assert.Equal(3, countMap.MaxKey(null));
}
}
44 changes: 0 additions & 44 deletions RunningStatistics.Tests/CountMap/TestCountMap_Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,6 @@ namespace RunningStatistics.Tests.CountMap;

public partial class TestCountMap
{
[Fact]
public void MinKey_ReturnsMinimumKeyForNumerics()
{
var countMap = new CountMap<int>();
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<int>();
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<string>();
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<string>();
countMap.Fit("a", 2);
countMap.Fit("b", 3);
countMap.Fit("c", 1);

Assert.Equal("c", countMap.MaxKey());
}

[Fact]
public void Sum_ReturnsCorrectSum()
{
Expand Down
19 changes: 1 addition & 18 deletions RunningStatistics/Extensions/CountMapExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
// ReSharper disable MemberCanBePrivate.Global

Expand All @@ -10,24 +11,6 @@ namespace RunningStatistics;

public static class CountMapExtensions
{
/// <summary>
/// Find the minimum key in a CountMap.
/// </summary>
public static T MinKey<T>(this CountMap<T> countMap) where T : notnull
{
var minKey = countMap.Keys.Min();
return minKey ?? throw new NullReferenceException();
}

/// <summary>
/// Find the maximum key in a CountMap.
/// </summary>
public static T MaxKey<T>(this CountMap<T> countMap) where T : notnull
{
var maxKey = countMap.Keys.Max();
return maxKey ?? throw new NullReferenceException();
}

/// <summary>
/// Find the sum of all observations in a CountMap of integers.
/// </summary>
Expand Down
104 changes: 104 additions & 0 deletions RunningStatistics/Statistics/CountMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ public sealed class CountMap<TObs> : RunningStatisticBase<TObs, CountMap<TObs>>,
{
private readonly Dictionary<TObs, long> _dict = new();
private long _nobs;


/// <summary>
/// Creates a CountMap object with the default observation comparer.
/// </summary>
public CountMap()
{
// leave Comparer as null to indicate default comparer is to be used,
// or to indicate that no ordering is possible
Comparer = null;
}

/// <summary>
/// Creates a CountMap object with the given observation comparer.
/// </summary>
public CountMap(IComparer<TObs>? comparer)
{
Comparer = comparer;
}


/// <summary>
Expand All @@ -42,7 +61,12 @@ public sealed class CountMap<TObs> : RunningStatisticBase<TObs, CountMap<TObs>>,
/// </summary>
public IEnumerable<long> Values => _dict.Values;

/// <summary>
/// Gets the value comparer used for ordering observations.
/// </summary>
public IComparer<TObs>? Comparer { get; }


protected override long GetNobs() => _nobs;

public override void Fit(TObs value, long count)
Expand Down Expand Up @@ -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);

/// <summary>
/// Finds the minimum key in the count map.
/// </summary>
/// <param name="comparer">The comparer to use for finding the minimum key.
/// If null, the default comparer is used.</param>
/// <returns>The minimum key in the count map, if any exist.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown if the count map is empty and no minimum key exists.
/// </exception>
public TObs MinKey(IComparer<TObs>? comparer)
{
comparer ??= Comparer<TObs>.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!;
}

/// <summary>
/// Finds the minimum key in the count map.
/// </summary>
/// <returns>The minimum key in the count map, if any exist.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown if the count map is empty and no minimum key exists.
/// </exception>
public TObs MinKey() => MinKey(Comparer);

/// <summary>
/// Finds the maximum key in the count map.
/// </summary>
/// <param name="comparer">The comparer to use for finding the maximum key.
/// If null, the default comparer is used.</param>
/// <returns>The maximum key in the count map, if any exist.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown if the count map is empty and no maximum key exists.
/// </exception>
public TObs MaxKey(IComparer<TObs>? comparer)
{
comparer ??= Comparer<TObs>.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!;
}

/// <summary>
/// Finds the maximum key in the count map.
/// </summary>
/// <returns>The maximum key in the count map, if any exist.</returns>
/// <exception cref="KeyNotFoundException">
/// Thrown if the count map is empty and no maximum key exists.
/// </exception>
public TObs MaxKey() => MaxKey(Comparer);

public IEnumerator<KeyValuePair<TObs, long>> GetEnumerator() => _dict.GetEnumerator();

Expand Down