Add IsWellKnownStringComparer methods (#50312)
authorLevi Broderick <GrabYourPitchforks@users.noreply.github.com>
Wed, 31 Mar 2021 17:30:24 +0000 (10:30 -0700)
committerGitHub <noreply@github.com>
Wed, 31 Mar 2021 17:30:24 +0000 (10:30 -0700)
src/libraries/System.Private.CoreLib/src/System/StringComparer.cs
src/libraries/System.Runtime/ref/System.Runtime.cs
src/libraries/System.Runtime/tests/System/StringComparerTests.cs

index 15a1055..ce22da6 100644 (file)
@@ -62,6 +62,102 @@ namespace System
             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;
@@ -202,6 +298,13 @@ namespace System
             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]
@@ -280,6 +383,12 @@ namespace System
             int hashCode = nameof(OrdinalComparer).GetHashCode();
             return _ignoreCase ? (~hashCode) : hashCode;
         }
+
+        private protected override bool IsWellKnownOrdinalComparerCore(out bool ignoreCase)
+        {
+            ignoreCase = _ignoreCase;
+            return true;
+        }
     }
 
     [Serializable]
index 303016c..97f2121 100644 (file)
@@ -3699,6 +3699,8 @@ namespace System
         public static System.StringComparer FromComparison(System.StringComparison comparisonType) { throw null; }
         public int GetHashCode(object obj) { throw null; }
         public abstract int GetHashCode(string obj);
+        public static bool IsWellKnownCultureAwareComparer(System.Collections.Generic.IEqualityComparer<string?>? comparer, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Globalization.CompareInfo? compareInfo, out System.Globalization.CompareOptions compareOptions) { throw null; }
+        public static bool IsWellKnownOrdinalComparer(System.Collections.Generic.IEqualityComparer<string?>? comparer, out bool ignoreCase) { throw null; }
     }
     public enum StringComparison
     {
index 364ecdf..3027c87 100644 (file)
@@ -1,7 +1,9 @@
 // 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
@@ -135,5 +137,116 @@ 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();
+        }
     }
 }