diff --git a/CLAUDE.md b/CLAUDE.md index 37daa63..f58eb65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -ktsu.Sorting is a .NET library providing advanced sorting utilities for specialized sorting scenarios. The primary component is `NaturalStringComparer`, which implements `IComparer` to sort strings containing embedded numbers in a human-friendly order (e.g., "file2" before "file10"). +ktsu.Sorting is a .NET library providing advanced sorting utilities for specialized sorting scenarios. The primary component is `NaturalStringComparer`, which implements `IComparer` (and the non-generic `IComparer`) to sort strings containing embedded numbers in a human-friendly order (e.g., "file2" before "file10"). ## Build Commands diff --git a/README.md b/README.md index c5b0957..d1d001c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ var sortedFiles = files.OrderBy(file => file, new NaturalStringComparer()); - Handles mixed alphanumeric strings - Properly compares strings with leading zeros - Fully compatible with .NET's `IComparer` interface +- Also implements the non-generic `System.Collections.IComparer`, for APIs that accept only that: `ArrayList.Sort`, WPF's `ListCollectionView.CustomSort`, WinForms' `ListView.ListViewItemSorter`. Arguments that aren't strings throw `ArgumentException` - Can be used with `Array.Sort()`, LINQ's `OrderBy()`, and other sorting mechanisms ## Examples diff --git a/Sorting.Test/NaturalStringComparerTests.cs b/Sorting.Test/NaturalStringComparerTests.cs index e2a460b..e27ca8e 100644 --- a/Sorting.Test/NaturalStringComparerTests.cs +++ b/Sorting.Test/NaturalStringComparerTests.cs @@ -4,6 +4,7 @@ namespace ktsu.Sorting.Tests; +using System.Collections; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] @@ -438,4 +439,56 @@ public void Compare_AllocatesNothing() Assert.AreEqual(0L, allocated, $"{allocated} bytes allocated over {pairs.Length * 2000} comparisons"); } + + [TestMethod] + public void NonGenericComparer_SortsAnArrayListNaturally() + { + ArrayList list = ["file10.txt", "file2.txt", null, "file1.txt"]; + + list.Sort(NonGeneric()); + + CollectionAssert.AreEqual(new object?[] { null, "file1.txt", "file2.txt", "file10.txt" }, list); + } + + [TestMethod] + public void NonGenericComparer_OrdersLikeTheGenericCompare() + { + IComparer nonGeneric = NonGeneric(); + string?[] values = [null, "", "file5", "file005", "file10", "file\u0665", "a5c", "5a", "File2"]; + foreach (string? x in values) + { + foreach (string? y in values) + { + Assert.AreEqual(Math.Sign(_comparer.Compare(x, y)), Math.Sign(nonGeneric.Compare(x, y)), $"\"{x}\" vs \"{y}\""); + } + } + } + + [TestMethod] + public void NonGenericComparer_SortsAnObjectArrayNaturally() + { + object[] items = ["img12", "img007", "img7", "img1"]; + + Array.Sort(items, NonGeneric()); + + CollectionAssert.AreEqual(new object[] { "img1", "img007", "img7", "img12" }, items); + } + + [TestMethod] + public void NonGenericComparer_ThrowsForArgumentsThatAreNotStrings() + { + IComparer nonGeneric = NonGeneric(); + + Assert.ThrowsExactly(() => nonGeneric.Compare(5, "file")); + Assert.ThrowsExactly(() => nonGeneric.Compare("file", 5)); + } + + private IComparer NonGeneric() + { + // Checked at run time, so the tests build, and fail, against a comparer without the interface. + object comparer = _comparer; + IComparer? nonGeneric = comparer as IComparer; + Assert.IsNotNull(nonGeneric, "NaturalStringComparer doesn't implement System.Collections.IComparer"); + return nonGeneric; + } } diff --git a/Sorting/NaturalStringComparer.cs b/Sorting/NaturalStringComparer.cs index 198021e..b29e4b9 100644 --- a/Sorting/NaturalStringComparer.cs +++ b/Sorting/NaturalStringComparer.cs @@ -2,6 +2,7 @@ namespace ktsu.Sorting; +using System.Collections; using System.Globalization; /// @@ -12,8 +13,10 @@ namespace ktsu.Sorting; /// in and whether or not it lies outside the Basic Multilingual Plane, and is compared by the value /// it spells rather than by its code points. A number compared with text orders exactly as the /// equivalent ASCII digits would, so every digit script sorts in the same place relative to text. +/// The comparer also implements the non-generic , so it can be handed to APIs +/// that accept only that, such as . /// -public partial class NaturalStringComparer : IComparer +public partial class NaturalStringComparer : IComparer, IComparer { /// /// Compares two strings using natural sorting, where embedded numbers are compared as numeric values. @@ -103,6 +106,31 @@ public int Compare(string? x, string? y) return string.CompareOrdinal(x, y); } + /// + /// Compares two objects that are strings or , exactly as + /// does. + /// + /// First object to compare. + /// Second object to compare. + /// The result of on the two strings. + /// + /// An argument is neither a string nor . + /// + int IComparer.Compare(object? x, object? y) + { + if (x is not (null or string)) + { + throw new ArgumentException($"{nameof(NaturalStringComparer)} compares only strings, not {x.GetType()}.", nameof(x)); + } + + if (y is not (null or string)) + { + throw new ArgumentException($"{nameof(NaturalStringComparer)} compares only strings, not {y.GetType()}.", nameof(y)); + } + + return Compare((string?)x, (string?)y); + } + /// /// Reports whether the code point at is a Unicode decimal digit /// (category Nd), reading a surrogate pair as the one code point it encodes.