From bee0091dcda442958008228f38ddda860e3fde41 Mon Sep 17 00:00:00 2001 From: Matthew Edmondson Date: Mon, 2 Nov 2026 03:29:57 +0000 Subject: [PATCH] Implement the non-generic IComparer on NaturalStringComparer [minor] ArrayList.Sort, Array.Sort(Array, IComparer), WPF's CustomSort and the WinForms sorters accept only System.Collections.IComparer, so callers had to write their own adapter. An explicit IComparer.Compare now forwards to the generic Compare, orders nulls the same way, and throws ArgumentException for arguments that aren't strings, as Comparer does. The generic Compare is unchanged and still allocates nothing. Fixes ktsu-dev/Sorting#74 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW3WL4nzXCqdxvXj85f --- CLAUDE.md | 2 +- README.md | 1 + Sorting.Test/NaturalStringComparerTests.cs | 53 ++++++++++++++++++++++ Sorting/NaturalStringComparer.cs | 30 +++++++++++- 4 files changed, 84 insertions(+), 2 deletions(-) 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.