Skip to content
Open
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string?>` 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<string?>` (and the non-generic `IComparer`) to sort strings containing embedded numbers in a human-friendly order (e.g., "file2" before "file10").

## Build Commands

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>` 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
Expand Down
53 changes: 53 additions & 0 deletions Sorting.Test/NaturalStringComparerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace ktsu.Sorting.Tests;

using System.Collections;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
Expand Down Expand Up @@ -438,4 +439,56 @@

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);

Check warning on line 450 in Sorting.Test/NaturalStringComparerTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Sorting&issues=AaGaVjmLlnoPP20lZDEx&open=AaGaVjmLlnoPP20lZDEx&pullRequest=75
}

[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);

Check warning on line 474 in Sorting.Test/NaturalStringComparerTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Sorting&issues=AaGaVjmLlnoPP20lZDEy&open=AaGaVjmLlnoPP20lZDEy&pullRequest=75
}

[TestMethod]
public void NonGenericComparer_ThrowsForArgumentsThatAreNotStrings()
{
IComparer nonGeneric = NonGeneric();

Assert.ThrowsExactly<ArgumentException>(() => nonGeneric.Compare(5, "file"));
Assert.ThrowsExactly<ArgumentException>(() => 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;
}
}
30 changes: 29 additions & 1 deletion Sorting/NaturalStringComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace ktsu.Sorting;

using System.Collections;
using System.Globalization;

/// <summary>
Expand All @@ -12,8 +13,10 @@
/// 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 <see cref="IComparer"/>, so it can be handed to APIs
/// that accept only that, such as <see cref="ArrayList.Sort(IComparer)"/>.
/// </remarks>
public partial class NaturalStringComparer : IComparer<string?>
public partial class NaturalStringComparer : IComparer<string?>, IComparer
{
/// <summary>
/// Compares two strings using natural sorting, where embedded numbers are compared as numeric values.
Expand All @@ -26,7 +29,7 @@
/// digit script are ordered next to each other, ordinally.
/// Greater than zero if x is greater than y.
/// </returns>
public int Compare(string? x, string? y)

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.

Check warning on line 32 in Sorting/NaturalStringComparer.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 22 to the 15 allowed.
{
if (x == null && y == null)
{
Expand Down Expand Up @@ -103,6 +106,31 @@
return string.CompareOrdinal(x, y);
}

/// <summary>
/// Compares two objects that are strings or <see langword="null"/>, exactly as
/// <see cref="Compare(string, string)"/> does.
/// </summary>
/// <param name="x">First object to compare.</param>
/// <param name="y">Second object to compare.</param>
/// <returns>The result of <see cref="Compare(string, string)"/> on the two strings.</returns>
/// <exception cref="ArgumentException">
/// An argument is neither a string nor <see langword="null"/>.
/// </exception>
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);
}

/// <summary>
/// Reports whether the code point at <paramref name="index"/> is a Unicode decimal digit
/// (category <c>Nd</c>), reading a surrogate pair as the one code point it encodes.
Expand Down
Loading