From 35b7277cf689417a9f43002544fc6f7ebcb5da90 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 02:36:28 -0400 Subject: [PATCH 01/19] Ignoe EOL warnings. --- src/Comparers/Comparers.csproj | 1 + src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj | 1 + src/Nito.Comparers/Nito.Comparers.csproj | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Comparers/Comparers.csproj b/src/Comparers/Comparers.csproj index df99963..f067eab 100644 --- a/src/Comparers/Comparers.csproj +++ b/src/Comparers/Comparers.csproj @@ -3,6 +3,7 @@ This old package just forwards to Nito.Comparers. netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 + false comparer;equalitycomparer;icomparable;iequatable true diff --git a/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj b/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj index 1ee9297..6ec02fe 100644 --- a/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj +++ b/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj @@ -3,6 +3,7 @@ Comparer extension methods for System.Linq. netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 + false Nito.Comparers diff --git a/src/Nito.Comparers/Nito.Comparers.csproj b/src/Nito.Comparers/Nito.Comparers.csproj index a222a9e..d0a42c5 100644 --- a/src/Nito.Comparers/Nito.Comparers.csproj +++ b/src/Nito.Comparers/Nito.Comparers.csproj @@ -3,7 +3,8 @@ The last comparison library you'll ever need! netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 - comparer;equalitycomparer;icomparable;iequatable + false + comparer;equalitycomparer;icomparable;iequatable true From 4e17c67bb2988543ce34cb584903b27b33fcde59 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 02:36:59 -0400 Subject: [PATCH 02/19] net6.0-era nullable annotations --- .../Advanced/AdvancedComparerBase.cs | 20 +++++------ .../Advanced/AdvancedEqualityComparerBase.cs | 16 ++++----- src/Nito.Comparers.Core/ComparableBase.cs | 4 +-- src/Nito.Comparers.Core/ComparerBuilderFor.cs | 4 +-- src/Nito.Comparers.Core/ComparerExtensions.cs | 4 +-- .../EqualityComparerBuilderFor.cs | 4 +-- .../EqualityComparerExtensions.cs | 4 +-- src/Nito.Comparers.Core/EquatableBase.cs | 2 +- .../Fixes/ExplicitGetHashCodeComparer.cs | 6 ++-- .../Fixes/FixComparerExtensions.cs | 2 +- .../Fixes/StandardNullHandlingComparer.cs | 4 +-- .../StandardNullHandlingEqualityComparer.cs | 4 +-- .../Nito.Comparers.Core.csproj | 2 +- .../Util/ComparableImplementations.cs | 36 +++++++++---------- src/Nito.Comparers.Core/Util/ComparerBase.cs | 10 +++--- .../Util/ComparerHelpers.cs | 4 +-- .../Util/CompoundComparer.cs | 10 +++--- .../Util/CompoundEqualityComparer.cs | 8 ++--- .../Util/DefaultComparer.cs | 6 ++-- .../Util/EqualityComparerBase.cs | 16 ++++----- src/Nito.Comparers.Core/Util/NullComparer.cs | 4 +-- .../Util/ReferenceEqualityComparer.cs | 4 +-- .../Util/ReverseComparer.cs | 4 +-- .../Util/SelectComparer.cs | 8 ++--- .../Util/SelectEqualityComparer.cs | 8 ++--- .../Util/SequenceComparer.cs | 10 +++--- .../Util/SequenceEqualityComparer.cs | 14 ++++---- .../Util/SourceComparerBase.cs | 4 +-- .../Util/UnorderedSequenceEqualityComparer.cs | 14 ++++---- 29 files changed, 118 insertions(+), 118 deletions(-) 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..9d3df72 100644 --- a/src/Nito.Comparers.Core/ComparerBuilderFor.cs +++ b/src/Nito.Comparers.Core/ComparerBuilderFor.cs @@ -46,7 +46,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 +64,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..1271c24 100644 --- a/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs +++ b/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs @@ -54,7 +54,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 +71,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/Nito.Comparers.Core.csproj b/src/Nito.Comparers.Core/Nito.Comparers.Core.csproj index d8bf617..a845d6c 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;net461;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..fa561b7 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. @@ -59,7 +59,7 @@ bool System.Collections.IEqualityComparer.Equals(object? x, object? y) return (x == null && y == null); } - return DoEquals((T)x!, (T)y!); + return DoEquals((T?)x, (T?)y); } /// @@ -75,11 +75,11 @@ int System.Collections.IEqualityComparer.GetHashCode(object? obj) if (!objValid) throw new ArgumentException("Invalid type for comparison."); - return DoGetHashCode((T)obj!); + return DoGetHashCode((T?)obj); } /// - public bool Equals(T x, T y) + public bool Equals(T? x, T? y) { if (!SpecialNullHandling) { @@ -87,11 +87,11 @@ public bool Equals(T x, T y) return (x == null && y == null); } - return DoEquals(x!, y!); + return DoEquals(x, y); } /// - public int GetHashCode(T obj) + public int GetHashCode(T? obj) { if (!SpecialNullHandling) { @@ -99,7 +99,7 @@ public int GetHashCode(T obj) return 0; } - return DoGetHashCode(obj!); + return DoGetHashCode(obj); } } } 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..72541c7 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..89d1a86 100644 --- a/src/Nito.Comparers.Core/Util/SequenceComparer.cs +++ b/src/Nito.Comparers.Core/Util/SequenceComparer.cs @@ -19,19 +19,19 @@ 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) { diff --git a/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs index 56ed126..3346685 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,8 +43,8 @@ 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) { 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..b2d4e83 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,8 +47,8 @@ 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) { From 5bb986c7546f0c6ff326376e5d510de34dc4168e Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 02:44:38 -0400 Subject: [PATCH 03/19] Move CheckEolTargetFramework to project.props. --- src/Comparers/Comparers.csproj | 3 +-- src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj | 1 - src/Nito.Comparers/Nito.Comparers.csproj | 1 - src/project.props | 1 + 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Comparers/Comparers.csproj b/src/Comparers/Comparers.csproj index f067eab..66032c6 100644 --- a/src/Comparers/Comparers.csproj +++ b/src/Comparers/Comparers.csproj @@ -1,9 +1,8 @@ - + This old package just forwards to Nito.Comparers. netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 - false comparer;equalitycomparer;icomparable;iequatable true diff --git a/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj b/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj index 6ec02fe..1ee9297 100644 --- a/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj +++ b/src/Nito.Comparers.Linq/Nito.Comparers.Linq.csproj @@ -3,7 +3,6 @@ Comparer extension methods for System.Linq. netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 - false Nito.Comparers diff --git a/src/Nito.Comparers/Nito.Comparers.csproj b/src/Nito.Comparers/Nito.Comparers.csproj index d0a42c5..8d4fea1 100644 --- a/src/Nito.Comparers/Nito.Comparers.csproj +++ b/src/Nito.Comparers/Nito.Comparers.csproj @@ -3,7 +3,6 @@ The last comparison library you'll ever need! netstandard1.0;netstandard1.3;netstandard2.0;net461;net472;netcoreapp2.0 - false 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 From 6f124c0a45673a51c3c4cb19e315597bbe00ea5e Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 02:45:18 -0400 Subject: [PATCH 04/19] Last nullability override. --- src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs index 72541c7..0c5a610 100644 --- a/src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/ReferenceEqualityComparer.cs @@ -25,7 +25,7 @@ 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); From 559faa73397d2eaba4289589433702ee0b4bcf5f Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Thu, 25 Jun 2020 22:12:59 -0400 Subject: [PATCH 05/19] wip --- .../Internals/NaturalStringComparison.cs | 194 ++++++++++++++++++ .../Nito.Comparers.Core.csproj | 2 +- .../Util/EqualityComparerBase.cs | 58 +----- .../Util/EqualityComparerHelpers.cs | 62 +++++- .../Util/NaturalStringComparer.cs | 30 +++ .../Util/StringSpanComparer.cs | 93 +++++++++ 6 files changed, 383 insertions(+), 56 deletions(-) create mode 100644 src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs create mode 100644 src/Nito.Comparers.Core/Util/NaturalStringComparer.cs create mode 100644 src/Nito.Comparers.Core/Util/StringSpanComparer.cs diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs new file mode 100644 index 0000000..0c126a2 --- /dev/null +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -0,0 +1,194 @@ +using Nito.Comparers.Internals; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +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' }; + + /// + /// 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 comparison to use for the text segments. + public static int GetHashCode(string obj, StringComparison comparison) + { + int index = 0; + var result = Murmur3Hash.Create(); + while (index < obj.Length) + { + var start = index; + NextSegment(obj, ref start, out var end, out var isNumeric); + + // Note that leading zeros have been stripped from the range [start, end), so an ordinal comparison is sufficient to detect numeric equality. + var segmentComparison = isNumeric ? StringComparison.Ordinal : comparison; + +#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET45 + var segmentHashCode = TryGetComparer(segmentComparison)?.GetHashCode(obj.Substring(start, end - start)) ?? 0; +#else + var segmentHashCode = string.GetHashCode(obj.AsSpan(start, end - start), segmentComparison); +#endif + + result.Combine(segmentHashCode); + index = end; + } + + return result.HashCode; + } + + // Implementation map: + // .NET Core 3.0+ - This method is not defined. string.GetHashCode is used instead. + // .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. + // The recommended solution on those platforms is "upgrade to a .NET Standard 2.0-compatible version". + // .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. +#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET45 +#if !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NET45 + private static StringComparer? TryGetComparer(StringComparison comparison) + { + try + { + return StringComparer.FromComparison(comparison); + } + catch (ArgumentException) + { + return null; + } + } +#else + private static StringComparer? TryGetComparer(StringComparison comparison) + { + switch (comparison) + { + case StringComparison.Ordinal: return StringComparer.Ordinal; + case StringComparison.OrdinalIgnoreCase: return StringComparer.OrdinalIgnoreCase; + case StringComparison.CurrentCulture: return StringComparer.CurrentCulture; + case StringComparison.CurrentCultureIgnoreCase: return StringComparer.CurrentCultureIgnoreCase; +#if !NETSTANDARD1_0 + case StringComparison.InvariantCulture: return StringComparer.InvariantCulture; + case StringComparison.InvariantCultureIgnoreCase: return StringComparer.InvariantCultureIgnoreCase; +#endif + default: return 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 comparison to use for the text segments. + public static int Compare(string x, string y, StringComparison comparison) + { + int xIndex = 0, yIndex = 0; + while (xIndex < x.Length && yIndex < y.Length) + { + var xStart = xIndex; + var yStart = yIndex; + NextSegment(x, ref xStart, out var xEnd, out var xIsNumeric); + NextSegment(y, ref yStart, out var yEnd, out var yIsNumeric); + if (xIsNumeric && yIsNumeric) + { + var xLength = xEnd - xStart; + var yLength = yEnd - yStart; + if (xLength < yLength) + return -1; + else if (xLength > yLength) + return 1; + var compareResult = string.Compare(x, xStart, y, yStart, xLength, StringComparison.Ordinal); + if (compareResult != 0) + return compareResult; + } + else if (!xIsNumeric && !yIsNumeric) + { + var xLength = xEnd - xStart; + var yLength = yEnd - yStart; + var compareResult = Compare(x, xStart, xLength, y, yStart, yLength, comparison); + if (compareResult != 0) + return compareResult; + var lengthCompare = xLength - yLength; + if (lengthCompare != 0) + return lengthCompare; + } + else if (xIsNumeric) + { + return -1; + } + else + { + return 1; + } + + xIndex = xEnd; + yIndex = yEnd; + } + + if (xIndex < x.Length) + return 1; + if (yIndex > y.Length) + return -1; + return 0; + } + + private static int Compare(string strA, int indexA, int lengthA, string strB, int indexB, int lengthB, StringComparison comparisonType) + { + // Blatantly stolen from https://dogmamix.com/cms/blog/Finding-substrings + return comparisonType switch + { + StringComparison.CurrentCulture => CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.None), + StringComparison.CurrentCultureIgnoreCase => CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.IgnoreCase), + (StringComparison)2 /* InvariantCulture */ => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.None), + (StringComparison)3 /* InvariantCultureIgnoreCase */ => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.IgnoreCase), + StringComparison.Ordinal => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.Ordinal), + StringComparison.OrdinalIgnoreCase => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.OrdinalIgnoreCase), + _ => throw new ArgumentException($"The string comparison type {comparisonType} is not supported.", nameof(comparisonType)), + }; + } + + private static void NextSegment(string source, ref int start, out int end, out bool isNumeric) + { + // Prerequisite: index < source.Length + 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; + } + + 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 a845d6c..dd9ee5a 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;net6.0 + netstandard1.0;netstandard2.0;netstandard2.1;net461;netcoreapp2.0;netcoreapp3.0;netcoreapp5.0;net6.0 comparer;equalitycomparer;icomparable;iequatable Nito.Comparers diff --git a/src/Nito.Comparers.Core/Util/EqualityComparerBase.cs b/src/Nito.Comparers.Core/Util/EqualityComparerBase.cs index fa561b7..9590a26 100644 --- a/src/Nito.Comparers.Core/Util/EqualityComparerBase.cs +++ b/src/Nito.Comparers.Core/Util/EqualityComparerBase.cs @@ -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..31e1e92 --- /dev/null +++ b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs @@ -0,0 +1,30 @@ +using Nito.Comparers.Internals; +using System; + +namespace Nito.Comparers.Util +{ + /// + /// The natural string comparer. + /// + internal sealed class NaturalStringComparer : ComparerBase + { + private readonly StringComparison _comparison; + + public NaturalStringComparer(StringComparison comparison) + : base(false) + { + _comparison = comparison; + } + + /// + protected override int DoGetHashCode(string obj) => NaturalStringComparison.GetHashCode(obj, _comparison); + + /// + protected override int DoCompare(string x, string y) => NaturalStringComparison.Compare(x, y, _comparison); + + /// + /// 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/StringSpanComparer.cs b/src/Nito.Comparers.Core/Util/StringSpanComparer.cs new file mode 100644 index 0000000..9d995e3 --- /dev/null +++ b/src/Nito.Comparers.Core/Util/StringSpanComparer.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text; + +namespace Nito.Comparers.Util +{ + public sealed class StringSpanComparer : IFullComparer + { + private readonly Func _getCompareInfo; + private readonly CompareOptions _options; + + public StringSpanComparer(Func getCompareInfo, CompareOptions options) + { + _getCompareInfo = getCompareInfo; + _options = options; + } + + public StringSpanComparer(CompareInfo compareInfo, CompareOptions options) + : this(() => compareInfo, options) + { + } + + 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)), + }; + } + + public int Compare(ReadOnlySpan x, ReadOnlySpan y) => _getCompareInfo().Compare(x, y, _options); + + public bool Equals(ReadOnlySpan x, ReadOnlySpan y) => Compare(x, y) == 0; + + public int GetHashCode(ReadOnlySpan obj) => _getCompareInfo().GetHashCode(obj, _options); + + public int Compare(string? x, string? y) + { + throw new NotImplementedException(); + } + + public bool Equals(string? x, string? y) + { + throw new NotImplementedException(); + } + + private bool DoEquals(string x, string y) => Equals(x.AsSpan(), y.AsSpan()); + + public int GetHashCode(string? obj) + { + throw new NotImplementedException(); + } + + private bool DoGetHashCode(string obj) => GetHashCode(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); + } +} From 43264be86d13fd9625f88d3704ad0f2ce23406ee Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 26 Oct 2020 22:02:26 -0400 Subject: [PATCH 06/19] More wip. --- .../Internals/NaturalStringComparison.cs | 5 ++ .../Util/StringSpanComparer.cs | 50 +++++++++++-------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index 0c126a2..80ffcc0 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -20,6 +20,8 @@ public static class NaturalStringComparison /// The string comparison to use for the text segments. public static int GetHashCode(string obj, StringComparison comparison) { + _ = obj ?? throw new ArgumentNullException(nameof(obj)); + int index = 0; var result = Murmur3Hash.Create(); while (index < obj.Length) @@ -92,6 +94,9 @@ public static int GetHashCode(string obj, StringComparison comparison) /// The string comparison to use for the text segments. public static int Compare(string x, string y, StringComparison comparison) { + _ = x ?? throw new ArgumentNullException(nameof(x)); + _ = y ?? throw new ArgumentNullException(nameof(y)); + int xIndex = 0, yIndex = 0; while (xIndex < x.Length && yIndex < y.Length) { diff --git a/src/Nito.Comparers.Core/Util/StringSpanComparer.cs b/src/Nito.Comparers.Core/Util/StringSpanComparer.cs index 9d995e3..eeae21a 100644 --- a/src/Nito.Comparers.Core/Util/StringSpanComparer.cs +++ b/src/Nito.Comparers.Core/Util/StringSpanComparer.cs @@ -7,24 +7,28 @@ 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 Func _getCompareInfo; + private readonly CompareInfo _compareInfo; private readonly CompareOptions _options; - public StringSpanComparer(Func getCompareInfo, CompareOptions options) - { - _getCompareInfo = getCompareInfo; - _options = options; - } - + /// + /// Creates a new instance using the specified compare info and options. + /// public StringSpanComparer(CompareInfo compareInfo, CompareOptions options) - : this(() => compareInfo, options) { + _compareInfo = compareInfo; + _options = options; } + /// + /// Creates a new instance using the specified string comparison. + /// public StringSpanComparer(StringComparison comparison) - : this(() => GetCompareInfo(comparison), GetCompareOptions(comparison)) + : this(GetCompareInfo(comparison), GetCompareOptions(comparison)) { } @@ -56,30 +60,36 @@ private static CompareOptions GetCompareOptions(StringComparison comparison) }; } - public int Compare(ReadOnlySpan x, ReadOnlySpan y) => _getCompareInfo().Compare(x, y, _options); + /// + /// 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; - public int GetHashCode(ReadOnlySpan obj) => _getCompareInfo().GetHashCode(obj, _options); + /// + /// 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) - { - 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.AsSpan(), y.AsSpan()); - public int GetHashCode(string? obj) - { - throw new NotImplementedException(); - } + /// + public int GetHashCode(string? obj) => EqualityComparerHelpers.ImplementGetHashCode(obj, false, DoGetHashCode!); - private bool DoGetHashCode(string obj) => GetHashCode(obj.AsSpan()); + private int DoGetHashCode(string obj) => GetHashCode(obj.AsSpan()); int IComparer.Compare(object? x, object? y) { From 87f8ecc68aee752a0b850846fcf1ea724b683535 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 04:06:08 -0400 Subject: [PATCH 07/19] Fix precompiler checks. --- .../Internals/NaturalStringComparison.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index 80ffcc0..801415a 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -32,7 +32,7 @@ public static int GetHashCode(string obj, StringComparison comparison) // Note that leading zeros have been stripped from the range [start, end), so an ordinal comparison is sufficient to detect numeric equality. var segmentComparison = isNumeric ? StringComparison.Ordinal : comparison; -#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET45 +#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET461 var segmentHashCode = TryGetComparer(segmentComparison)?.GetHashCode(obj.Substring(start, end - start)) ?? 0; #else var segmentHashCode = string.GetHashCode(obj.AsSpan(start, end - start), segmentComparison); @@ -53,9 +53,9 @@ public static int GetHashCode(string obj, StringComparison comparison) // 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. // The recommended solution on those platforms is "upgrade to a .NET Standard 2.0-compatible version". // .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. -#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET45 -#if !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NET45 + // .NET Framework 4.6.1+ - This method is a switch statement, supporting all StringComparison values. +#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET461 +#if !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NET461 private static StringComparer? TryGetComparer(StringComparison comparison) { try From 2440f14e35e17ef469452c9d2c1b6330649ce477 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 04:06:20 -0400 Subject: [PATCH 08/19] Fix nullability. --- src/Nito.Comparers.Core/Util/NaturalStringComparer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs index 31e1e92..cac4c4d 100644 --- a/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs +++ b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs @@ -17,10 +17,10 @@ public NaturalStringComparer(StringComparison comparison) } /// - protected override int DoGetHashCode(string obj) => NaturalStringComparison.GetHashCode(obj, _comparison); + protected override int DoGetHashCode(string? obj) => NaturalStringComparison.GetHashCode(obj!, _comparison); /// - protected override int DoCompare(string x, string y) => NaturalStringComparison.Compare(x, y, _comparison); + protected override int DoCompare(string? x, string? y) => NaturalStringComparison.Compare(x!, y!, _comparison); /// /// Returns a short, human-readable description of the comparer. This is intended for debugging and not for other purposes. From 016ba0c9b78e55d3f2ecb5cc858158571d31b3ba Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 04:14:14 -0400 Subject: [PATCH 09/19] Corrections to span comparer. --- src/Nito.Comparers.Core/Util/StringSpanComparer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nito.Comparers.Core/Util/StringSpanComparer.cs b/src/Nito.Comparers.Core/Util/StringSpanComparer.cs index eeae21a..0ea0c09 100644 --- a/src/Nito.Comparers.Core/Util/StringSpanComparer.cs +++ b/src/Nito.Comparers.Core/Util/StringSpanComparer.cs @@ -84,12 +84,12 @@ public int Compare(string? x, string? y) /// public bool Equals(string? x, string? y) => EqualityComparerHelpers.ImplementEquals(x, y, false, DoEquals!); - private bool DoEquals(string x, string y) => Equals(x.AsSpan(), y.AsSpan()); + 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.AsSpan()); + private int DoGetHashCode(string? obj) => GetHashCode(obj == null ? default : obj.AsSpan()); int IComparer.Compare(object? x, object? y) { From 4df8cff0b33f9fe51a76c650924f5941359588f1 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 04:15:57 -0400 Subject: [PATCH 10/19] Move StringSpanComparer to future. --- {src/Nito.Comparers.Core/Util => future}/StringSpanComparer.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src/Nito.Comparers.Core/Util => future}/StringSpanComparer.cs (100%) diff --git a/src/Nito.Comparers.Core/Util/StringSpanComparer.cs b/future/StringSpanComparer.cs similarity index 100% rename from src/Nito.Comparers.Core/Util/StringSpanComparer.cs rename to future/StringSpanComparer.cs From 6baa242b84d5f74080de6d3951ae7f03a59a5b9b Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 04:24:12 -0400 Subject: [PATCH 11/19] Cleanup. --- src/Nito.Comparers.Core/ComparerBuilderFor.cs | 2 + .../EqualityComparerBuilderFor.cs | 3 + .../Internals/NaturalStringComparison.cs | 26 ++++---- .../Util/SequenceComparer.cs | 28 ++++----- .../Util/SequenceEqualityComparer.cs | 30 +++++---- .../Util/UnorderedSequenceEqualityComparer.cs | 62 +++++++++---------- 6 files changed, 76 insertions(+), 75 deletions(-) diff --git a/src/Nito.Comparers.Core/ComparerBuilderFor.cs b/src/Nito.Comparers.Core/ComparerBuilderFor.cs index 9d3df72..2d4f01e 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 { /// diff --git a/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs b/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs index 1271c24..7c039b3 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 { /// diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index 801415a..2fdda7c 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -1,9 +1,11 @@ -using Nito.Comparers.Internals; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Text; +#pragma warning disable IDE0079 +#pragma warning disable IDE0057 + namespace Nito.Comparers.Internals { /// @@ -70,18 +72,18 @@ public static int GetHashCode(string obj, StringComparison comparison) #else private static StringComparer? TryGetComparer(StringComparison comparison) { - switch (comparison) + return comparison switch { - case StringComparison.Ordinal: return StringComparer.Ordinal; - case StringComparison.OrdinalIgnoreCase: return StringComparer.OrdinalIgnoreCase; - case StringComparison.CurrentCulture: return StringComparer.CurrentCulture; - case StringComparison.CurrentCultureIgnoreCase: return StringComparer.CurrentCultureIgnoreCase; + StringComparison.Ordinal => StringComparer.Ordinal, + StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase, + StringComparison.CurrentCulture => StringComparer.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, #if !NETSTANDARD1_0 - case StringComparison.InvariantCulture: return StringComparer.InvariantCulture; - case StringComparison.InvariantCultureIgnoreCase: return StringComparer.InvariantCultureIgnoreCase; + StringComparison.InvariantCulture => StringComparer.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, #endif - default: return null; - } + _ => null, + }; } #endif #endif @@ -193,7 +195,7 @@ private static void NextSegment(string source, ref int start, out int end, out b end = index; } - bool IsDigit(char ch) => ch >= '0' && ch <= '9'; + static bool IsDigit(char ch) => ch >= '0' && ch <= '9'; } } } diff --git a/src/Nito.Comparers.Core/Util/SequenceComparer.cs b/src/Nito.Comparers.Core/Util/SequenceComparer.cs index 89d1a86..6aec643 100644 --- a/src/Nito.Comparers.Core/Util/SequenceComparer.cs +++ b/src/Nito.Comparers.Core/Util/SequenceComparer.cs @@ -30,25 +30,23 @@ protected override int DoGetHashCode(IEnumerable? obj) /// 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 3346685..a59b591 100644 --- a/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/SequenceEqualityComparer.cs @@ -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/UnorderedSequenceEqualityComparer.cs b/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs index b2d4e83..caa742c 100644 --- a/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs +++ b/src/Nito.Comparers.Core/Util/UnorderedSequenceEqualityComparer.cs @@ -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; + } } /// From 0aae19d230ff58c0322c79e34d7706ae4c28743b Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 06:15:08 -0400 Subject: [PATCH 12/19] Use delegates in NaturalStringComparison. --- .../Internals/NaturalStringComparison.cs | 120 ++++++++++-------- .../Util/NaturalStringComparer.cs | 12 +- 2 files changed, 77 insertions(+), 55 deletions(-) diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index 2fdda7c..b2ad38c 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -14,15 +14,17 @@ namespace Nito.Comparers.Internals 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 comparison to use for the text segments. - public static int GetHashCode(string obj, StringComparison comparison) + /// 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)); int index = 0; var result = Murmur3Hash.Create(); @@ -32,13 +34,8 @@ public static int GetHashCode(string obj, StringComparison comparison) NextSegment(obj, ref start, out var end, out var isNumeric); // Note that leading zeros have been stripped from the range [start, end), so an ordinal comparison is sufficient to detect numeric equality. - var segmentComparison = isNumeric ? StringComparison.Ordinal : comparison; - -#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET461 - var segmentHashCode = TryGetComparer(segmentComparison)?.GetHashCode(obj.Substring(start, end - start)) ?? 0; -#else - var segmentHashCode = string.GetHashCode(obj.AsSpan(start, end - start), segmentComparison); -#endif + var segmentGetHashCode = isNumeric ? OrdinalStringGetHashCode : substringGetHashCode; + var segmentHashCode = segmentGetHashCode(obj, start, end - start); result.Combine(segmentHashCode); index = end; @@ -47,57 +44,74 @@ public static int GetHashCode(string obj, StringComparison comparison) return result.HashCode; } - // Implementation map: - // .NET Core 3.0+ - This method is not defined. string.GetHashCode is used instead. - // .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. - // The recommended solution on those platforms is "upgrade to a .NET Standard 2.0-compatible version". - // .NET Core 2.0-2.2 - This method forwards to StringComparer.FromComparison. - // .NET Framework 4.6.1+ - This method is a switch statement, supporting all StringComparison values. + /// + /// 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 || 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 + + // Implementation map: + // .NET Core 3.0+ - This method is not defined. string.GetHashCode is used instead. + // .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. + // The recommended solution on those platforms is "upgrade to a .NET Standard 2.0-compatible version". + // .NET Core 2.0-2.2 - This method forwards to StringComparer.FromComparison. + // .NET Framework 4.6.1+ - This method is a switch statement, supporting all StringComparison values. #if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET461 #if !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NET461 - private static StringComparer? TryGetComparer(StringComparison comparison) - { - try - { - return StringComparer.FromComparison(comparison); - } - catch (ArgumentException) + static StringComparer? TryGetComparer(StringComparison comparison) { - return null; + try + { + return StringComparer.FromComparison(comparison); + } + catch (ArgumentException) + { + return null; + } } - } #else - private static StringComparer? TryGetComparer(StringComparison comparison) - { - return comparison switch + static StringComparer? TryGetComparer(StringComparison comparison) { - StringComparison.Ordinal => StringComparer.Ordinal, - StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase, - StringComparison.CurrentCulture => StringComparer.CurrentCulture, - StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase, + 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, + StringComparison.InvariantCulture => StringComparer.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase, #endif - _ => null, - }; - } + _ => 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 comparison to use for the text segments. - public static int Compare(string x, string y, StringComparison comparison) + /// 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)); int xIndex = 0, yIndex = 0; while (xIndex < x.Length && yIndex < y.Length) @@ -122,7 +136,7 @@ public static int Compare(string x, string y, StringComparison comparison) { var xLength = xEnd - xStart; var yLength = yEnd - yStart; - var compareResult = Compare(x, xStart, xLength, y, yStart, yLength, comparison); + var compareResult = substringCompare(x, xStart, xLength, y, yStart, yLength); if (compareResult != 0) return compareResult; var lengthCompare = xLength - yLength; @@ -149,18 +163,22 @@ public static int Compare(string x, string y, StringComparison comparison) return 0; } - private static int Compare(string strA, int indexA, int lengthA, string strB, int indexB, int lengthB, StringComparison comparisonType) + /// + /// 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 comparisonType switch + return stringComparison switch { - StringComparison.CurrentCulture => CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.None), - StringComparison.CurrentCultureIgnoreCase => CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.IgnoreCase), - (StringComparison)2 /* InvariantCulture */ => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.None), - (StringComparison)3 /* InvariantCultureIgnoreCase */ => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.IgnoreCase), - StringComparison.Ordinal => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.Ordinal), - StringComparison.OrdinalIgnoreCase => CultureInfo.InvariantCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, CompareOptions.OrdinalIgnoreCase), - _ => throw new ArgumentException($"The string comparison type {comparisonType} is not supported.", nameof(comparisonType)), + 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)), }; } diff --git a/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs index cac4c4d..b802267 100644 --- a/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs +++ b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs @@ -1,6 +1,8 @@ using Nito.Comparers.Internals; using System; +#pragma warning disable CA1812 + namespace Nito.Comparers.Util { /// @@ -8,19 +10,21 @@ namespace Nito.Comparers.Util /// internal sealed class NaturalStringComparer : ComparerBase { - private readonly StringComparison _comparison; + private readonly Func _substringCompare; + private readonly Func _substringGetHashCode; public NaturalStringComparer(StringComparison comparison) : base(false) { - _comparison = comparison; + _substringCompare = NaturalStringComparison.GetSubstringCompare(comparison); + _substringGetHashCode = NaturalStringComparison.GetSubstringGetHashCode(comparison); } /// - protected override int DoGetHashCode(string? obj) => NaturalStringComparison.GetHashCode(obj!, _comparison); + protected override int DoGetHashCode(string? obj) => NaturalStringComparison.GetHashCode(obj!, _substringGetHashCode); /// - protected override int DoCompare(string? x, string? y) => NaturalStringComparison.Compare(x!, y!, _comparison); + 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. From 87010d1267c027527366afae44ec466dc1ad6352 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 12:30:43 -0400 Subject: [PATCH 13/19] Comments. --- src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index b2ad38c..84efe61 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -61,13 +61,13 @@ public static Func GetSubstringGetHashCode(StringComparis // Implementation map: // .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.6.1+ - 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. // The recommended solution on those platforms is "upgrade to a .NET Standard 2.0-compatible version". - // .NET Core 2.0-2.2 - This method forwards to StringComparer.FromComparison. - // .NET Framework 4.6.1+ - This method is a switch statement, supporting all StringComparison values. #if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET461 #if !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NET461 static StringComparer? TryGetComparer(StringComparison comparison) From 490907165537e178f68671fc4329dec12166b3f7 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 12:49:34 -0400 Subject: [PATCH 14/19] Extend support to net45, to ensure all .NET Framework versions support invariant cultures for natural string comparison GetHashCode implementations. --- src/Comparers.Ix/Comparers.Ix.csproj | 2 +- src/Comparers.Rx/Comparers.Rx.csproj | 2 +- src/Comparers/Comparers.csproj | 2 +- .../Internals/Murmur3Hash.cs | 4 +-- .../Internals/NaturalStringComparison.cs | 29 +++++++++++-------- .../Nito.Comparers.Core.csproj | 2 +- .../Nito.Comparers.Ix.csproj | 2 +- .../Nito.Comparers.Linq.csproj | 2 +- .../Nito.Comparers.Rx.csproj | 4 +-- src/Nito.Comparers/Nito.Comparers.csproj | 2 +- 10 files changed, 28 insertions(+), 23 deletions(-) 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 66032c6..f090ddd 100644 --- a/src/Comparers/Comparers.csproj +++ b/src/Comparers/Comparers.csproj @@ -2,7 +2,7 @@ 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/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 index 84efe61..06de579 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -50,7 +50,7 @@ public static int GetHashCode(string obj, Func substringG /// The comparison type used by the returned delegate. public static Func GetSubstringGetHashCode(StringComparison stringComparison) { -#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET461 +#if NETSTANDARD1_0 || NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NET45 || NET461 var comparer = TryGetComparer(stringComparison); if (comparer == null) return (str, offset, length) => 0; @@ -59,17 +59,22 @@ public static Func GetSubstringGetHashCode(StringComparis return (str, offset, length) => string.GetHashCode(str.AsSpan(offset, length), stringComparison); #endif - // Implementation map: - // .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.6.1+ - 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. - // 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 || NET461 -#if !NETSTANDARD1_0 && !NETSTANDARD2_0 && !NET461 + // 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. + // 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 diff --git a/src/Nito.Comparers.Core/Nito.Comparers.Core.csproj b/src/Nito.Comparers.Core/Nito.Comparers.Core.csproj index dd9ee5a..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;netstandard2.1;net461;netcoreapp2.0;netcoreapp3.0;netcoreapp5.0;net6.0 + 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.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 8d4fea1..6c74f9d 100644 --- a/src/Nito.Comparers/Nito.Comparers.csproj +++ b/src/Nito.Comparers/Nito.Comparers.csproj @@ -2,7 +2,7 @@ The last comparison library you'll ever need! - 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 From 957bd0780b455aa1326c9c9ed21ca99a339a1534 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 13:14:08 -0400 Subject: [PATCH 15/19] Expose NaturalStringComparer. --- src/Nito.Comparers.Core/ComparerBuilderFor.cs | 7 +++++++ src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs | 7 +++++++ .../Internals/NaturalStringComparison.cs | 1 + src/Nito.Comparers.Core/Util/NaturalStringComparer.cs | 2 -- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Nito.Comparers.Core/ComparerBuilderFor.cs b/src/Nito.Comparers.Core/ComparerBuilderFor.cs index 2d4f01e..ebd0e5c 100644 --- a/src/Nito.Comparers.Core/ComparerBuilderFor.cs +++ b/src/Nito.Comparers.Core/ComparerBuilderFor.cs @@ -37,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. /// diff --git a/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs b/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs index 7c039b3..792c592 100644 --- a/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs +++ b/src/Nito.Comparers.Core/EqualityComparerBuilderFor.cs @@ -38,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. /// diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index 06de579..85ec8d0 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -72,6 +72,7 @@ public static Func GetSubstringGetHashCode(StringComparis // .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 diff --git a/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs index b802267..8e5ee3e 100644 --- a/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs +++ b/src/Nito.Comparers.Core/Util/NaturalStringComparer.cs @@ -1,8 +1,6 @@ using Nito.Comparers.Internals; using System; -#pragma warning disable CA1812 - namespace Nito.Comparers.Util { /// From bac0fbfd512d4be1b30fd6f7c956e31ec54af51e Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 19:08:24 -0400 Subject: [PATCH 16/19] Introduce SegmentParser. --- .../Internals/NaturalStringComparison.cs | 120 ++++++++++-------- 1 file changed, 67 insertions(+), 53 deletions(-) diff --git a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index 85ec8d0..626043d 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -26,19 +26,17 @@ public static int GetHashCode(string obj, Func substringG _ = obj ?? throw new ArgumentNullException(nameof(obj)); _ = substringGetHashCode ?? throw new ArgumentNullException(nameof(substringGetHashCode)); - int index = 0; + var parser = new SegmentParser(obj); var result = Murmur3Hash.Create(); - while (index < obj.Length) + while (!parser.IsDone) { - var start = index; - NextSegment(obj, ref start, out var end, out var isNumeric); + 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 = isNumeric ? OrdinalStringGetHashCode : substringGetHashCode; - var segmentHashCode = segmentGetHashCode(obj, start, end - start); + var segmentGetHashCode = parser.IsNumeric ? OrdinalStringGetHashCode : substringGetHashCode; + var segmentHashCode = segmentGetHashCode(parser.Source, parser.Start, parser.Length); result.Combine(segmentHashCode); - index = end; } return result.HashCode; @@ -119,37 +117,36 @@ public static int Compare(string x, string y, Func yLength) return 1; - var compareResult = string.Compare(x, xStart, y, yStart, xLength, StringComparison.Ordinal); + var compareResult = string.Compare(xParser.Source, xParser.Start, yParser.Source, yParser.Start, xLength, StringComparison.Ordinal); if (compareResult != 0) return compareResult; } - else if (!xIsNumeric && !yIsNumeric) + else if (!xParser.IsNumeric && !yParser.IsNumeric) { - var xLength = xEnd - xStart; - var yLength = yEnd - yStart; - var compareResult = substringCompare(x, xStart, xLength, y, yStart, yLength); + 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; var lengthCompare = xLength - yLength; if (lengthCompare != 0) return lengthCompare; } - else if (xIsNumeric) + else if (xParser.IsNumeric) { return -1; } @@ -157,15 +154,12 @@ public static int Compare(string x, string y, Func y.Length) + if (xParser.IsDone) return -1; + if (yParser.IsDone) + return 1; return 0; } @@ -188,38 +182,58 @@ public static Func GetSubstringCompare( }; } - private static void NextSegment(string source, ref int start, out int end, out bool isNumeric) + private ref struct SegmentParser { - // Prerequisite: index < source.Length - var index = start; - isNumeric = IsDigit(source[index++]); - if (isNumeric) + 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() { - // Skip leading zeros, but keep one if that's the only digit. - if (source[start] == '0') + // Prerequisite: index < source.Length + + Start = End; + var index = Start; + IsNumeric = IsDigit(Source[index++]); + if (IsNumeric) { - do + // Skip leading zeros, but keep one if that's the only digit. + if (Source[Start] == '0') { - ++start; - } while (start < source.Length && source[start] == '0'); - if (start == source.Length || !IsDigit(source[start])) - --start; - index = start + 1; + 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; } - while (index < source.Length && IsDigit(source[index])) - ++index; - end = index; + static bool IsDigit(char ch) => ch >= '0' && ch <= '9'; } - else - { - index = source.IndexOfAny(Digits, index); - if (index == -1) - index = source.Length; - end = index; - } - - static bool IsDigit(char ch) => ch >= '0' && ch <= '9'; } } } From 481406a3f0770fe180657d6203bf3480c179f98c Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Mon, 21 Mar 2022 19:18:38 -0400 Subject: [PATCH 17/19] Fix actions. --- .github/workflows/build.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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: | From e8bbec067072757a4de104bf93d9183d635c0834 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Wed, 23 Mar 2022 22:38:49 -0400 Subject: [PATCH 18/19] Unit tests and bugfix. --- CHANGELOG.md | 7 +++ .../Internals/NaturalStringComparison.cs | 10 ++-- test/UnitTests/Compare_NaturalString.cs | 54 +++++++++++++++++++ .../EqualityCompare_NaturalString.cs | 53 ++++++++++++++++++ test/UnitTests/IComparerTUnitTests.cs | 10 +++- test/UnitTests/IComparerUnitTests.cs | 8 +++ test/UnitTests/IEqualityComparerTUnitTests.cs | 14 +++++ test/UnitTests/IEqualityComparerUnitTests.cs | 14 +++++ 8 files changed, 163 insertions(+), 7 deletions(-) create mode 100644 test/UnitTests/Compare_NaturalString.cs create mode 100644 test/UnitTests/EqualityCompare_NaturalString.cs 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/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index 626043d..b941ce2 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -142,9 +142,6 @@ public static int Compare(string x, string y, Func 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 = From 68b5408b27bc4d0e8bfe0a9867d94f4b6f12f6e5 Mon Sep 17 00:00:00 2001 From: Stephen Cleary Date: Fri, 23 Jun 2023 19:25:40 -0400 Subject: [PATCH 19/19] Work in progress --- .../Internals/IStringSplitter.cs | 22 +++++++ .../Internals/ISubstringComparer.cs | 12 ++++ .../Internals/NaturalStringComparison.cs | 64 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 src/Nito.Comparers.Core/Internals/IStringSplitter.cs create mode 100644 src/Nito.Comparers.Core/Internals/ISubstringComparer.cs 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/NaturalStringComparison.cs b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs index b941ce2..cdbb993 100644 --- a/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs +++ b/src/Nito.Comparers.Core/Internals/NaturalStringComparison.cs @@ -180,6 +180,32 @@ public static Func GetSubstringCompare( }; } + 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) @@ -233,5 +259,43 @@ public void ParseNext() 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'; + } + } } }