diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4767f7a..23af429 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,5 @@ -# build.yml v1.4 +# build.yml v1.5 +# 1.5 - Use prerelease ProjProps. # 1.4 - Avoid set-env. # 1.3 - Include tag workflow in this file. # 1.2 - Define DOTNET_SKIP_FIRST_TIME_EXPERIENCE/NUGET_XMLDOC_MODE. @@ -52,8 +53,8 @@ jobs: - name: Get current version run: | - dotnet tool install --global Nito.ProjProps - echo "NEWTAG=v$(projprops --name version --output-format SingleValueOnly --project src --project-search)" >> $GITHUB_ENV + dotnet tool install --global Nito.ProjProps --version 2.0.0-pre01 + echo "NEWTAG=v$(projprops --name version --project src)" >> $GITHUB_ENV - name: Build run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index a008789..08e3ace 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [6.3.0] - +### Added +- More nullable reference type support. +- Natural string comparers that compare numeric substrings as numeric values. + - Note: `GetHashCode` for natural string comparers will allocate memory on all platforms older than .NET Core 3.0, including all versions of .NET Framework. + - Note: `GetHashCode` will cause extra collisions when used with invariant cultures on platforms that only support .NET Standard 1.x (except .NET Framework). This means Xamarin.Android 7.1, Xamarin.iOS 10.8, and Xamarin.Mac 3.0 will have pathologically inefficient `GetHashCode` implementations for natural string comparers using an invariant culture. Xamarin.Android 8.0+, Xamarin.iOS 10.14+, and Xamarin.Mac 3.8+ will work properly. + ## [6.2.2] - 2021-09-25 ### Changed - Bump Rx and Ix dependencies. diff --git a/future/StringSpanComparer.cs b/future/StringSpanComparer.cs new file mode 100644 index 0000000..0ea0c09 --- /dev/null +++ b/future/StringSpanComparer.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; + +namespace Nito.Comparers.Util +{ + /// + /// A type that can compare strings as well as read-only spans of characters. + /// + public sealed class StringSpanComparer : IFullComparer + { + private readonly CompareInfo _compareInfo; + private readonly CompareOptions _options; + + /// + /// Creates a new instance using the specified compare info and options. + /// + public StringSpanComparer(CompareInfo compareInfo, CompareOptions options) + { + _compareInfo = compareInfo; + _options = options; + } + + /// + /// Creates a new instance using the specified string comparison. + /// + public StringSpanComparer(StringComparison comparison) + : this(GetCompareInfo(comparison), GetCompareOptions(comparison)) + { + } + + private static CompareInfo GetCompareInfo(StringComparison comparison) + { + return comparison switch + { + StringComparison.CurrentCulture => CultureInfo.CurrentCulture.CompareInfo, + StringComparison.CurrentCultureIgnoreCase => CultureInfo.CurrentCulture.CompareInfo, + (StringComparison)2 /* InvariantCulture */ => CultureInfo.InvariantCulture.CompareInfo, + (StringComparison)3 /* InvariantCultureIgnoreCase */ => CultureInfo.InvariantCulture.CompareInfo, + StringComparison.Ordinal => CultureInfo.InvariantCulture.CompareInfo, + StringComparison.OrdinalIgnoreCase => CultureInfo.InvariantCulture.CompareInfo, + _ => throw new ArgumentException($"The string comparison type {comparison} is not supported.", nameof(comparison)), + }; + } + + private static CompareOptions GetCompareOptions(StringComparison comparison) + { + return comparison switch + { + StringComparison.CurrentCulture => CompareOptions.None, + StringComparison.CurrentCultureIgnoreCase => CompareOptions.IgnoreCase, + (StringComparison)2 /* InvariantCulture */ => CompareOptions.None, + (StringComparison)3 /* InvariantCultureIgnoreCase */ => CompareOptions.IgnoreCase, + StringComparison.Ordinal => CompareOptions.Ordinal, + StringComparison.OrdinalIgnoreCase => CompareOptions.OrdinalIgnoreCase, + _ => throw new ArgumentException($"The string comparison type {comparison} is not supported.", nameof(comparison)), + }; + } + + /// + /// Compares two read-only spans of characters as though they were strings. + /// + public int Compare(ReadOnlySpan x, ReadOnlySpan y) => _compareInfo.Compare(x, y, _options); + + /// + /// Determines whether two read-only spans of characters are equal, as though they were strings. + /// + public bool Equals(ReadOnlySpan x, ReadOnlySpan y) => Compare(x, y) == 0; + + /// + /// Gets a hash code for a read-only span of characters, as though it were a string. + /// + public int GetHashCode(ReadOnlySpan obj) => _compareInfo.GetHashCode(obj, _options); + + /// + public int Compare(string? x, string? y) + { + throw new NotImplementedException(); + } + + /// + public bool Equals(string? x, string? y) => EqualityComparerHelpers.ImplementEquals(x, y, false, DoEquals!); + + private bool DoEquals(string? x, string? y) => Equals(x == null ? default : x.AsSpan(), y == null ? default : y.AsSpan()); + + /// + public int GetHashCode(string? obj) => EqualityComparerHelpers.ImplementGetHashCode(obj, false, DoGetHashCode!); + + private int DoGetHashCode(string? obj) => GetHashCode(obj == null ? default : obj.AsSpan()); + + int IComparer.Compare(object? x, object? y) + { + throw new NotImplementedException(); + } + + bool IEqualityComparer.Equals(object? x, object? y) => EqualityComparerHelpers.ImplementEquals(x, y, false, DoEquals); + + int IEqualityComparer.GetHashCode(object? obj) => EqualityComparerHelpers.ImplementGetHashCode(obj, false, DoGetHashCode); + } +} diff --git a/src/Comparers.Ix/Comparers.Ix.csproj b/src/Comparers.Ix/Comparers.Ix.csproj index 559658b..f342d1b 100644 --- a/src/Comparers.Ix/Comparers.Ix.csproj +++ b/src/Comparers.Ix/Comparers.Ix.csproj @@ -2,7 +2,7 @@ This old package just forwards to Nito.Comparers.Ix. - netstandard1.0;netstandard2.0;net461 + netstandard1.0;netstandard2.0;net45;net461 comparer;equalitycomparer;icomparable;iequatable true diff --git a/src/Comparers.Rx/Comparers.Rx.csproj b/src/Comparers.Rx/Comparers.Rx.csproj index 8b6cc67..dfa1606 100644 --- a/src/Comparers.Rx/Comparers.Rx.csproj +++ b/src/Comparers.Rx/Comparers.Rx.csproj @@ -2,7 +2,7 @@ This old package just forwards to Nito.Comparers.Rx. - netstandard1.0;netstandard2.0;net461 + netstandard1.0;netstandard2.0;net45;net461 comparer;equalitycomparer;icomparable;iequatable true diff --git a/src/Comparers/Comparers.csproj b/src/Comparers/Comparers.csproj index df99963..f090ddd 100644 --- a/src/Comparers/Comparers.csproj +++ b/src/Comparers/Comparers.csproj @@ -1,8 +1,8 @@ - + This old package just forwards to Nito.Comparers. - netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 + netstandard1.0;netstandard1.3;netstandard2.0;net45;net461;net472;netcoreapp2.0 comparer;equalitycomparer;icomparable;iequatable true diff --git a/src/Nito.Comparers.Core/Advanced/AdvancedComparerBase.cs b/src/Nito.Comparers.Core/Advanced/AdvancedComparerBase.cs index 0e5c8a0..a32b8a8 100644 --- a/src/Nito.Comparers.Core/Advanced/AdvancedComparerBase.cs +++ b/src/Nito.Comparers.Core/Advanced/AdvancedComparerBase.cs @@ -42,7 +42,7 @@ protected AdvancedComparerBase(bool specialNullHandling) /// /// The object for which to return a hash code. May be null if is true. /// A hash code for the specified object. - protected abstract int DoGetHashCode(T obj); + protected abstract int DoGetHashCode(T? obj); /// /// Compares two objects and returns a value less than 0 if is less than , 0 if is equal to , or greater than 0 if is greater than . @@ -50,22 +50,22 @@ protected AdvancedComparerBase(bool specialNullHandling) /// The first object to compare. May be null if is true. /// The second object to compare. May be null if is true. /// A value less than 0 if is less than , 0 if is equal to , or greater than 0 if is greater than . - protected abstract int DoCompare(T x, T y); + protected abstract int DoCompare(T? x, T? y); /// - public int Compare(T x, T y) => ((IComparer)_implementation).Compare(x, y); + public int Compare(T? x, T? y) => ((IComparer)_implementation).Compare(x!, y!); /// - public bool Equals(T x, T y) => ((IEqualityComparer)_implementation).Equals(x, y); + public bool Equals(T? x, T? y) => ((IEqualityComparer)_implementation).Equals(x!, y!); /// - public int GetHashCode(T obj) => ((IEqualityComparer)_implementation).GetHashCode(obj); + public int GetHashCode(T? obj) => ((IEqualityComparer)_implementation).GetHashCode(obj!); - int IComparer.Compare(object x, object y) => ((IComparer)_implementation).Compare(x, y); + int IComparer.Compare(object? x, object? y) => ((IComparer)_implementation).Compare(x, y); - bool IEqualityComparer.Equals(object x, object y) => ((IEqualityComparer)_implementation).Equals(x, y); + bool IEqualityComparer.Equals(object? x, object? y) => ((IEqualityComparer)_implementation).Equals(x, y); - int IEqualityComparer.GetHashCode(object obj) => ((IEqualityComparer)_implementation).GetHashCode(obj); + int IEqualityComparer.GetHashCode(object? obj) => ((IEqualityComparer)_implementation).GetHashCode(obj!); private sealed class Implementation : ComparerBase { @@ -79,9 +79,9 @@ public Implementation(bool specialNullHandling, AdvancedComparerBase parent) public bool SpecialNullHandlingValue => SpecialNullHandling; - protected override int DoGetHashCode(T obj) => _parent.DoGetHashCode(obj); + protected override int DoGetHashCode(T? obj) => _parent.DoGetHashCode(obj); - protected override int DoCompare(T x, T y) => _parent.DoCompare(x, y); + protected override int DoCompare(T? x, T? y) => _parent.DoCompare(x, y); } } } diff --git a/src/Nito.Comparers.Core/Advanced/AdvancedEqualityComparerBase.cs b/src/Nito.Comparers.Core/Advanced/AdvancedEqualityComparerBase.cs index 015e3de..7dcfd95 100644 --- a/src/Nito.Comparers.Core/Advanced/AdvancedEqualityComparerBase.cs +++ b/src/Nito.Comparers.Core/Advanced/AdvancedEqualityComparerBase.cs @@ -42,7 +42,7 @@ protected AdvancedEqualityComparerBase(bool specialNullHandling) /// /// The object for which to return a hash code. May be null if is true. /// A hash code for the specified object. - protected abstract int DoGetHashCode(T obj); + protected abstract int DoGetHashCode(T? obj); /// /// Compares two objects and returns true if they are equal and false if they are not equal. @@ -50,17 +50,17 @@ protected AdvancedEqualityComparerBase(bool specialNullHandling) /// The first object to compare. May be null if is true. /// The second object to compare. May be null if is true. /// true if is equal to ; otherwise, false. - protected abstract bool DoEquals(T x, T y); + protected abstract bool DoEquals(T? x, T? y); /// - public bool Equals(T x, T y) => ((IEqualityComparer)_implementation).Equals(x, y); + public bool Equals(T? x, T? y) => ((IEqualityComparer)_implementation).Equals(x!, y!); /// - public int GetHashCode(T obj) => ((IEqualityComparer)_implementation).GetHashCode(obj); + public int GetHashCode(T? obj) => ((IEqualityComparer)_implementation).GetHashCode(obj!); - bool IEqualityComparer.Equals(object x, object y) => ((IEqualityComparer)_implementation).Equals(x, y); + bool IEqualityComparer.Equals(object? x, object? y) => ((IEqualityComparer)_implementation).Equals(x, y); - int IEqualityComparer.GetHashCode(object obj) => ((IEqualityComparer)_implementation).GetHashCode(obj); + int IEqualityComparer.GetHashCode(object? obj) => ((IEqualityComparer)_implementation).GetHashCode(obj!); private sealed class Implementation : EqualityComparerBase { @@ -74,9 +74,9 @@ public Implementation(bool specialNullHandling, AdvancedEqualityComparerBase public bool SpecialNullHandlingValue => SpecialNullHandling; - protected override int DoGetHashCode(T obj) => _parent.DoGetHashCode(obj); + protected override int DoGetHashCode(T? obj) => _parent.DoGetHashCode(obj); - protected override bool DoEquals(T x, T y) => _parent.DoEquals(x, y); + protected override bool DoEquals(T? x, T? y) => _parent.DoEquals(x, y); } } } diff --git a/src/Nito.Comparers.Core/ComparableBase.cs b/src/Nito.Comparers.Core/ComparableBase.cs index e694feb..c370455 100644 --- a/src/Nito.Comparers.Core/ComparableBase.cs +++ b/src/Nito.Comparers.Core/ComparableBase.cs @@ -36,7 +36,7 @@ public abstract class ComparableBase : IEquatable, IComparable, IComparabl /// /// The object to compare with this instance. May be null. /// A value indicating whether this instance is equal to the specified object. - public bool Equals(T other) => ComparableImplementations.ImplementEquals(DefaultComparer, (T)this, other!); + public bool Equals(T? other) => ComparableImplementations.ImplementEquals(DefaultComparer, (T)this, other!); /// /// Returns a value indicating the relative order of this instance and the specified object: a negative value if this instance is less than the specified object; zero if this instance is equal to the specified object; and a positive value if this instance is greater than the specified object. @@ -50,6 +50,6 @@ public abstract class ComparableBase : IEquatable, IComparable, IComparabl /// /// The object to compare with this instance. May be null. /// A value indicating the relative order of this instance and the specified object: a negative value if this instance is less than the specified object; zero if this instance is equal to the specified object; and a positive value if this instance is greater than the specified object. - public int CompareTo(T other) => ComparableImplementations.ImplementCompareTo(DefaultComparer, (T)this, other!); + public int CompareTo(T? other) => ComparableImplementations.ImplementCompareTo(DefaultComparer, (T)this, other!); } } diff --git a/src/Nito.Comparers.Core/ComparerBuilderFor.cs b/src/Nito.Comparers.Core/ComparerBuilderFor.cs index 51f01b6..ebd0e5c 100644 --- a/src/Nito.Comparers.Core/ComparerBuilderFor.cs +++ b/src/Nito.Comparers.Core/ComparerBuilderFor.cs @@ -3,6 +3,8 @@ using System.ComponentModel; using Nito.Comparers.Util; +#pragma warning disable IDE0060 + namespace Nito.Comparers { /// @@ -35,6 +37,13 @@ public static class ComparerBuilderForExtensions /// public static IFullComparer Default(this ComparerBuilderFor @this) => (IFullComparer)ComparerHelpers.NormalizeDefault(null); + /// + /// Gets a natural string comparer, which treats numeric sequences (0-9) as numeric. + /// + /// The comparer builder. + /// The comparison type used to compare the text segments of the string (not used for numeric segments). + public static IFullComparer Natural(this ComparerBuilderFor @this, StringComparison comparison) => new NaturalStringComparer(comparison); + /// /// Creates a key comparer. /// @@ -46,7 +55,7 @@ public static class ComparerBuilderForExtensions /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. /// A value indicating whether the sorting is done in descending order. If false (the default), then the sort is in ascending order. /// A key comparer. - public static IFullComparer OrderBy(this ComparerBuilderFor @this, Func selector, Func, IComparer> comparerFactory, bool specialNullHandling = false, bool descending = false) + public static IFullComparer OrderBy(this ComparerBuilderFor @this, Func selector, Func, IComparer> comparerFactory, bool specialNullHandling = false, bool descending = false) { _ = comparerFactory ?? throw new ArgumentNullException(nameof(comparerFactory)); var comparer = comparerFactory(ComparerBuilder.For()); @@ -64,7 +73,7 @@ public static IFullComparer OrderBy(this ComparerBuilderFor @this /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. /// A value indicating whether the sorting is done in descending order. If false (the default), then the sort is in ascending order. /// A key comparer. - public static IFullComparer OrderBy(this ComparerBuilderFor @this, Func selector, IComparer? keyComparer = null, bool specialNullHandling = false, bool descending = false) + public static IFullComparer OrderBy(this ComparerBuilderFor @this, Func selector, IComparer? keyComparer = null, bool specialNullHandling = false, bool descending = false) { var selectComparer = new SelectComparer(selector, keyComparer, specialNullHandling); return descending ? selectComparer.Reverse() : selectComparer; diff --git a/src/Nito.Comparers.Core/ComparerExtensions.cs b/src/Nito.Comparers.Core/ComparerExtensions.cs index f819cd2..36e6b8f 100644 --- a/src/Nito.Comparers.Core/ComparerExtensions.cs +++ b/src/Nito.Comparers.Core/ComparerExtensions.cs @@ -39,7 +39,7 @@ public static IFullComparer ThenBy(this IComparer? source, IComparer /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. /// A value indicating whether the sorting is done in descending order. If false (the default), then the sort is in ascending order. /// A comparer that uses a key comparer if the source comparer determines the objects are equal. - public static IFullComparer ThenBy(this IComparer? source, Func selector, Func, IComparer> comparerFactory, bool specialNullHandling = false, bool descending = false) + public static IFullComparer ThenBy(this IComparer? source, Func selector, Func, IComparer> comparerFactory, bool specialNullHandling = false, bool descending = false) { _ = comparerFactory ?? throw new ArgumentNullException(nameof(comparerFactory)); var comparer = comparerFactory(ComparerBuilder.For()); @@ -57,7 +57,7 @@ public static IFullComparer ThenBy(this IComparer? source, FuncA value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. /// A value indicating whether the sorting is done in descending order. If false (the default), then the sort is in ascending order. /// A comparer that uses a key comparer if the source comparer determines the objects are equal. - public static IFullComparer ThenBy(this IComparer? source, Func selector, IComparer? keyComparer = null, bool specialNullHandling = false, bool descending = false) + public static IFullComparer ThenBy(this IComparer? source, Func selector, IComparer? keyComparer = null, bool specialNullHandling = false, bool descending = false) { var selectComparer = new SelectComparer(selector, keyComparer, specialNullHandling); return source.ThenBy(selectComparer, descending); diff --git a/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs b/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs index 3da81a6..792c592 100644 --- a/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs +++ b/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs @@ -3,6 +3,9 @@ using System.ComponentModel; using Nito.Comparers.Util; +#pragma warning disable IDE0079 +#pragma warning disable IDE0060 + namespace Nito.Comparers { /// @@ -35,6 +38,13 @@ public static class EqualityComparerBuilderForExtensions /// public static IFullEqualityComparer Default(this EqualityComparerBuilderFor @this) => (IFullEqualityComparer)EqualityComparerHelpers.NormalizeDefault(null); + /// + /// Gets a natural string equality comparer, which treats numeric sequences (0-9) as numeric. + /// + /// The equality comparer builder. + /// The comparison type used to compare the text segments of the string (not used for numeric segments). + public static IFullEqualityComparer Natural(this EqualityComparerBuilderFor @this, StringComparison comparison) => new NaturalStringComparer(comparison); + /// /// Gets the reference equality comparer for this type. /// @@ -54,7 +64,7 @@ public static IFullEqualityComparer Reference(this EqualityComparerBuilder /// The definition of the key comparer. May not be null. /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. /// A key comparer. - public static IFullEqualityComparer EquateBy(this EqualityComparerBuilderFor @this, Func selector, Func, IEqualityComparer> comparerFactory, bool specialNullHandling = false) + public static IFullEqualityComparer EquateBy(this EqualityComparerBuilderFor @this, Func selector, Func, IEqualityComparer> comparerFactory, bool specialNullHandling = false) { _ = comparerFactory ?? throw new ArgumentNullException(nameof(comparerFactory)); var comparer = comparerFactory(EqualityComparerBuilder.For()); @@ -71,7 +81,7 @@ public static IFullEqualityComparer EquateBy(this EqualityComparerBu /// The key comparer. Defaults to null. If this is null, the default comparer is used. /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. /// A key comparer. - public static IFullEqualityComparer EquateBy(this EqualityComparerBuilderFor @this, Func selector, IEqualityComparer? keyComparer = null, bool specialNullHandling = false) + public static IFullEqualityComparer EquateBy(this EqualityComparerBuilderFor @this, Func selector, IEqualityComparer? keyComparer = null, bool specialNullHandling = false) { return new SelectEqualityComparer(selector, keyComparer, specialNullHandling); } diff --git a/src/Nito.Comparers.Core/EqualityComparerExtensions.cs b/src/Nito.Comparers.Core/EqualityComparerExtensions.cs index d2f295a..580848b 100644 --- a/src/Nito.Comparers.Core/EqualityComparerExtensions.cs +++ b/src/Nito.Comparers.Core/EqualityComparerExtensions.cs @@ -29,7 +29,7 @@ public static IFullEqualityComparer ThenEquateBy(this IEqualityComparer /// The definition of the key comparer. May not be null. /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. /// A comparer that uses a key comparer if the source comparer determines the objects are equal. - public static IFullEqualityComparer ThenEquateBy(this IEqualityComparer? source, Func selector, Func, IEqualityComparer> comparerFactory, bool specialNullHandling = false) + public static IFullEqualityComparer ThenEquateBy(this IEqualityComparer? source, Func selector, Func, IEqualityComparer> comparerFactory, bool specialNullHandling = false) { _ = comparerFactory ?? throw new ArgumentNullException(nameof(comparerFactory)); var comparer = comparerFactory(EqualityComparerBuilder.For()); @@ -46,7 +46,7 @@ public static IFullEqualityComparer ThenEquateBy(this IEqualityCompa /// The key comparer. Defaults to null. If this is null, the default comparer is used. /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. /// A comparer that uses a key comparer if the source comparer determines the objects are equal. - public static IFullEqualityComparer ThenEquateBy(this IEqualityComparer? source, Func selector, IEqualityComparer? keyComparer = null, bool specialNullHandling = false) + public static IFullEqualityComparer ThenEquateBy(this IEqualityComparer? source, Func selector, IEqualityComparer? keyComparer = null, bool specialNullHandling = false) { var selectComparer = new SelectEqualityComparer(selector, keyComparer, specialNullHandling); return source.ThenEquateBy(selectComparer); diff --git a/src/Nito.Comparers.Core/EquatableBase.cs b/src/Nito.Comparers.Core/EquatableBase.cs index 2372d1f..62b2d05 100644 --- a/src/Nito.Comparers.Core/EquatableBase.cs +++ b/src/Nito.Comparers.Core/EquatableBase.cs @@ -36,6 +36,6 @@ public abstract class EquatableBase : IEquatable where T : EquatableBase /// The object to compare with this instance. May be null. /// A value indicating whether this instance is equal to the specified object. - public bool Equals(T other) => ComparableImplementations.ImplementEquals(DefaultComparer, (T)this, other!); + public bool Equals(T? other) => ComparableImplementations.ImplementEquals(DefaultComparer, (T)this, other); } } diff --git a/src/Nito.Comparers.Core/Fixes/ExplicitGetHashCodeComparer.cs b/src/Nito.Comparers.Core/Fixes/ExplicitGetHashCodeComparer.cs index f56f450..97bd966 100644 --- a/src/Nito.Comparers.Core/Fixes/ExplicitGetHashCodeComparer.cs +++ b/src/Nito.Comparers.Core/Fixes/ExplicitGetHashCodeComparer.cs @@ -15,16 +15,16 @@ internal sealed class ExplicitGetHashCodeComparer : SourceComparerBase /// /// The source comparer. If this is null, the default comparer is used. /// The GetHashCode implementation to use. If this is null, this type will attempt to find GetHashCode on ; if none is found, throws an exception. - public ExplicitGetHashCodeComparer(IComparer? source, Func? getHashCode) + public ExplicitGetHashCodeComparer(IComparer? source, Func? getHashCode) : base(source, getHashCode, false) { } /// - protected override int DoGetHashCode(T obj) => SourceGetHashCode(obj); + protected override int DoGetHashCode(T? obj) => SourceGetHashCode(obj); /// - protected override int DoCompare(T x, T y) => Source.Compare(x, y); + protected override int DoCompare(T? x, T? y) => Source.Compare(x!, y!); /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. diff --git a/src/Nito.Comparers.Core/Fixes/FixComparerExtensions.cs b/src/Nito.Comparers.Core/Fixes/FixComparerExtensions.cs index b817ada..6e4464c 100644 --- a/src/Nito.Comparers.Core/Fixes/FixComparerExtensions.cs +++ b/src/Nito.Comparers.Core/Fixes/FixComparerExtensions.cs @@ -23,7 +23,7 @@ public static IFullComparer WithStandardNullHandling(this IComparer? so /// The type of objects being compared. /// The source comparer. If this is null, the default comparer is used. /// The GetHashCode implementation. - public static IFullComparer WithGetHashCode(this IComparer? source, Func getHashCode) => + public static IFullComparer WithGetHashCode(this IComparer? source, Func getHashCode) => new ExplicitGetHashCodeComparer(source, getHashCode); /// diff --git a/src/Nito.Comparers.Core/Fixes/StandardNullHandlingComparer.cs b/src/Nito.Comparers.Core/Fixes/StandardNullHandlingComparer.cs index 36bb96d..e229702 100644 --- a/src/Nito.Comparers.Core/Fixes/StandardNullHandlingComparer.cs +++ b/src/Nito.Comparers.Core/Fixes/StandardNullHandlingComparer.cs @@ -20,10 +20,10 @@ public StandardNullHandlingComparer(IComparer? source) } /// - protected override int DoCompare(T x, T y) => Source.Compare(x, y); + protected override int DoCompare(T? x, T? y) => Source.Compare(x!, y!); /// - protected override int DoGetHashCode(T obj) => SourceGetHashCode(obj); + protected override int DoGetHashCode(T? obj) => SourceGetHashCode(obj); /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. diff --git a/src/Nito.Comparers.Core/Fixes/StandardNullHandlingEqualityComparer.cs b/src/Nito.Comparers.Core/Fixes/StandardNullHandlingEqualityComparer.cs index 8101f81..6e8e7a0 100644 --- a/src/Nito.Comparers.Core/Fixes/StandardNullHandlingEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Fixes/StandardNullHandlingEqualityComparer.cs @@ -21,10 +21,10 @@ public StandardNullHandlingEqualityComparer(IEqualityComparer? source) } /// - protected override int DoGetHashCode(T obj) => Source.GetHashCode(obj!); + protected override int DoGetHashCode(T? obj) => Source.GetHashCode(obj!); /// - protected override bool DoEquals(T x, T y) => Source.Equals(x, y); + protected override bool DoEquals(T? x, T? y) => Source.Equals(x!, y!); /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. diff --git a/src/Nito.Comparers.Core/Internals/IStringSplitter.cs b/src/Nito.Comparers.Core/Internals/IStringSplitter.cs new file mode 100644 index 0000000..181ee11 --- /dev/null +++ b/src/Nito.Comparers.Core/Internals/IStringSplitter.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Nito.Comparers.Internals +{ + /// + /// + /// + public interface IStringSplitter + { + /// + /// + /// + /// + /// + /// + /// + /// + void MoveNext(string source, ref int offset, out int length, out bool isNumeric); + } +} diff --git a/src/Nito.Comparers.Core/Internals/ISubstringComparer.cs b/src/Nito.Comparers.Core/Internals/ISubstringComparer.cs new file mode 100644 index 0000000..2627659 --- /dev/null +++ b/src/Nito.Comparers.Core/Internals/ISubstringComparer.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Nito.Comparers.Internals +{ + public interface ISubstringComparer + { + int GetHashCode(string source, int offset, int length); + int Compare(string stringA, int offsetA, int lengthA, string stringB, int offsetB, int lengthB); + } +} diff --git a/src/Nito.Comparers.Core/Internals/Murmur3Hash.cs b/src/Nito.Comparers.Core/Internals/Murmur3Hash.cs index 87f844b..626c9c9 100644 --- a/src/Nito.Comparers.Core/Internals/Murmur3Hash.cs +++ b/src/Nito.Comparers.Core/Internals/Murmur3Hash.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Text; -#if !NET461 && !NETCOREAPP2_0 && !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NETSTANDARD2_1 +#if !NET45 && !NET461 && !NETCOREAPP2_0 && !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NETSTANDARD2_1 using static System.Numerics.BitOperations; #endif @@ -69,7 +69,7 @@ public void Combine(int data) } } -#if NET461 || NETCOREAPP2_0 || NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 +#if NET45 || NET461 || NETCOREAPP2_0 || NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 private static uint RotateLeft(uint value, int bits) => (value << bits) | (value >> (32 - bits)); #endif } diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs new file mode 100644 index 0000000..cdbb993 --- /dev/null +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +#pragma warning disable IDE0079 +#pragma warning disable IDE0057 + +namespace Nito.Comparers.Internals +{ + /// + /// Implements "natural" string comparison. + /// + public static class NaturalStringComparison + { + private static readonly char[] Digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + private static Func OrdinalStringGetHashCode = GetSubstringGetHashCode(StringComparison.Ordinal); + + /// + /// Gets a hash code for the specified string, using the specified string comparison for the text segments. + /// + /// The string to calculate the hash value of. May not be null. + /// The string delegate used to get the hash code of the text segments (not used for numeric segments). + public static int GetHashCode(string obj, Func substringGetHashCode) + { + _ = obj ?? throw new ArgumentNullException(nameof(obj)); + _ = substringGetHashCode ?? throw new ArgumentNullException(nameof(substringGetHashCode)); + + var parser = new SegmentParser(obj); + var result = Murmur3Hash.Create(); + while (!parser.IsDone) + { + parser.ParseNext(); + + // Note that leading zeros have been stripped from the range [start, end), so an ordinal comparison is sufficient to detect numeric equality. + var segmentGetHashCode = parser.IsNumeric ? OrdinalStringGetHashCode : substringGetHashCode; + var segmentHashCode = segmentGetHashCode(parser.Source, parser.Start, parser.Length); + + result.Combine(segmentHashCode); + } + + return result.HashCode; + } + + /// + /// Returns a delegate that performs a substring hash code using the specified comparision type. + /// + /// The comparison type used by the returned delegate. + public static Func GetSubstringGetHashCode(StringComparison stringComparison) + { +#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET45 || NET461 + var comparer = TryGetComparer(stringComparison); + if (comparer == null) + return (str, offset, length) => 0; + return (str, offset, length) => comparer.GetHashCode(str.Substring(offset, length)); +#else + return (str, offset, length) => string.GetHashCode(str.AsSpan(offset, length), stringComparison); +#endif + + // Implementations, in order of preference: + // 1) Use string.GetHashCode (.NET Core 3.0+). + // 2) Forward to StringComparer.FromComparison (.NET Core 2.0-2.2, .NET Standard 2.1+). + // 3) Use a switch statement that supports all StringComparison values (.NET Framework 4.5+, .NET Standard 2.0). + // 4) Use a switch statement that doesn't support the invariant culture (.NET Standard 1.0-1.6). + // By platform: + // .NET Core 3.0+ - This method is not defined. string.GetHashCode is used instead. + // .NET Core 2.0-2.2 - This method forwards to StringComparer.FromComparison. + // .NET Framework 4.5+ - This method is a switch statement, supporting all StringComparison values. + // .NET Standard 2.1+ - This method forwards to StringComparer.FromComparison. + // .NET Standard 2.0 - This method is a switch statement, supporting all StringComparison values. + // .NET Standard 1.0-1.6 - This method is a switch statement and does not support invariant cultures. + // This can be a problem for Xamarin.Android 7.1, Xamarin.iOS 10.8, and Xamarin.Mac 3.0, all of which have invariant comparers but do not support .NET Standard 2.0. + // This will cause tons of hash code collisions. + // The recommended solution on those platforms is "upgrade to a .NET Standard 2.0-compatible version". +#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET45 || NET461 +#if !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NET45 && !NET461 + static StringComparer? TryGetComparer(StringComparison comparison) + { + try + { + return StringComparer.FromComparison(comparison); + } + catch (ArgumentException) + { + return null; + } + } +#else + static StringComparer? TryGetComparer(StringComparison comparison) + { + return comparison switch + { + StringComparison.Ordinal => StringComparer.Ordinal, + StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase, + StringComparison.CurrentCulture => StringComparer.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, +#if !NETSTANDARD1_0 + StringComparison.InvariantCulture => StringComparer.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, +#endif + _ => null, + }; + } +#endif +#endif + } + + /// + /// Compares the specified strings, using the specified string comparison for the text segments. + /// + /// The first string to compare. May not be null. + /// The first string to compare. May not be null. + /// The string delegate used to compare the text segments (not used for numeric segments). + public static int Compare(string x, string y, Func substringCompare) + { + _ = x ?? throw new ArgumentNullException(nameof(x)); + _ = y ?? throw new ArgumentNullException(nameof(y)); + _ = substringCompare ?? throw new ArgumentNullException(nameof(substringCompare)); + + var xParser = new SegmentParser(x); + var yParser = new SegmentParser(y); + while (!xParser.IsDone && !yParser.IsDone) + { + xParser.ParseNext(); + yParser.ParseNext(); + if (xParser.IsNumeric && yParser.IsNumeric) + { + var xLength = xParser.Length; + var yLength = yParser.Length; + if (xLength < yLength) + return -1; + else if (xLength > yLength) + return 1; + var compareResult = string.Compare(xParser.Source, xParser.Start, yParser.Source, yParser.Start, xLength, StringComparison.Ordinal); + if (compareResult != 0) + return compareResult; + } + else if (!xParser.IsNumeric && !yParser.IsNumeric) + { + var xLength = xParser.Length; + var yLength = yParser.Length; + var compareResult = substringCompare(xParser.Source, xParser.Start, xLength, yParser.Source, yParser.Start, yLength); + if (compareResult != 0) + return compareResult; + } + else if (xParser.IsNumeric) + { + return -1; + } + else + { + return 1; + } + } + + if (xParser.IsDone && yParser.IsDone) + return 0; + + if (xParser.IsDone) + return -1; + return 1; + } + + /// + /// Returns a delegate that performs a substring comparison using the specified comparision type. + /// + /// The comparison type used by the returned delegate. + public static Func GetSubstringCompare(StringComparison stringComparison) + { + // Blatantly stolen from https://dogmamix.com/cms/blog/Finding-substrings + return stringComparison switch + { + StringComparison.CurrentCulture => (strA, indexA, lengthA, strB, indexB, lengthB) => CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.None), + StringComparison.CurrentCultureIgnoreCase => (strA, indexA, lengthA, strB, indexB, lengthB) => CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.IgnoreCase), + (StringComparison)2 /* InvariantCulture */ => (strA, indexA, lengthA, strB, indexB, lengthB) => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.None), + (StringComparison)3 /* InvariantCultureIgnoreCase */ => (strA, indexA, lengthA, strB, indexB, lengthB) => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.IgnoreCase), + StringComparison.Ordinal => (strA, indexA, lengthA, strB, indexB, lengthB) => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.Ordinal), + StringComparison.OrdinalIgnoreCase => (strA, indexA, lengthA, strB, indexB, lengthB) => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.OrdinalIgnoreCase), + _ => throw new ArgumentException($"The string comparison type {stringComparison} is not supported.", nameof(stringComparison)), + }; + } + + private sealed class DefaultSubstringComparer : ISubstringComparer + { + private readonly Func _getCultureInfo; + private readonly CompareOptions _compareOptions; + + public DefaultSubstringComparer(StringComparison comparison) + { + _getCultureInfo = comparison switch + { + StringComparison.CurrentCulture => () => CultureInfo.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase => () => CultureInfo.CurrentCulture, + _ => () => CultureInfo.InvariantCulture, + }; + } + + public int Compare(string stringA, int inclusiveStartA, int exclusiveEndA, string stringB, int inclusiveStartB, int exclusiveEndB) + { + throw new NotImplementedException(); + } + + public int GetHashCode(string source, int inclusiveStart, int exclusiveEnd) + { + throw new NotImplementedException(); + } + } + + private ref struct SegmentParser + { + public SegmentParser(string source) + { + Source = source; + Start = 0; + End = 0; + IsNumeric = false; + } + + public string Source { get; } + public int Start { get; private set; } + public int End { get; private set; } + public int Length => End - Start; + public bool IsDone => End == Source.Length; + public bool IsNumeric { get; private set; } + + public void ParseNext() + { + // Prerequisite: index < source.Length + + Start = End; + var index = Start; + IsNumeric = IsDigit(Source[index++]); + if (IsNumeric) + { + // Skip leading zeros, but keep one if that's the only digit. + if (Source[Start] == '0') + { + do + { + ++Start; + } while (Start < Source.Length && Source[Start] == '0'); + if (Start == Source.Length || !IsDigit(Source[Start])) + --Start; + index = Start + 1; + } + + while (index < Source.Length && IsDigit(Source[index])) + ++index; + End = index; + } + else + { + index = Source.IndexOfAny(Digits, index); + if (index == -1) + index = Source.Length; + End = index; + } + + static bool IsDigit(char ch) => ch >= '0' && ch <= '9'; + } + } + + private sealed class DefaultSplitter : IStringSplitter + { + public void MoveNext(string source, ref int offset, out int length, out bool isNumeric) + { + // Prerequisite: start < source.Length + + var index = offset; + isNumeric = IsDigit(source[index++]); + if (isNumeric) + { + // Skip leading zeros, but keep one if that's the only digit. + if (source[offset] == '0') + { + do + { + ++offset; + } while (offset < source.Length && source[offset] == '0'); + if (offset == source.Length || !IsDigit(source[offset])) + --offset; + index = offset + 1; + } + + while (index < source.Length && IsDigit(source[index])) + ++index; + length = index - offset; + } + else + { + index = source.IndexOfAny(Digits, index); + if (index == -1) + index = source.Length; + length = index - offset; + } + + static bool IsDigit(char ch) => ch >= '0' && ch <= '9'; + } + } + } +} diff --git a/src/Nito.Comparers.Core/Nito.Comparers.Core.csproj b/src/Nito.Comparers.Core/Nito.Comparers.Core.csproj index d8bf617..4523a18 100644 --- a/src/Nito.Comparers.Core/Nito.Comparers.Core.csproj +++ b/src/Nito.Comparers.Core/Nito.Comparers.Core.csproj @@ -1,7 +1,7 @@  The last comparison library you'll ever need! - netstandard1.0;netstandard2.0;net461 + netstandard1.0;netstandard2.0;netstandard2.1;net45;net461;netcoreapp2.0;netcoreapp3.0;netcoreapp5.0;net6.0 comparer;equalitycomparer;icomparable;iequatable Nito.Comparers diff --git a/src/Nito.Comparers.Core/Util/ComparableImplementations.cs b/src/Nito.Comparers.Core/Util/ComparableImplementations.cs index 99f68e4..08be19c 100644 --- a/src/Nito.Comparers.Core/Util/ComparableImplementations.cs +++ b/src/Nito.Comparers.Core/Util/ComparableImplementations.cs @@ -15,10 +15,10 @@ public static class ComparableImplementations /// The comparer. May not be null. /// The object doing the implementing. /// The other object. - public static int ImplementCompareTo(IComparer comparer, T @this, T other) where T : IComparable + public static int ImplementCompareTo(IComparer comparer, T? @this, T? other) where T : IComparable { _ = comparer ?? throw new ArgumentNullException(nameof(comparer)); - return comparer.Compare(@this, other); + return comparer.Compare(@this!, other!); } /// @@ -27,7 +27,7 @@ public static int ImplementCompareTo(IComparer comparer, T @this, T other) /// The comparer. May not be null. /// The object doing the implementing. /// The other object. - public static int ImplementCompareTo(System.Collections.IComparer comparer, IComparable @this, object? obj) + public static int ImplementCompareTo(System.Collections.IComparer comparer, IComparable? @this, object? obj) { _ = comparer ?? throw new ArgumentNullException(nameof(comparer)); return comparer.Compare(@this, obj); @@ -39,7 +39,7 @@ public static int ImplementCompareTo(System.Collections.IComparer comparer, ICom /// The type of objects being compared. /// The comparer. May not be null. /// The object doing the implementing. - public static int ImplementGetHashCode(IEqualityComparer equalityComparer, T @this) + public static int ImplementGetHashCode(IEqualityComparer equalityComparer, T? @this) { _ = equalityComparer ?? throw new ArgumentNullException(nameof(equalityComparer)); return equalityComparer.GetHashCode(@this!); @@ -52,10 +52,10 @@ public static int ImplementGetHashCode(IEqualityComparer equalityComparer, /// The comparer. May not be null. /// The object doing the implementing. /// The other object. - public static bool ImplementEquals(IEqualityComparer equalityComparer, T @this, T other) where T : IEquatable + public static bool ImplementEquals(IEqualityComparer equalityComparer, T? @this, T? other) where T : IEquatable { _ = equalityComparer ?? throw new ArgumentNullException(nameof(equalityComparer)); - return equalityComparer.Equals(@this, other); + return equalityComparer.Equals(@this!, other!); } /// @@ -77,10 +77,10 @@ public static bool ImplementEquals(System.Collections.IEqualityComparer equality /// The comparer. May not be null. /// A value of type or null. /// A value of type or null. - public static bool ImplementOpEquality(IEqualityComparer equalityComparer, T left, T right) + public static bool ImplementOpEquality(IEqualityComparer equalityComparer, T? left, T? right) { _ = equalityComparer ?? throw new ArgumentNullException(nameof(equalityComparer)); - return equalityComparer.Equals(left, right); + return equalityComparer.Equals(left!, right!); } /// @@ -90,10 +90,10 @@ public static bool ImplementOpEquality(IEqualityComparer equalityComparer, /// The comparer. May not be null. /// A value of type or null. /// A value of type or null. - public static bool ImplementOpInequality(IEqualityComparer equalityComparer, T left, T right) + public static bool ImplementOpInequality(IEqualityComparer equalityComparer, T? left, T? right) { _ = equalityComparer ?? throw new ArgumentNullException(nameof(equalityComparer)); - return !equalityComparer.Equals(left, right); + return !equalityComparer.Equals(left!, right!); } /// @@ -103,10 +103,10 @@ public static bool ImplementOpInequality(IEqualityComparer equalityCompare /// The comparer. May not be null. /// A value of type or null. /// A value of type or null. - public static bool ImplementOpLessThan(IComparer comparer, T left, T right) + public static bool ImplementOpLessThan(IComparer comparer, T? left, T? right) { _ = comparer ?? throw new ArgumentNullException(nameof(comparer)); - return comparer.Compare(left, right) < 0; + return comparer.Compare(left!, right!) < 0; } /// @@ -116,10 +116,10 @@ public static bool ImplementOpLessThan(IComparer comparer, T left, T right /// The comparer. May not be null. /// A value of type or null. /// A value of type or null. - public static bool ImplementOpGreaterThan(IComparer comparer, T left, T right) + public static bool ImplementOpGreaterThan(IComparer comparer, T? left, T? right) { _ = comparer ?? throw new ArgumentNullException(nameof(comparer)); - return comparer.Compare(left, right) > 0; + return comparer.Compare(left!, right!) > 0; } /// @@ -129,10 +129,10 @@ public static bool ImplementOpGreaterThan(IComparer comparer, T left, T ri /// The comparer. May not be null. /// A value of type or null. /// A value of type or null. - public static bool ImplementOpLessThanOrEqual(IComparer comparer, T left, T right) + public static bool ImplementOpLessThanOrEqual(IComparer comparer, T? left, T? right) { _ = comparer ?? throw new ArgumentNullException(nameof(comparer)); - return comparer.Compare(left, right) <= 0; + return comparer.Compare(left!, right!) <= 0; } /// @@ -142,10 +142,10 @@ public static bool ImplementOpLessThanOrEqual(IComparer comparer, T left, /// The comparer. May not be null. /// A value of type or null. /// A value of type or null. - public static bool ImplementOpGreaterThanOrEqual(IComparer comparer, T left, T right) + public static bool ImplementOpGreaterThanOrEqual(IComparer comparer, T? left, T? right) { _ = comparer ?? throw new ArgumentNullException(nameof(comparer)); - return comparer.Compare(left, right) >= 0; + return comparer.Compare(left!, right!) >= 0; } } } diff --git a/src/Nito.Comparers.Core/Util/ComparerBase.cs b/src/Nito.Comparers.Core/Util/ComparerBase.cs index f0fa003..464a25b 100644 --- a/src/Nito.Comparers.Core/Util/ComparerBase.cs +++ b/src/Nito.Comparers.Core/Util/ComparerBase.cs @@ -15,10 +15,10 @@ internal abstract class ComparerBase : EqualityComparerBase, IFullComparer /// The first object to compare. May be null if is true. /// The second object to compare. May be null if is true. /// A value less than 0 if is less than , 0 if is equal to , or greater than 0 if is greater than . - protected abstract int DoCompare(T x, T y); + protected abstract int DoCompare(T? x, T? y); /// - protected override bool DoEquals(T x, T y) => Compare(x, y) == 0; + protected override bool DoEquals(T? x, T? y) => Compare(x, y) == 0; /// /// Initializes a new instance of the class. @@ -56,11 +56,11 @@ int System.Collections.IComparer.Compare(object? x, object? y) } } - return DoCompare((T)x!, (T)y!); + return DoCompare((T?)x, (T?)y); } /// - public int Compare(T x, T y) + public int Compare(T? x, T? y) { if (!SpecialNullHandling) { @@ -76,7 +76,7 @@ public int Compare(T x, T y) } } - return DoCompare(x!, y!); + return DoCompare(x, y); } } } diff --git a/src/Nito.Comparers.Core/Util/ComparerHelpers.cs b/src/Nito.Comparers.Core/Util/ComparerHelpers.cs index dd3104b..931c2ce 100644 --- a/src/Nito.Comparers.Core/Util/ComparerHelpers.cs +++ b/src/Nito.Comparers.Core/Util/ComparerHelpers.cs @@ -16,10 +16,10 @@ internal static class ComparerHelpers /// /// The type of objects being compared. /// The comparer to use to calculate a hash code. May be null, but this method will throw an exception since null does not support hash codes. - public static Func ComparerGetHashCode(IComparer? comparer) + public static Func ComparerGetHashCode(IComparer? comparer) { if (comparer is IEqualityComparer equalityComparer) - return equalityComparer.GetHashCode; + return equalityComparer.GetHashCode!; if (comparer is IEqualityComparer objectEqualityComparer) return obj => objectEqualityComparer.GetHashCode(obj!); diff --git a/src/Nito.Comparers.Core/Util/CompoundComparer.cs b/src/Nito.Comparers.Core/Util/CompoundComparer.cs index 149bc50..2ca42a7 100644 --- a/src/Nito.Comparers.Core/Util/CompoundComparer.cs +++ b/src/Nito.Comparers.Core/Util/CompoundComparer.cs @@ -18,7 +18,7 @@ internal sealed class CompoundComparer : SourceComparerBase /// /// The GetHashCode implementation for the second comparer. /// - private readonly Func _secondSourceGetHashCode; + private readonly Func _secondSourceGetHashCode; /// /// Initializes a new instance of the class. @@ -33,7 +33,7 @@ public CompoundComparer(IComparer? source, IComparer? secondSource) } /// - protected override int DoGetHashCode(T obj) + protected override int DoGetHashCode(T? obj) { unchecked { @@ -44,12 +44,12 @@ protected override int DoGetHashCode(T obj) } /// - protected override int DoCompare(T x, T y) + protected override int DoCompare(T? x, T? y) { - var ret = Source.Compare(x, y); + var ret = Source.Compare(x!, y!); if (ret != 0) return ret; - return _secondSource.Compare(x, y); + return _secondSource.Compare(x!, y!); } /// diff --git a/src/Nito.Comparers.Core/Util/CompoundEqualityComparer.cs b/src/Nito.Comparers.Core/Util/CompoundEqualityComparer.cs index c982284..77a94ba 100644 --- a/src/Nito.Comparers.Core/Util/CompoundEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/CompoundEqualityComparer.cs @@ -26,7 +26,7 @@ public CompoundEqualityComparer(IEqualityComparer? source, IEqualityComparer< } /// - protected override int DoGetHashCode(T obj) + protected override int DoGetHashCode(T? obj) { unchecked { @@ -37,12 +37,12 @@ protected override int DoGetHashCode(T obj) } /// - protected override bool DoEquals(T x, T y) + protected override bool DoEquals(T? x, T? y) { - var ret = Source.Equals(x, y); + var ret = Source.Equals(x!, y!); if (!ret) return false; - return _secondSource.Equals(x, y); + return _secondSource.Equals(x!, y!); } /// diff --git a/src/Nito.Comparers.Core/Util/DefaultComparer.cs b/src/Nito.Comparers.Core/Util/DefaultComparer.cs index ef69ba9..2288f35 100644 --- a/src/Nito.Comparers.Core/Util/DefaultComparer.cs +++ b/src/Nito.Comparers.Core/Util/DefaultComparer.cs @@ -19,13 +19,13 @@ static DefaultComparer() } /// - protected override int DoGetHashCode(T obj) => EqualityComparer.Default.GetHashCode(obj!); + protected override int DoGetHashCode(T? obj) => EqualityComparer.Default.GetHashCode(obj!); /// - protected override bool DoEquals(T x, T y) => EqualityComparer.Default.Equals(x, y); + protected override bool DoEquals(T? x, T? y) => EqualityComparer.Default.Equals(x!, y!); /// - protected override int DoCompare(T x, T y) => Comparer.Default.Compare(x, y); + protected override int DoCompare(T? x, T? y) => Comparer.Default.Compare(x!, y!); /// /// Gets the default comparer for this type. diff --git a/src/Nito.Comparers.Core/Util/EqualityComparerBase.cs b/src/Nito.Comparers.Core/Util/EqualityComparerBase.cs index d4ef9b9..9590a26 100644 --- a/src/Nito.Comparers.Core/Util/EqualityComparerBase.cs +++ b/src/Nito.Comparers.Core/Util/EqualityComparerBase.cs @@ -19,7 +19,7 @@ internal abstract class EqualityComparerBase : IFullEqualityComparer /// /// The object for which to return a hash code. May be null if is true. /// A hash code for the specified object. - protected abstract int DoGetHashCode(T obj); + protected abstract int DoGetHashCode(T? obj); /// /// Compares two objects and returns true if they are equal and false if they are not equal. @@ -27,7 +27,7 @@ internal abstract class EqualityComparerBase : IFullEqualityComparer /// The first object to compare. May be null if is true. /// The second object to compare. May be null if is true. /// true if is equal to ; otherwise, false. - protected abstract bool DoEquals(T x, T y); + protected abstract bool DoEquals(T? x, T? y); /// /// Initializes a new instance of the class. @@ -41,65 +41,15 @@ protected EqualityComparerBase(bool specialNullHandling) } /// - bool System.Collections.IEqualityComparer.Equals(object? x, object? y) - { - // EqualityComparer.IEqualityComparer.Equals will throw in this situation, but int.Equals returns false. - var xValid = x is T || x == null; - var yValid = y is T || y == null; - if (!xValid || !yValid) - { - if (!xValid && !yValid) - throw new ArgumentException("Invalid types for equality comparison."); - return false; - } - - if (!SpecialNullHandling) - { - if (x == null || y == null) - return (x == null && y == null); - } - - return DoEquals((T)x!, (T)y!); - } + bool System.Collections.IEqualityComparer.Equals(object? x, object? y) => EqualityComparerHelpers.ImplementEquals(x, y, SpecialNullHandling, DoEquals); /// - int System.Collections.IEqualityComparer.GetHashCode(object? obj) - { - if (!SpecialNullHandling) - { - if (obj == null) - return 0; - } - - var objValid = obj is T || obj == null; - if (!objValid) - throw new ArgumentException("Invalid type for comparison."); - - return DoGetHashCode((T)obj!); - } + int System.Collections.IEqualityComparer.GetHashCode(object? obj) => EqualityComparerHelpers.ImplementGetHashCode(obj, SpecialNullHandling, DoGetHashCode); /// - public bool Equals(T x, T y) - { - if (!SpecialNullHandling) - { - if (x == null || y == null) - return (x == null && y == null); - } - - return DoEquals(x!, y!); - } + public bool Equals(T? x, T? y) => EqualityComparerHelpers.ImplementEquals(x, y, SpecialNullHandling, DoEquals); /// - public int GetHashCode(T obj) - { - if (!SpecialNullHandling) - { - if (obj == null) - return 0; - } - - return DoGetHashCode(obj!); - } + public int GetHashCode(T? obj) => EqualityComparerHelpers.ImplementGetHashCode(obj, SpecialNullHandling, DoGetHashCode); } } diff --git a/src/Nito.Comparers.Core/Util/EqualityComparerHelpers.cs b/src/Nito.Comparers.Core/Util/EqualityComparerHelpers.cs index 7d8a538..d1b0182 100644 --- a/src/Nito.Comparers.Core/Util/EqualityComparerHelpers.cs +++ b/src/Nito.Comparers.Core/Util/EqualityComparerHelpers.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -35,5 +37,63 @@ public static IEqualityComparer NormalizeDefault(IEqualityComparer? com var instance = constructor.Invoke(new object[] { null! }); return (IEqualityComparer)instance; } + + public static bool ImplementEquals(object? x, object? y, bool specialNullHandling, Func doEquals) + { + // EqualityComparer.IEqualityComparer.Equals will throw in this situation, but int.Equals returns false. + var xValid = x is T || x == null; + var yValid = y is T || y == null; + if (!xValid || !yValid) + { + if (!xValid && !yValid) + throw new ArgumentException("Invalid types for equality comparison."); + return false; + } + + if (!specialNullHandling) + { + if (x == null || y == null) + return (x == null && y == null); + } + + return doEquals((T?) x, (T?) y); + } + + public static bool ImplementEquals(T? x, T? y, bool specialNullHandling, Func doEquals) + { + if (!specialNullHandling) + { + if (x == null || y == null) + return (x == null && y == null); + } + + return doEquals(x, y); + } + + public static int ImplementGetHashCode(object? obj, bool specialNullHandling, Func doGetHashCode) + { + if (!specialNullHandling) + { + if (obj == null) + return 0; + } + + var objValid = obj is T || obj == null; + if (!objValid) + throw new ArgumentException("Invalid type for comparison."); + + return doGetHashCode((T?) obj); + } + + public static int ImplementGetHashCode(T? obj, bool specialNullHandling, Func doGetHashCode) + { + if (!specialNullHandling) + { + if (obj == null) + return 0; + } + + return doGetHashCode(obj); + } } } diff --git a/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs new file mode 100644 index 0000000..8e5ee3e --- /dev/null +++ b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs @@ -0,0 +1,32 @@ +using Nito.Comparers.Internals; +using System; + +namespace Nito.Comparers.Util +{ + /// + /// The natural string comparer. + /// + internal sealed class NaturalStringComparer : ComparerBase + { + private readonly Func _substringCompare; + private readonly Func _substringGetHashCode; + + public NaturalStringComparer(StringComparison comparison) + : base(false) + { + _substringCompare = NaturalStringComparison.GetSubstringCompare(comparison); + _substringGetHashCode = NaturalStringComparison.GetSubstringGetHashCode(comparison); + } + + /// + protected override int DoGetHashCode(string? obj) => NaturalStringComparison.GetHashCode(obj!, _substringGetHashCode); + + /// + protected override int DoCompare(string? x, string? y) => NaturalStringComparison.Compare(x!, y!, _substringCompare); + + /// + /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. + /// + public override string ToString() => "NaturalString"; + } +} diff --git a/src/Nito.Comparers.Core/Util/NullComparer.cs b/src/Nito.Comparers.Core/Util/NullComparer.cs index c80cd6c..7eadbf5 100644 --- a/src/Nito.Comparers.Core/Util/NullComparer.cs +++ b/src/Nito.Comparers.Core/Util/NullComparer.cs @@ -22,10 +22,10 @@ static NullComparer() public static NullComparer Instance { get; } = new NullComparer(); /// - protected override int DoGetHashCode(T obj) => -1421968373; + protected override int DoGetHashCode(T? obj) => -1421968373; /// - protected override int DoCompare(T x, T y) => 0; + protected override int DoCompare(T? x, T? y) => 0; /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. diff --git a/src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs index e26b940..0c5a610 100644 --- a/src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs @@ -25,10 +25,10 @@ static ReferenceEqualityComparer() public static ReferenceEqualityComparer Instance { get; } = new ReferenceEqualityComparer(); /// - protected override int DoGetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + protected override int DoGetHashCode(T? obj) => RuntimeHelpers.GetHashCode(obj!); /// - protected override bool DoEquals(T x, T y) => object.ReferenceEquals(x, y); + protected override bool DoEquals(T? x, T? y) => object.ReferenceEquals(x, y); /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. diff --git a/src/Nito.Comparers.Core/Util/ReverseComparer.cs b/src/Nito.Comparers.Core/Util/ReverseComparer.cs index 3b44f96..f2f9362 100644 --- a/src/Nito.Comparers.Core/Util/ReverseComparer.cs +++ b/src/Nito.Comparers.Core/Util/ReverseComparer.cs @@ -18,10 +18,10 @@ public ReverseComparer(IComparer? source) } /// - protected override int DoGetHashCode(T obj) => SourceGetHashCode(obj); + protected override int DoGetHashCode(T? obj) => SourceGetHashCode(obj); /// - protected override int DoCompare(T x, T y) => Source.Compare(y, x); + protected override int DoCompare(T? x, T? y) => Source.Compare(y!, x!); /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. diff --git a/src/Nito.Comparers.Core/Util/SelectComparer.cs b/src/Nito.Comparers.Core/Util/SelectComparer.cs index 78d8ced..0304147 100644 --- a/src/Nito.Comparers.Core/Util/SelectComparer.cs +++ b/src/Nito.Comparers.Core/Util/SelectComparer.cs @@ -13,7 +13,7 @@ internal sealed class SelectComparer : SourceComparerBase /// The key selector. /// - private readonly Func _selector; + private readonly Func _selector; /// /// Initializes a new instance of the class. @@ -21,17 +21,17 @@ internal sealed class SelectComparer : SourceComparerBaseThe key selector. May not be null. /// The source comparer. If this is null, the default comparer is used. /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. - public SelectComparer(Func selector, IComparer? source, bool specialNullHandling) + public SelectComparer(Func selector, IComparer? source, bool specialNullHandling) : base(source, null, specialNullHandling) { _selector = selector; } /// - protected override int DoGetHashCode(T obj) => SourceGetHashCode(_selector(obj)); + protected override int DoGetHashCode(T? obj) => SourceGetHashCode(_selector(obj)); /// - protected override int DoCompare(T x, T y) => Source.Compare(_selector(x), _selector(y)); + protected override int DoCompare(T? x, T? y) => Source.Compare(_selector(x)!, _selector(y)!); /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. diff --git a/src/Nito.Comparers.Core/Util/SelectEqualityComparer.cs b/src/Nito.Comparers.Core/Util/SelectEqualityComparer.cs index 4f9c472..b8f49fd 100644 --- a/src/Nito.Comparers.Core/Util/SelectEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/SelectEqualityComparer.cs @@ -13,7 +13,7 @@ internal sealed class SelectEqualityComparer : SourceEqualityCompare /// /// The key selector. /// - private readonly Func _selector; + private readonly Func _selector; /// /// Initializes a new instance of the class. @@ -21,17 +21,17 @@ internal sealed class SelectEqualityComparer : SourceEqualityCompare /// The key selector. May not be null. /// The source comparer. If this is null, the default comparer is used. /// A value indicating whether null values are passed to . If false, then null values are considered less than any non-null values and are not passed to . This value is ignored if is a non-nullable type. - public SelectEqualityComparer(Func selector, IEqualityComparer? source, bool specialNullHandling) + public SelectEqualityComparer(Func selector, IEqualityComparer? source, bool specialNullHandling) : base(source, specialNullHandling) { _selector = selector; } /// - protected override int DoGetHashCode(T obj) => Source.GetHashCode(_selector(obj)!); + protected override int DoGetHashCode(T? obj) => Source.GetHashCode(_selector(obj)!); /// - protected override bool DoEquals(T x, T y) => Source.Equals(_selector(x), _selector(y)); + protected override bool DoEquals(T? x, T? y) => Source.Equals(_selector(x)!, _selector(y)!); /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. diff --git a/src/Nito.Comparers.Core/Util/SequenceComparer.cs b/src/Nito.Comparers.Core/Util/SequenceComparer.cs index 4030e00..6aec643 100644 --- a/src/Nito.Comparers.Core/Util/SequenceComparer.cs +++ b/src/Nito.Comparers.Core/Util/SequenceComparer.cs @@ -19,36 +19,34 @@ public SequenceComparer(IComparer? source) } /// - protected override int DoGetHashCode(IEnumerable obj) + protected override int DoGetHashCode(IEnumerable? obj) { var ret = Murmur3Hash.Create(); - foreach (var item in obj) + foreach (var item in obj!) ret.Combine(SourceGetHashCode(item)); return ret.HashCode; } /// - protected override int DoCompare(IEnumerable x, IEnumerable y) + protected override int DoCompare(IEnumerable? x, IEnumerable? y) { - using (var xIter = x.GetEnumerator()) - using (var yIter = y.GetEnumerator()) + using var xIter = x!.GetEnumerator(); + using var yIter = y!.GetEnumerator(); + while (true) { - while (true) + if (!xIter.MoveNext()) { - if (!xIter.MoveNext()) - { - if (!yIter.MoveNext()) - return 0; - return -1; - } - if (!yIter.MoveNext()) - return 1; - - var ret = Source.Compare(xIter.Current, yIter.Current); - if (ret != 0) - return ret; + return 0; + return -1; } + + if (!yIter.MoveNext()) + return 1; + + var ret = Source.Compare(xIter.Current, yIter.Current); + if (ret != 0) + return ret; } } diff --git a/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs index 56ed126..a59b591 100644 --- a/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs @@ -19,21 +19,21 @@ public SequenceEqualityComparer(IEqualityComparer? source) } /// - protected override int DoGetHashCode(IEnumerable obj) + protected override int DoGetHashCode(IEnumerable? obj) { var ret = Murmur3Hash.Create(); - foreach (var item in obj) + foreach (var item in obj!) ret.Combine(Source.GetHashCode(item!)); return ret.HashCode; } /// - protected override bool DoEquals(IEnumerable x, IEnumerable y) + protected override bool DoEquals(IEnumerable? x, IEnumerable? y) { - var xCount = x.TryGetCount(); + var xCount = x!.TryGetCount(); if (xCount != null) { - var yCount = y.TryGetCount(); + var yCount = y!.TryGetCount(); if (yCount != null) { if (xCount.Value != yCount.Value) @@ -43,26 +43,24 @@ protected override bool DoEquals(IEnumerable x, IEnumerable y) } } - using (var xIter = x.GetEnumerator()) - using (var yIter = y.GetEnumerator()) + using var xIter = x!.GetEnumerator(); + using var yIter = y!.GetEnumerator(); + while (true) { - while (true) + if (!xIter.MoveNext()) { - if (!xIter.MoveNext()) - { - if (!yIter.MoveNext()) - return true; - return false; - } - if (!yIter.MoveNext()) - return false; - - var ret = Source.Equals(xIter.Current, yIter.Current); - if (!ret) - return false; + return true; + return false; } - } + + if (!yIter.MoveNext()) + return false; + + var ret = Source.Equals(xIter.Current, yIter.Current); + if (!ret) + return false; + } } /// diff --git a/src/Nito.Comparers.Core/Util/SourceComparerBase.cs b/src/Nito.Comparers.Core/Util/SourceComparerBase.cs index b9238f2..4fc18f8 100644 --- a/src/Nito.Comparers.Core/Util/SourceComparerBase.cs +++ b/src/Nito.Comparers.Core/Util/SourceComparerBase.cs @@ -18,7 +18,7 @@ internal abstract class SourceComparerBase : ComparerBase /// /// The GetHashCode implementation for the source comparer. /// - protected readonly Func SourceGetHashCode; + protected readonly Func SourceGetHashCode; /// /// Initializes a new instance of the class. @@ -26,7 +26,7 @@ internal abstract class SourceComparerBase : ComparerBase /// The source comparer. If this is null, the default comparer is used. /// The GetHashCode implementation to use. If this is null, this type will attempt to find GetHashCode on ; if none is found, throws an exception. /// A value indicating whether null values are passed to and . If false, then null values are considered less than any non-null values and are not passed to nor . This value is ignored if is a non-nullable type. - protected SourceComparerBase(IComparer? source, Func? getHashCode, bool specialNullHandling) + protected SourceComparerBase(IComparer? source, Func? getHashCode, bool specialNullHandling) : base(specialNullHandling) { Source = ComparerHelpers.NormalizeDefault(source); diff --git a/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs index 061ad11..caa742c 100644 --- a/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs @@ -21,21 +21,21 @@ public UnorderedSequenceEqualityComparer(IEqualityComparer? source) } /// - protected override int DoGetHashCode(IEnumerable obj) + protected override int DoGetHashCode(IEnumerable? obj) { var ret = CommutativeHashCombiner.Create(); - foreach (var item in obj) + foreach (var item in obj!) ret.Combine(Source.GetHashCode(item!)); return ret.HashCode; } /// - protected override bool DoEquals(IEnumerable x, IEnumerable y) + protected override bool DoEquals(IEnumerable? x, IEnumerable? y) { - var xCount = x.TryGetCount(); + var xCount = x!.TryGetCount(); if (xCount != null) { - var yCount = y.TryGetCount(); + var yCount = y!.TryGetCount(); if (yCount != null) { if (xCount.Value != yCount.Value) @@ -47,46 +47,44 @@ protected override bool DoEquals(IEnumerable x, IEnumerable y) var equivalenceClassCounts = new Dictionary(EqualityComparerBuilder.For().EquateBy(w => w.Value, Source)); - using (var xIter = x.GetEnumerator()) - using (var yIter = y.GetEnumerator()) + using var xIter = x!.GetEnumerator(); + using var yIter = y!.GetEnumerator(); + while (true) { - while (true) + if (!xIter.MoveNext()) { - if (!xIter.MoveNext()) + if (!yIter.MoveNext()) { - if (!yIter.MoveNext()) - { - // We have reached the end of both sequences simultaneously. - // They are equivalent if all equivalence class counts have canceled each other out. - return equivalenceClassCounts.All(kvp => kvp.Value == 0); - } - - return false; + // We have reached the end of both sequences simultaneously. + // They are equivalent if all equivalence class counts have canceled each other out. + return equivalenceClassCounts.All(kvp => kvp.Value == 0); } - if (!yIter.MoveNext()) - return false; + return false; + } - // If both items are equivalent, just skip the equivalence class counts. - if (Source.Equals(xIter.Current, yIter.Current)) - continue; + if (!yIter.MoveNext()) + return false; - var xKey = new Wrapper { Value = xIter.Current }; - var yKey = new Wrapper { Value = yIter.Current }; + // If both items are equivalent, just skip the equivalence class counts. + if (Source.Equals(xIter.Current, yIter.Current)) + continue; - // Treat `x` as adding counts and `y` as subtracting counts; any counts not present are 0. - if (equivalenceClassCounts.TryGetValue(xKey, out var xValue)) - ++xValue; - else - xValue = 1; - equivalenceClassCounts[xKey] = xValue; - if (equivalenceClassCounts.TryGetValue(yKey, out var yValue)) - --yValue; - else - yValue = -1; - equivalenceClassCounts[yKey] = yValue; - } - } + var xKey = new Wrapper { Value = xIter.Current }; + var yKey = new Wrapper { Value = yIter.Current }; + + // Treat `x` as adding counts and `y` as subtracting counts; any counts not present are 0. + if (equivalenceClassCounts.TryGetValue(xKey, out var xValue)) + ++xValue; + else + xValue = 1; + equivalenceClassCounts[xKey] = xValue; + if (equivalenceClassCounts.TryGetValue(yKey, out var yValue)) + --yValue; + else + yValue = -1; + equivalenceClassCounts[yKey] = yValue; + } } /// diff --git a/src/Nito.Comparers.Ix/Nito.Comparers.Ix.csproj b/src/Nito.Comparers.Ix/Nito.Comparers.Ix.csproj index 73ef777..877a8f1 100644 --- a/src/Nito.Comparers.Ix/Nito.Comparers.Ix.csproj +++ b/src/Nito.Comparers.Ix/Nito.Comparers.Ix.csproj @@ -2,7 +2,7 @@ Comparer extensions for System Interactive (Ix). - netstandard1.0;netstandard2.0;net461 + netstandard1.0;netstandard2.0;net45;net461 comparer;equalitycomparer;icomparable;iequatable Nito.Comparers diff --git a/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj b/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj index 1ee9297..9846b2f 100644 --- a/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj +++ b/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj @@ -2,7 +2,7 @@ Comparer extension methods for System.Linq. - netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 + netstandard1.0;netstandard1.3;netstandard2.0;net45;net461;net472;netcoreapp2.0 Nito.Comparers diff --git a/src/Nito.Comparers.Rx/Nito.Comparers.Rx.csproj b/src/Nito.Comparers.Rx/Nito.Comparers.Rx.csproj index e7cf3e0..90cf7da 100644 --- a/src/Nito.Comparers.Rx/Nito.Comparers.Rx.csproj +++ b/src/Nito.Comparers.Rx/Nito.Comparers.Rx.csproj @@ -2,7 +2,7 @@ Comparer extension methods for System.Reactive. - netstandard1.0;netstandard2.0;net461 + netstandard1.0;netstandard2.0;net45;net461 comparer;equalitycomparer;icomparable;iequatable Nito.Comparers @@ -11,7 +11,7 @@ - + diff --git a/src/Nito.Comparers/Nito.Comparers.csproj b/src/Nito.Comparers/Nito.Comparers.csproj index a222a9e..6c74f9d 100644 --- a/src/Nito.Comparers/Nito.Comparers.csproj +++ b/src/Nito.Comparers/Nito.Comparers.csproj @@ -2,8 +2,8 @@ The last comparison library you'll ever need! - netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 - comparer;equalitycomparer;icomparable;iequatable + netstandard1.0;netstandard1.3;netstandard2.0;net45;net461;net472;netcoreapp2.0 + comparer;equalitycomparer;icomparable;iequatable true diff --git a/src/project.props b/src/project.props index 8dd712e..89c24f6 100644 --- a/src/project.props +++ b/src/project.props @@ -2,5 +2,6 @@ 6.2.2 Stephen Cleary + false diff --git a/test/UnitTests/Compare_NaturalString.cs b/test/UnitTests/Compare_NaturalString.cs new file mode 100644 index 0000000..047a6c7 --- /dev/null +++ b/test/UnitTests/Compare_NaturalString.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nito.Comparers; +using Xunit; +using System.Globalization; + +#pragma warning disable CS0162 + +namespace UnitTests +{ + public class Compare_NaturalString + { + [Theory] + [InlineData("a10", "a10", StringComparison.Ordinal)] + [InlineData("a10", "A10", StringComparison.OrdinalIgnoreCase)] + [InlineData("a010", "a10", StringComparison.Ordinal)] + [InlineData("a000010", "a10", StringComparison.Ordinal)] + [InlineData("a000b", "a0b", StringComparison.Ordinal)] + [InlineData("0000", "0", StringComparison.Ordinal)] + [InlineData("1.01", "1.1", StringComparison.Ordinal)] // Possibly surprising + public void ExpectedEqual(string x, string y, StringComparison stringComparison) + { + var comparer = ComparerBuilder.For().Natural(stringComparison); + Assert.Equal(0, comparer.Compare(x, y)); + } + + [Theory] + [InlineData("a09", "a10", StringComparison.Ordinal)] + [InlineData("a9", "a10", StringComparison.Ordinal)] + [InlineData("a9b", "a10b", StringComparison.Ordinal)] + [InlineData("a10a", "a10b", StringComparison.Ordinal)] + [InlineData("a", "aa", StringComparison.Ordinal)] + [InlineData("1", "11", StringComparison.Ordinal)] + [InlineData("", "a", StringComparison.Ordinal)] + [InlineData("", "0", StringComparison.Ordinal)] + [InlineData("", "3", StringComparison.Ordinal)] + [InlineData("0", "a", StringComparison.Ordinal)] + [InlineData("1.1", "1.10", StringComparison.Ordinal)] // Possibly surprising + [InlineData("a a", "ab", StringComparison.Ordinal)] // Possibly surprising + public void ExpectedLessThan(string x, string y, StringComparison stringComparison) + { + var comparer = ComparerBuilder.For().Natural(stringComparison); + Assert.True(comparer.Compare(x, y) < 0); + Assert.True(comparer.Compare(y, x) > 0); + } + + [Fact] + public void ToString_DumpsComparer() + { + Assert.Equal("NaturalString", ComparerBuilder.For().Natural(StringComparison.Ordinal).ToString()); + } + } +} diff --git a/test/UnitTests/EqualityCompare_NaturalString.cs b/test/UnitTests/EqualityCompare_NaturalString.cs new file mode 100644 index 0000000..ddcef77 --- /dev/null +++ b/test/UnitTests/EqualityCompare_NaturalString.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nito.Comparers; +using Xunit; +using System.Globalization; + +#pragma warning disable CS0162 + +namespace UnitTests +{ + public class EqualityCompare_NaturalString + { + [Theory] + [InlineData("a10", "a10", StringComparison.Ordinal)] + [InlineData("a10", "A10", StringComparison.OrdinalIgnoreCase)] + [InlineData("a010", "a10", StringComparison.Ordinal)] + [InlineData("a000010", "a10", StringComparison.Ordinal)] + [InlineData("a000b", "a0b", StringComparison.Ordinal)] + [InlineData("0000", "0", StringComparison.Ordinal)] + [InlineData("1.01", "1.1", StringComparison.Ordinal)] // Possibly surprising + public void ExpectedEqual(string x, string y, StringComparison stringComparison) + { + var equalityComparer = EqualityComparerBuilder.For().Natural(stringComparison); + Assert.True(equalityComparer.Equals(x, y)); + } + + [Theory] + [InlineData("a09", "a10", StringComparison.Ordinal)] + [InlineData("a9", "a10", StringComparison.Ordinal)] + [InlineData("a9b", "a10b", StringComparison.Ordinal)] + [InlineData("a10a", "a10b", StringComparison.Ordinal)] + [InlineData("a", "aa", StringComparison.Ordinal)] + [InlineData("1", "11", StringComparison.Ordinal)] + [InlineData("", "a", StringComparison.Ordinal)] + [InlineData("", "0", StringComparison.Ordinal)] + [InlineData("", "3", StringComparison.Ordinal)] + [InlineData("0", "a", StringComparison.Ordinal)] + [InlineData("1.1", "1.10", StringComparison.Ordinal)] // Possibly surprising + [InlineData("a a", "ab", StringComparison.Ordinal)] // Possibly surprising + public void ExpectedLessThan(string x, string y, StringComparison stringComparison) + { + var equalityComparer = EqualityComparerBuilder.For().Natural(stringComparison); + Assert.False(equalityComparer.Equals(x, y)); + } + + [Fact] + public void ToString_DumpsComparer() + { + Assert.Equal("NaturalString", EqualityComparerBuilder.For().Natural(StringComparison.Ordinal).ToString()); + } + } +} diff --git a/test/UnitTests/IComparerTUnitTests.cs b/test/UnitTests/IComparerTUnitTests.cs index e99882c..3a2785d 100644 --- a/test/UnitTests/IComparerTUnitTests.cs +++ b/test/UnitTests/IComparerTUnitTests.cs @@ -59,7 +59,15 @@ public class IComparerTUnitTests () => ComparerBuilder.For().OrderBy(x => x.Id, (IComparer)null, false, false).Reverse(), () => ComparerBuilder.For().Default().Reverse(), () => ComparerBuilder.For().Default().Reverse(), - () => ComparerBuilder.For().OrderBy(x => (string)x, (IComparer)null, false, false).Reverse() + () => ComparerBuilder.For().OrderBy(x => (string)x, (IComparer)null, false, false).Reverse(), + + // Natural String Comparers + () => ComparerBuilder.For().Natural(StringComparison.Ordinal), + () => ComparerBuilder.For().Natural(StringComparison.OrdinalIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.InvariantCulture), + () => ComparerBuilder.For().Natural(StringComparison.InvariantCultureIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.CurrentCulture), + () => ComparerBuilder.For().Natural(StringComparison.CurrentCultureIgnoreCase), }; public static readonly List> ComparersExceptObject = diff --git a/test/UnitTests/IComparerUnitTests.cs b/test/UnitTests/IComparerUnitTests.cs index 20035fd..042dd0c 100644 --- a/test/UnitTests/IComparerUnitTests.cs +++ b/test/UnitTests/IComparerUnitTests.cs @@ -61,6 +61,14 @@ public class IComparerUnitTests () => ComparerBuilder.For().Default().Reverse(), () => ComparerBuilder.For().Default().Reverse(), () => ComparerBuilder.For().OrderBy(x => (string)x, (IComparer)null, false, false).Reverse(), + + // Natural String Comparers + () => ComparerBuilder.For().Natural(StringComparison.Ordinal), + () => ComparerBuilder.For().Natural(StringComparison.OrdinalIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.InvariantCulture), + () => ComparerBuilder.For().Natural(StringComparison.InvariantCultureIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.CurrentCulture), + () => ComparerBuilder.For().Natural(StringComparison.CurrentCultureIgnoreCase), }; public static readonly List> ComparersExceptObject = diff --git a/test/UnitTests/IEqualityComparerTUnitTests.cs b/test/UnitTests/IEqualityComparerTUnitTests.cs index a71d0f2..3a51032 100644 --- a/test/UnitTests/IEqualityComparerTUnitTests.cs +++ b/test/UnitTests/IEqualityComparerTUnitTests.cs @@ -99,6 +99,20 @@ public class IEqualityComparerTUnitTests () => ComparerBuilder.For().Default().Reverse(), () => ComparerBuilder.For().Default().Reverse(), () => ComparerBuilder.For().OrderBy(x => (string)x, (IComparer)null, false, false).Reverse(), + + // Natural String Comparers + () => EqualityComparerBuilder.For().Natural(StringComparison.Ordinal), + () => EqualityComparerBuilder.For().Natural(StringComparison.OrdinalIgnoreCase), + () => EqualityComparerBuilder.For().Natural(StringComparison.InvariantCulture), + () => EqualityComparerBuilder.For().Natural(StringComparison.InvariantCultureIgnoreCase), + () => EqualityComparerBuilder.For().Natural(StringComparison.CurrentCulture), + () => EqualityComparerBuilder.For().Natural(StringComparison.CurrentCultureIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.Ordinal), + () => ComparerBuilder.For().Natural(StringComparison.OrdinalIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.InvariantCulture), + () => ComparerBuilder.For().Natural(StringComparison.InvariantCultureIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.CurrentCulture), + () => ComparerBuilder.For().Natural(StringComparison.CurrentCultureIgnoreCase), }; public static readonly List> EqualityComparersExceptObject = diff --git a/test/UnitTests/IEqualityComparerUnitTests.cs b/test/UnitTests/IEqualityComparerUnitTests.cs index 9245b4a..cda0748 100644 --- a/test/UnitTests/IEqualityComparerUnitTests.cs +++ b/test/UnitTests/IEqualityComparerUnitTests.cs @@ -107,6 +107,20 @@ public class IEqualityComparerUnitTests () => ComparerBuilder.For().Default().Reverse(), () => ComparerBuilder.For().Default().Reverse(), () => ComparerBuilder.For().OrderBy(x => (string)x, (IComparer)null, false, false).Reverse(), + + // Natural String Comparers + () => EqualityComparerBuilder.For().Natural(StringComparison.Ordinal), + () => EqualityComparerBuilder.For().Natural(StringComparison.OrdinalIgnoreCase), + () => EqualityComparerBuilder.For().Natural(StringComparison.InvariantCulture), + () => EqualityComparerBuilder.For().Natural(StringComparison.InvariantCultureIgnoreCase), + () => EqualityComparerBuilder.For().Natural(StringComparison.CurrentCulture), + () => EqualityComparerBuilder.For().Natural(StringComparison.CurrentCultureIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.Ordinal), + () => ComparerBuilder.For().Natural(StringComparison.OrdinalIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.InvariantCulture), + () => ComparerBuilder.For().Natural(StringComparison.InvariantCultureIgnoreCase), + () => ComparerBuilder.For().Natural(StringComparison.CurrentCulture), + () => ComparerBuilder.For().Natural(StringComparison.CurrentCultureIgnoreCase), }; public static readonly List> EqualityComparersExceptObject =