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
16 changes: 16 additions & 0 deletions Containers.Test/OrderedCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,22 @@ public void WorksWithStrings_MaintainsAlphabeticalOrder()
Assert.AreEqual("zebra", collection[3]);
}

[TestMethod]
public void Constructor_WithNullableType_WithoutComparer_SortsNullFirst()
{
OrderedCollection<int?> collection = [3, null, 1];
OrderedCollection<int?> withCapacity = new(10) { 2, null };
OrderedCollection<int?> fromCollection = new([2, null, 2]);

Assert.AreSequenceEqual([null, 1, 3], collection);
Assert.AreSequenceEqual([null, 2], withCapacity);
Assert.AreSequenceEqual([null, 2, 2], fromCollection);
}

[TestMethod]
public void Constructor_WithNullableOfNonComparableType_WithoutComparer_ThrowsArgumentException() =>
Assert.ThrowsExactly<ArgumentException>(() => new OrderedCollection<KeyValuePair<int, int>?>());

[TestMethod]
public void Constructor_WithNonComparableType_WithoutComparer_ThrowsArgumentException()
{
Expand Down
18 changes: 18 additions & 0 deletions Containers.Test/OrderedMapTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,24 @@ public void Values_IsReadOnly()
/// <summary>
/// Tests error handling for constructor with non-comparable type.
/// </summary>
// OrderedMap constrains TKey to notnull, as Dictionary does, so a nullable key type draws a nullability
// warning at compile time. It must still construct and order keys at run time.
#pragma warning disable CS8714 // Nullability of type argument doesn't match 'notnull' constraint
[TestMethod]
public void Constructor_NullableKeyType_SortsKeys()
{
OrderedMap<int?, string> map = new() { [3] = "three", [1] = "one" };
OrderedMap<int?, string> withCapacity = new(10) { [5] = "five", [2] = "two" };

Assert.AreSequenceEqual([1, 3], map.Keys);
Assert.AreSequenceEqual([2, 5], withCapacity.Keys);
}

[TestMethod]
public void Constructor_NullableOfNonComparableKeyType_ThrowsArgumentException() =>
Assert.ThrowsExactly<ArgumentException>(() => new OrderedMap<KeyValuePair<int, int>?, string>());
#pragma warning restore CS8714

[TestMethod]
public void Constructor_NonComparableType_ThrowsArgumentException()
{
Expand Down
16 changes: 16 additions & 0 deletions Containers.Test/OrderedSetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@ public void Constructor_WithCollectionAndComparer_CreatesSetFromCollectionWithCo
Assert.AreSequenceEqual(expected, set);
}

[TestMethod]
public void Constructor_NullableTypeWithoutComparer_SortsNullFirst()
{
OrderedSet<int?> set = [3, null, 1, null];
OrderedSet<int?> withCapacity = new(10) { 2, null };
OrderedSet<int?> fromCollection = new([2, null, 2]);

Assert.AreSequenceEqual([null, 1, 3], set);
Assert.AreSequenceEqual([null, 2], withCapacity);
Assert.AreSequenceEqual([null, 2], fromCollection);
}

[TestMethod]
public void Constructor_NullableOfNonComparableTypeWithoutComparer_ThrowsArgumentException() =>
Assert.ThrowsExactly<ArgumentException>(() => _ = new OrderedSet<KeyValuePair<int, int>?>());

[TestMethod]
public void Constructor_NonComparableTypeWithoutComparer_ThrowsArgumentException()
{
Expand Down
31 changes: 31 additions & 0 deletions Containers/Comparability.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Containers;

/// <summary>
/// Checks whether a type can be ordered by <see cref="Comparer{T}.Default"/> when no comparer is supplied.
/// </summary>
internal static class Comparability
{
/// <summary>
/// Determines whether <typeparamref name="T"/> has a default ordering.
/// </summary>
/// <remarks>
/// A <see cref="Nullable{T}"/> implements neither comparison interface itself, but <see cref="Comparer{T}.Default"/>
/// orders it through its underlying type, with null first, so the underlying type is the one checked.
/// </remarks>
/// <typeparam name="T">The type to check.</typeparam>
/// <returns><see langword="true"/> if <typeparamref name="T"/> or its underlying type implements <see cref="IComparable{T}"/> or <see cref="IComparable"/>.</returns>
internal static bool HasDefaultOrdering<T>()
{
if (typeof(IComparable<T>).IsAssignableFrom(typeof(T)) || typeof(IComparable).IsAssignableFrom(typeof(T)))
{
return true;
}

Type? underlyingType = Nullable.GetUnderlyingType(typeof(T));
return underlyingType is not null
&& (typeof(IComparable<>).MakeGenericType(underlyingType).IsAssignableFrom(underlyingType)
|| typeof(IComparable).IsAssignableFrom(underlyingType));
}
}
15 changes: 3 additions & 12 deletions Containers/OrderedCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,7 @@ public T this[int index]
/// <exception cref="ArgumentException">Thrown when T does not implement IComparable{T}.</exception>
public OrderedCollection()
{
if (
!typeof(IComparable<T>).IsAssignableFrom(typeof(T))
&& !typeof(IComparable).IsAssignableFrom(typeof(T))
)
if (!Comparability.HasDefaultOrdering<T>())
{
throw new ArgumentException(
$"Type {typeof(T)} must implement IComparable<T> or IComparable when no comparer is provided."
Expand Down Expand Up @@ -116,10 +113,7 @@ public OrderedCollection(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity);

if (
!typeof(IComparable<T>).IsAssignableFrom(typeof(T))
&& !typeof(IComparable).IsAssignableFrom(typeof(T))
)
if (!Comparability.HasDefaultOrdering<T>())
{
throw new ArgumentException(
$"Type {typeof(T)} must implement IComparable<T> or IComparable when no comparer is provided."
Expand Down Expand Up @@ -156,10 +150,7 @@ public OrderedCollection(IEnumerable<T> collection)
{
Ensure.NotNull(collection);

if (
!typeof(IComparable<T>).IsAssignableFrom(typeof(T))
&& !typeof(IComparable).IsAssignableFrom(typeof(T))
)
if (!Comparability.HasDefaultOrdering<T>())
{
throw new ArgumentException(
$"Type {typeof(T)} must implement IComparable<T> or IComparable when no comparer is provided."
Expand Down
13 changes: 3 additions & 10 deletions Containers/OrderedMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ private struct Entry(TKey key, TValue value)
/// </summary>
private readonly List<Entry> items =
comparer is null
&& !typeof(IComparable<TKey>).IsAssignableFrom(typeof(TKey))
&& !typeof(IComparable).IsAssignableFrom(typeof(TKey))
&& !Comparability.HasDefaultOrdering<TKey>()
? throw new ArgumentException(
$"Type {typeof(TKey)} must implement IComparable<TKey> or IComparable when no comparer is provided."
)
Expand Down Expand Up @@ -145,10 +144,7 @@ public OrderedMap(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity);

if (
!typeof(IComparable<TKey>).IsAssignableFrom(typeof(TKey))
&& !typeof(IComparable).IsAssignableFrom(typeof(TKey))
)
if (!Comparability.HasDefaultOrdering<TKey>())
{
throw new ArgumentException(
$"Type {typeof(TKey)} must implement IComparable<TKey> or IComparable when no comparer is provided."
Expand Down Expand Up @@ -187,10 +183,7 @@ public OrderedMap(IDictionary<TKey, TValue> dictionary)
{
Ensure.NotNull(dictionary);

if (
!typeof(IComparable<TKey>).IsAssignableFrom(typeof(TKey))
&& !typeof(IComparable).IsAssignableFrom(typeof(TKey))
)
if (!Comparability.HasDefaultOrdering<TKey>())
{
throw new ArgumentException(
$"Type {typeof(TKey)} must implement IComparable<TKey> or IComparable when no comparer is provided."
Expand Down
15 changes: 3 additions & 12 deletions Containers/OrderedSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,7 @@ public class OrderedSet<T> : ISet<T>
/// <exception cref="ArgumentException">Thrown when T does not implement IComparable{T}.</exception>
public OrderedSet()
{
if (
!typeof(IComparable<T>).IsAssignableFrom(typeof(T))
&& !typeof(IComparable).IsAssignableFrom(typeof(T))
)
if (!Comparability.HasDefaultOrdering<T>())
{
throw new ArgumentException(
$"Type {typeof(T)} must implement IComparable<T> or IComparable when no comparer is provided."
Expand Down Expand Up @@ -106,10 +103,7 @@ public OrderedSet(int capacity)
{
ArgumentOutOfRangeException.ThrowIfNegative(capacity);

if (
!typeof(IComparable<T>).IsAssignableFrom(typeof(T))
&& !typeof(IComparable).IsAssignableFrom(typeof(T))
)
if (!Comparability.HasDefaultOrdering<T>())
{
throw new ArgumentException(
$"Type {typeof(T)} must implement IComparable<T> or IComparable when no comparer is provided."
Expand Down Expand Up @@ -146,10 +140,7 @@ public OrderedSet(IEnumerable<T> collection)
{
Ensure.NotNull(collection);

if (
!typeof(IComparable<T>).IsAssignableFrom(typeof(T))
&& !typeof(IComparable).IsAssignableFrom(typeof(T))
)
if (!Comparability.HasDefaultOrdering<T>())
{
throw new ArgumentException(
$"Type {typeof(T)} must implement IComparable<T> or IComparable when no comparer is provided."
Expand Down
Loading