return new CultureAwareComparer(culture, options);
}
+ /// <summary>
+ /// Determines whether the specified <see cref="IEqualityComparer{String}"/> is a well-known ordinal string comparer.
+ /// </summary>
+ /// <param name="comparer">The comparer to query.</param>
+ /// <param name="ignoreCase">When this method returns, contains a value stating whether <paramref name="comparer"/>
+ /// is case-insensitive. Set to <see langword="false"/> if this method returns <see langword="false"/>.</param>
+ /// <returns>
+ /// <see langword="true"/> if <paramref name="comparer"/> is a well-known ordinal string comparer;
+ /// otherwise, <see langword="false"/>.
+ /// </returns>
+ /// <remarks>
+ /// A "well-known ordinal comparer" describes a comparer which behaves identically to <see cref="Ordinal"/>
+ /// when passed to <see cref="Dictionary{String, TValue}.Dictionary"/> or <see cref="HashSet{String}.HashSet"/>.
+ /// For example, <see cref="EqualityComparer{String}.Default"/> is a well-known ordinal comparer because
+ /// a <see cref="Dictionary{String, TValue}"/> given <see cref="EqualityComparer{String}.Default"/> as a constructor
+ /// argument will behave identically to a <see cref="Dictionary{String, TValue}"/> given <see cref="Ordinal"/>
+ /// as a constructor argument. If <paramref name="ignoreCase"/> is <see langword="true"/> on method exit,
+ /// then <paramref name="comparer"/> behaves identically to <see cref="OrdinalIgnoreCase"/> when passed to the
+ /// constructor of such a collection.
+ /// </remarks>
+ public static bool IsWellKnownOrdinalComparer(IEqualityComparer<string?>? comparer, out bool ignoreCase)
+ {
+ if (comparer is IInternalStringEqualityComparer internalStringComparer)
+ {
+ comparer = internalStringComparer.GetUnderlyingEqualityComparer(); // unwrap if necessary
+ }
+
+ switch (comparer)
+ {
+ case StringComparer stringComparer:
+ return stringComparer.IsWellKnownOrdinalComparerCore(out ignoreCase);
+ case GenericEqualityComparer<string>:
+ // special-case EqualityComparer<string>.Default, which is Ordinal-equivalent
+ ignoreCase = false;
+ return true;
+ default:
+ // unknown comparer
+ ignoreCase = default;
+ return false;
+ }
+ }
+
+ private protected virtual bool IsWellKnownOrdinalComparerCore(out bool ignoreCase)
+ {
+ // unless specialized comparer overrides this, we're not a well-known ordinal comparer
+ ignoreCase = default;
+ return false;
+ }
+
+ /// <summary>
+ /// Determines whether the specified <see cref="IEqualityComparer{String}"/> is a well-known culture-aware string comparer.
+ /// </summary>
+ /// <param name="comparer">The comparer to query.</param>
+ /// <param name="compareInfo">When this method returns, contains a value indicating which <see cref="CompareInfo"/> was used
+ /// to create <paramref name="comparer"/>. Set to <see langword="null"/> if this method returns <see langword="false"/>.</param>
+ /// <param name="compareOptions">When this method returns, contains a value indicating which <see cref="CompareOptions"/> was used
+ /// to create <paramref name="comparer"/>. Set to <see cref="CompareOptions.None"/> if this method returns <see langword="false"/>.</param>
+ /// whether <paramref name="comparer"/>
+ /// <returns>
+ /// <see langword="true"/> if <paramref name="comparer"/> is a well-known culture-aware string comparer;
+ /// otherwise, <see langword="false"/>.
+ /// </returns>
+ /// <remarks>
+ /// A "well-known culture-aware comparer" describes a comparer which is tied to a specific <see cref="CompareInfo"/> using
+ /// some defined <see cref="CompareOptions"/>. To create a <see cref="StringComparer"/> instance wrapped around a
+ /// <see cref="CompareInfo"/> and <see cref="CompareOptions"/>, use <see cref="GlobalizationExtensions.GetStringComparer(CompareInfo, CompareOptions)"/>.
+ /// This method returns <see langword="false"/> when given <see cref="Ordinal"/> and other non-linguistic comparers as input.
+ /// </remarks>
+ public static bool IsWellKnownCultureAwareComparer(IEqualityComparer<string?>? comparer, [NotNullWhen(true)] out CompareInfo? compareInfo, out CompareOptions compareOptions)
+ {
+ if (comparer is IInternalStringEqualityComparer internalStringComparer)
+ {
+ comparer = internalStringComparer.GetUnderlyingEqualityComparer(); // unwrap if necessary
+ }
+
+ if (comparer is StringComparer stringComparer)
+ {
+ return stringComparer.IsWellKnownCultureAwareComparerCore(out compareInfo, out compareOptions);
+ }
+ else
+ {
+ // unknown comparer
+ compareInfo = default;
+ compareOptions = default;
+ return false;
+ }
+ }
+
+ private protected virtual bool IsWellKnownCultureAwareComparerCore([NotNullWhen(true)] out CompareInfo? compareInfo, out CompareOptions compareOptions)
+ {
+ // unless specialized comparer overrides this, we're not a well-known culture-aware comparer
+ compareInfo = default;
+ compareOptions = default;
+ return false;
+ }
+
public int Compare(object? x, object? y)
{
if (x == y) return 0;
info.AddValue("_options", _options);
info.AddValue("_ignoreCase", (_options & CompareOptions.IgnoreCase) != 0);
}
+
+ private protected override bool IsWellKnownCultureAwareComparerCore([NotNullWhen(true)] out CompareInfo? compareInfo, out CompareOptions compareOptions)
+ {
+ compareInfo = _compareInfo;
+ compareOptions = _options;
+ return true;
+ }
}
[Serializable]
int hashCode = nameof(OrdinalComparer).GetHashCode();
return _ignoreCase ? (~hashCode) : hashCode;
}
+
+ private protected override bool IsWellKnownOrdinalComparerCore(out bool ignoreCase)
+ {
+ ignoreCase = _ignoreCase;
+ return true;
+ }
}
[Serializable]
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Generic;
using System.Globalization;
+using System.Reflection;
using Xunit;
namespace System.Tests
Assert.Equal(1, c.Compare("42", null));
Assert.Throws<ArgumentException>(() => c.Compare(42, "84"));
}
+
+ [Fact]
+ public void IsWellKnownOrdinalComparer_TestCases()
+ {
+ CompareInfo ci_enUS = CompareInfo.GetCompareInfo("en-US");
+
+ // First, instantiate and test the comparers directly
+
+ RunTest(null, false, false);
+ RunTest(EqualityComparer<string>.Default, true, false); // EC<string>.Default is Ordinal-equivalent
+ RunTest(EqualityComparer<object>.Default, false, false); // EC<object>.Default isn't a string comparer
+ RunTest(StringComparer.Ordinal, true, false);
+ RunTest(StringComparer.OrdinalIgnoreCase, true, true);
+ RunTest(StringComparer.InvariantCulture, false, false); // not ordinal
+ RunTest(StringComparer.InvariantCultureIgnoreCase, false, false); // not ordinal
+ RunTest(GetNonRandomizedComparer("WrappedAroundDefaultComparer"), true, false); // EC<string>.Default is Ordinal-equivalent
+ RunTest(GetNonRandomizedComparer("WrappedAroundStringComparerOrdinal"), true, false);
+ RunTest(GetNonRandomizedComparer("WrappedAroundStringComparerOrdinalIgnoreCase"), true, true);
+ RunTest(new CustomStringComparer(), false, false); // not an inbox comparer
+ RunTest(ci_enUS.GetStringComparer(CompareOptions.None), false, false); // linguistic
+ RunTest(ci_enUS.GetStringComparer(CompareOptions.Ordinal), true, false);
+ RunTest(ci_enUS.GetStringComparer(CompareOptions.OrdinalIgnoreCase), true, true);
+
+ // Then, make sure that this API works with common collection types
+
+ RunTest(new Dictionary<string, object>().Comparer, true, false);
+ RunTest(new Dictionary<string, object>(StringComparer.Ordinal).Comparer, true, false);
+ RunTest(new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase).Comparer, true, true);
+ RunTest(new Dictionary<string, object>(StringComparer.InvariantCulture).Comparer, false, false);
+ RunTest(new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase).Comparer, false, false);
+
+ RunTest(new HashSet<string>().Comparer, true, false);
+ RunTest(new HashSet<string>(StringComparer.Ordinal).Comparer, true, false);
+ RunTest(new HashSet<string>(StringComparer.OrdinalIgnoreCase).Comparer, true, true);
+ RunTest(new HashSet<string>(StringComparer.InvariantCulture).Comparer, false, false);
+ RunTest(new HashSet<string>(StringComparer.InvariantCultureIgnoreCase).Comparer, false, false);
+
+ static void RunTest(IEqualityComparer<string> comparer, bool expectedIsOrdinal, bool expectedIgnoreCase)
+ {
+ Assert.Equal(expectedIsOrdinal, StringComparer.IsWellKnownOrdinalComparer(comparer, out bool actualIgnoreCase));
+ Assert.Equal(expectedIgnoreCase, actualIgnoreCase);
+ }
+ }
+
+ [Fact]
+ public void IsWellKnownCultureAwareComparer_TestCases()
+ {
+ CompareInfo ci_enUS = CompareInfo.GetCompareInfo("en-US");
+ CompareInfo ci_inv = CultureInfo.InvariantCulture.CompareInfo;
+
+ // First, instantiate and test the comparers directly
+
+ RunTest(null, null, default);
+ RunTest(EqualityComparer<string>.Default, null, default); // EC<string>.Default is not culture-aware
+ RunTest(EqualityComparer<object>.Default, null, default); // EC<object>.Default isn't a string comparer
+ RunTest(StringComparer.Ordinal, null, default);
+ RunTest(StringComparer.OrdinalIgnoreCase, null, default);
+ RunTest(StringComparer.InvariantCulture, ci_inv, CompareOptions.None);
+ RunTest(StringComparer.InvariantCultureIgnoreCase, ci_inv, CompareOptions.IgnoreCase);
+ RunTest(GetNonRandomizedComparer("WrappedAroundDefaultComparer"), null, default); // EC<string>.Default is Ordinal-equivalent
+ RunTest(GetNonRandomizedComparer("WrappedAroundStringComparerOrdinal"), null, default);
+ RunTest(GetNonRandomizedComparer("WrappedAroundStringComparerOrdinalIgnoreCase"), null, default);
+ RunTest(new CustomStringComparer(), null, default); // not an inbox comparer
+ RunTest(ci_enUS.GetStringComparer(CompareOptions.None), ci_enUS, CompareOptions.None);
+ RunTest(ci_enUS.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType), ci_enUS, CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType);
+ RunTest(ci_enUS.GetStringComparer(CompareOptions.Ordinal), null, default); // not linguistic
+ RunTest(ci_enUS.GetStringComparer(CompareOptions.OrdinalIgnoreCase), null, default); // not linguistic
+ RunTest(StringComparer.Create(CultureInfo.InvariantCulture, false), ci_inv, CompareOptions.None);
+ RunTest(StringComparer.Create(CultureInfo.InvariantCulture, true), ci_inv, CompareOptions.IgnoreCase);
+ RunTest(StringComparer.Create(CultureInfo.InvariantCulture, CompareOptions.IgnoreSymbols), ci_inv, CompareOptions.IgnoreSymbols);
+
+ // Then, make sure that this API works with common collection types
+
+ RunTest(new Dictionary<string, object>().Comparer, null, default);
+ RunTest(new Dictionary<string, object>(StringComparer.Ordinal).Comparer, null, default);
+ RunTest(new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase).Comparer, null, default);
+ RunTest(new Dictionary<string, object>(StringComparer.InvariantCulture).Comparer, ci_inv, CompareOptions.None);
+ RunTest(new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase).Comparer, ci_inv, CompareOptions.IgnoreCase);
+
+ RunTest(new HashSet<string>().Comparer, null, default);
+ RunTest(new HashSet<string>(StringComparer.Ordinal).Comparer, null, default);
+ RunTest(new HashSet<string>(StringComparer.OrdinalIgnoreCase).Comparer, null, default);
+ RunTest(new HashSet<string>(StringComparer.InvariantCulture).Comparer, ci_inv, CompareOptions.None);
+ RunTest(new HashSet<string>(StringComparer.InvariantCultureIgnoreCase).Comparer, ci_inv, CompareOptions.IgnoreCase);
+
+ static void RunTest(IEqualityComparer<string> comparer, CompareInfo expectedCompareInfo, CompareOptions expectedCompareOptions)
+ {
+ bool actualReturnValue = StringComparer.IsWellKnownCultureAwareComparer(comparer, out CompareInfo actualCompareInfo, out CompareOptions actualCompareOptions);
+ Assert.Equal(expectedCompareInfo != null, actualReturnValue);
+ Assert.Equal(expectedCompareInfo, actualCompareInfo);
+ Assert.Equal(expectedCompareOptions, actualCompareOptions);
+ }
+ }
+
+ private static IEqualityComparer<string> GetNonRandomizedComparer(string name)
+ {
+ Type nonRandomizedComparerType = typeof(StringComparer).Assembly.GetType("System.Collections.Generic.NonRandomizedStringEqualityComparer");
+ Assert.NotNull(nonRandomizedComparerType);
+
+ FieldInfo fi = nonRandomizedComparerType.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
+ Assert.NotNull(fi);
+
+ return (IEqualityComparer<string>)fi.GetValue(null);
+ }
+
+ private class CustomStringComparer : StringComparer
+ {
+ public override int Compare(string x, string y) => throw new NotImplementedException();
+ public override bool Equals(string x, string y) => throw new NotImplementedException();
+ public override int GetHashCode(string obj) => throw new NotImplementedException();
+ }
}
}