File: src\libraries\System.Private.CoreLib\src\System\Globalization\CompareInfo.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
 
namespace System.Globalization
{
    /// <summary>
    /// This class implements a set of methods for comparing strings.
    /// </summary>
    [Serializable]
    [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
    public sealed partial class CompareInfo : IDeserializationCallback
    {
        // Mask used to check if IndexOf()/LastIndexOf()/IsPrefix()/IsPostfix() has the right flags.
        private const CompareOptions ValidIndexMaskOffFlags =
            ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
              CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType);
 
        // Mask used to check if Compare() / GetHashCode(string) / GetSortKey has the right flags.
        private const CompareOptions ValidCompareMaskOffFlags =
            ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
              CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.StringSort);
 
        // Cache the invariant CompareInfo
        internal static readonly CompareInfo Invariant = CultureInfo.InvariantCulture.CompareInfo;
 
        // CompareInfos have an interesting identity.  They are attached to the locale that created them,
        // ie: en-US would have an en-US sort.  For haw-US (custom), then we serialize it as haw-US.
        // The interesting part is that since haw-US doesn't have its own sort, it has to point at another
        // locale, which is what SCOMPAREINFO does.
        [OptionalField(VersionAdded = 2)]
        private string m_name;  // The name used to construct this CompareInfo. Do not rename (binary serialization)
 
        [NonSerialized]
        private IntPtr _sortHandle;
 
        [NonSerialized]
        private string _sortName; // The name that defines our behavior
 
        [OptionalField(VersionAdded = 3)]
        private SortVersion? m_SortVersion; // Do not rename (binary serialization)
 
        private int culture; // Do not rename (binary serialization). The fields sole purpose is to support Desktop serialization.
 
        internal CompareInfo(CultureInfo culture)
        {
            m_name = culture._name;
            InitSort(culture);
        }
 
        /// <summary>
        /// Get the CompareInfo constructed from the data table in the specified
        /// assembly for the specified culture.
        /// Warning: The assembly versioning mechanism is dead!
        /// </summary>
        public static CompareInfo GetCompareInfo(int culture, Assembly assembly)
        {
            ArgumentNullException.ThrowIfNull(assembly);
 
            // Parameter checking.
            if (assembly != typeof(object).Module.Assembly)
            {
                throw new ArgumentException(SR.Argument_OnlyMscorlib, nameof(assembly));
            }
 
            return GetCompareInfo(culture);
        }
 
        /// <summary>
        /// Get the CompareInfo constructed from the data table in the specified
        /// assembly for the specified culture.
        /// The purpose of this method is to provide version for CompareInfo tables.
        /// </summary>
        public static CompareInfo GetCompareInfo(string name, Assembly assembly)
        {
            ArgumentNullException.ThrowIfNull(name);
            ArgumentNullException.ThrowIfNull(assembly);
 
            if (assembly != typeof(object).Module.Assembly)
            {
                throw new ArgumentException(SR.Argument_OnlyMscorlib, nameof(assembly));
            }
 
            return GetCompareInfo(name);
        }
 
        /// <summary>
        /// Get the CompareInfo for the specified culture.
        /// This method is provided for ease of integration with NLS-based software.
        /// </summary>
        public static CompareInfo GetCompareInfo(int culture)
        {
            if (CultureData.IsCustomCultureId(culture))
            {
                throw new ArgumentException(SR.Argument_CustomCultureCannotBePassedByNumber, nameof(culture));
            }
 
            return CultureInfo.GetCultureInfo(culture).CompareInfo;
        }
 
        /// <summary>
        /// Get the CompareInfo for the specified culture.
        /// </summary>
        public static CompareInfo GetCompareInfo(string name)
        {
            ArgumentNullException.ThrowIfNull(name);
 
            return CultureInfo.GetCultureInfo(name).CompareInfo;
        }
 
        public static bool IsSortable(char ch)
        {
            return IsSortable(new ReadOnlySpan<char>(in ch));
        }
 
        public static bool IsSortable(string text)
        {
            ArgumentNullException.ThrowIfNull(text);
 
            return IsSortable(text.AsSpan());
        }
 
        /// <summary>
        /// Indicates whether a specified Unicode string is sortable.
        /// </summary>
        /// <param name="text">A string of zero or more Unicode characters.</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="text"/> is non-empty and contains
        /// only sortable Unicode characters; otherwise, <see langword="false"/>.
        /// </returns>
        public static bool IsSortable(ReadOnlySpan<char> text)
        {
            if (text.Length == 0)
            {
                return false;
            }
 
            if (GlobalizationMode.Invariant)
            {
                return true; // all chars are sortable in invariant mode
            }
 
            return (GlobalizationMode.UseNls) ? NlsIsSortable(text) : IcuIsSortable(text);
        }
 
        /// <summary>
        /// Indicates whether a specified <see cref="Rune"/> is sortable.
        /// </summary>
        /// <param name="value">A Unicode scalar value.</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="value"/> is a sortable Unicode scalar
        /// value; otherwise, <see langword="false"/>.
        /// </returns>
        public static bool IsSortable(Rune value)
        {
            Span<char> valueAsUtf16 = stackalloc char[Rune.MaxUtf16CharsPerRune];
            int charCount = value.EncodeToUtf16(valueAsUtf16);
            return IsSortable(valueAsUtf16.Slice(0, charCount));
        }
 
        [MemberNotNull(nameof(_sortName))]
        private void InitSort(CultureInfo culture)
        {
            _sortName = culture.SortName;
 
            if (GlobalizationMode.UseNls)
            {
                NlsInitSortHandle();
            }
            else
            {
                IcuInitSortHandle(culture.InteropName!);
            }
        }
 
        [OnDeserializing]
        private void OnDeserializing(StreamingContext ctx)
        {
            // this becomes null for a brief moment before deserialization
            // after serialization is finished it is never null.
            m_name = null!;
        }
 
        void IDeserializationCallback.OnDeserialization(object? sender)
        {
            OnDeserialized();
        }
 
        [OnDeserialized]
        private void OnDeserialized(StreamingContext ctx)
        {
            OnDeserialized();
        }
 
        private void OnDeserialized()
        {
            // If we didn't have a name, use the LCID
            if (m_name == null)
            {
                // From whidbey, didn't have a name
                m_name = CultureInfo.GetCultureInfo(culture)._name;
            }
            else
            {
                InitSort(CultureInfo.GetCultureInfo(m_name));
            }
        }
 
        [OnSerializing]
        private void OnSerializing(StreamingContext ctx)
        {
            // This is merely for serialization compatibility with Whidbey/Orcas, it can go away when we don't want that compat any more.
            culture = CultureInfo.GetCultureInfo(Name).LCID; // This is the lcid of the constructing culture (still have to dereference to get target sort)
            Debug.Assert(m_name != null, "CompareInfo.OnSerializing - expected m_name to be set already");
        }
 
        /// <summary>
        ///  Returns the name of the culture (well actually, of the sort).
        ///  Very important for providing a non-LCID way of identifying
        ///  what the sort is.
        ///
        ///  Note that this name isn't dereferenced in case the CompareInfo is a different locale
        ///  which is consistent with the behaviors of earlier versions.  (so if you ask for a sort
        ///  and the locale's changed behavior, then you'll get changed behavior, which is like
        ///  what happens for a version update)
        /// </summary>
        public string Name
        {
            get
            {
                Debug.Assert(m_name != null, "CompareInfo.Name Expected _name to be set");
                if (m_name == "zh-CHT" || m_name == "zh-CHS")
                {
                    return m_name;
                }
 
                return _sortName;
            }
        }
 
        /// <summary>
        /// Compares the two strings with the given options.  Returns 0 if the
        /// two strings are equal, a number less than 0 if string1 is less
        /// than string2, and a number greater than 0 if string1 is greater
        /// than string2.
        /// </summary>
        public int Compare(string? string1, string? string2)
        {
            return Compare(string1, string2, CompareOptions.None);
        }
 
        public int Compare(string? string1, string? string2, CompareOptions options)
        {
            int retVal;
 
            // Our paradigm is that null sorts less than any other string and
            // that two nulls sort as equal.
 
            if (string1 == null)
            {
                retVal = (string2 == null) ? 0 : -1;
                goto CheckOptionsAndReturn;
            }
            if (string2 == null)
            {
                retVal = 1;
                goto CheckOptionsAndReturn;
            }
 
            return Compare(string1.AsSpan(), string2.AsSpan(), options);
 
        CheckOptionsAndReturn:
 
            // If we're short-circuiting the globalization logic, we still need to check that
            // the provided options were valid.
 
            CheckCompareOptionsForCompare(options);
            return retVal;
        }
 
        internal int CompareOptionIgnoreCase(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2) =>
             GlobalizationMode.Invariant ?
                InvariantModeCasing.CompareStringIgnoreCase(ref MemoryMarshal.GetReference(string1), string1.Length, ref MemoryMarshal.GetReference(string2), string2.Length) :
                CompareStringCore(string1, string2, CompareOptions.IgnoreCase);
 
        /// <summary>
        /// Compares the specified regions of the two strings with the given
        /// options.
        /// Returns 0 if the two strings are equal, a number less than 0 if
        /// string1 is less than string2, and a number greater than 0 if
        /// string1 is greater than string2.
        /// </summary>
        public int Compare(string? string1, int offset1, int length1, string? string2, int offset2, int length2)
        {
            return Compare(string1, offset1, length1, string2, offset2, length2, CompareOptions.None);
        }
 
        public int Compare(string? string1, int offset1, string? string2, int offset2, CompareOptions options)
        {
            return Compare(string1, offset1, string1 == null ? 0 : string1.Length - offset1,
                           string2, offset2, string2 == null ? 0 : string2.Length - offset2, options);
        }
 
        public int Compare(string? string1, int offset1, string? string2, int offset2)
        {
            return Compare(string1, offset1, string2, offset2, CompareOptions.None);
        }
 
        public int Compare(string? string1, int offset1, int length1, string? string2, int offset2, int length2, CompareOptions options)
        {
            ReadOnlySpan<char> span1 = default;
            ReadOnlySpan<char> span2 = default;
 
            if (string1 == null)
            {
                if (offset1 != 0 || length1 != 0)
                {
                    goto BoundsCheckError;
                }
            }
            else if (!string1.TryGetSpan(offset1, length1, out span1))
            {
                goto BoundsCheckError;
            }
 
            if (string2 == null)
            {
                if (offset2 != 0 || length2 != 0)
                {
                    goto BoundsCheckError;
                }
            }
            else if (!string2.TryGetSpan(offset2, length2, out span2))
            {
                goto BoundsCheckError;
            }
 
            // At this point both string1 and string2 have been bounds-checked.
 
            int retVal;
 
            // Our paradigm is that null sorts less than any other string and
            // that two nulls sort as equal.
 
            if (string1 == null)
            {
                retVal = (string2 == null) ? 0 : -1;
                goto CheckOptionsAndReturn;
            }
            if (string2 == null)
            {
                retVal = 1;
                goto CheckOptionsAndReturn;
            }
 
            // At this point we know both string1 and string2 weren't null,
            // though they may have been empty.
 
            Debug.Assert(!Unsafe.IsNullRef(ref MemoryMarshal.GetReference(span1)));
            Debug.Assert(!Unsafe.IsNullRef(ref MemoryMarshal.GetReference(span2)));
 
            return Compare(span1, span2, options);
 
        CheckOptionsAndReturn:
 
            // If we're short-circuiting the globalization logic, we still need to check that
            // the provided options were valid.
 
            CheckCompareOptionsForCompare(options);
            return retVal;
 
        BoundsCheckError:
 
            // We know a bounds check error occurred. Now we just need to figure
            // out the correct error message to surface.
 
            ArgumentOutOfRangeException.ThrowIfNegative(length1);
            ArgumentOutOfRangeException.ThrowIfNegative(length2);
 
            ArgumentOutOfRangeException.ThrowIfNegative(offset1);
            ArgumentOutOfRangeException.ThrowIfNegative(offset2);
 
            if (offset1 > (string1 == null ? 0 : string1.Length) - length1)
            {
                throw new ArgumentOutOfRangeException(nameof(string1), SR.ArgumentOutOfRange_OffsetLength);
            }
 
            Debug.Assert(offset2 > (string2 == null ? 0 : string2.Length) - length2);
            throw new ArgumentOutOfRangeException(nameof(string2), SR.ArgumentOutOfRange_OffsetLength);
        }
 
        /// <summary>
        /// Compares two strings.
        /// </summary>
        /// <param name="string1">The first string to compare.</param>
        /// <param name="string2">The second string to compare.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the comparison.</param>
        /// <returns>
        /// Zero if <paramref name="string1"/> and <paramref name="string2"/> are equal;
        /// or a negative value if <paramref name="string1"/> sorts before <paramref name="string2"/>;
        /// or a positive value if <paramref name="string1"/> sorts after <paramref name="string2"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        public int Compare(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options = CompareOptions.None)
        {
            if (string1 == string2) // referential equality + length
            {
                CheckCompareOptionsForCompare(options);
                return 0;
            }
 
            if ((options & ValidCompareMaskOffFlags) == 0)
            {
                // Common case: caller is attempting to perform linguistic comparison.
                // Pass the flags down to NLS or ICU unless we're running in invariant
                // mode, at which point we normalize the flags to Ordinal[IgnoreCase].
 
                if (!GlobalizationMode.Invariant)
                {
                    return CompareStringCore(string1, string2, options);
                }
 
                if ((options & CompareOptions.IgnoreCase) == 0)
                {
                    return string1.SequenceCompareTo(string2);
                }
 
                return Ordinal.CompareStringIgnoreCase(ref MemoryMarshal.GetReference(string1), string1.Length, ref MemoryMarshal.GetReference(string2), string2.Length);
            }
            else
            {
                // Less common case: caller is attempting to perform non-linguistic comparison,
                // or an invalid combination of flags was supplied.
 
                if (options == CompareOptions.Ordinal)
                {
                    return string1.SequenceCompareTo(string2);
                }
 
                if (options == CompareOptions.OrdinalIgnoreCase)
                {
                    return Ordinal.CompareStringIgnoreCase(ref MemoryMarshal.GetReference(string1), string1.Length, ref MemoryMarshal.GetReference(string2), string2.Length);
                }
 
                ThrowCompareOptionsCheckFailed(options);
 
                return -1; // make the compiler happy;
            }
        }
 
        // Checks that 'CompareOptions' is valid for a call to Compare, throwing the appropriate
        // exception if the check fails.
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        [StackTraceHidden]
        private static void CheckCompareOptionsForCompare(CompareOptions options)
        {
            // Any combination of defined CompareOptions flags is valid, except for
            // Ordinal and OrdinalIgnoreCase, which may only be used in isolation.
 
            if ((options & ValidCompareMaskOffFlags) != 0)
            {
                if (options != CompareOptions.Ordinal && options != CompareOptions.OrdinalIgnoreCase)
                {
                    ThrowCompareOptionsCheckFailed(options);
                }
            }
        }
 
        [DoesNotReturn]
        [StackTraceHidden]
        private static void ThrowCompareOptionsCheckFailed(CompareOptions options)
        {
            throw new ArgumentException(
                paramName: nameof(options),
                message: ((options & CompareOptions.Ordinal) != 0) ? SR.Argument_CompareOptionOrdinal : SR.Argument_InvalidFlag);
        }
 
        private unsafe int CompareStringCore(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options) =>
            GlobalizationMode.UseNls ?
                NlsCompareString(string1, string2, options) :
#if TARGET_BROWSER
            GlobalizationMode.Hybrid ?
                JsCompareString(string1, string2, options) :
#elif TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS
            GlobalizationMode.Hybrid ?
                CompareStringNative(string1, string2, options) :
#endif
                IcuCompareString(string1, string2, options);
 
        /// <summary>
        /// Determines whether prefix is a prefix of string.  If prefix equals
        /// string.Empty, true is returned.
        /// </summary>
        public bool IsPrefix(string source, string prefix, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
            if (prefix == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.prefix);
            }
 
            return IsPrefix(source.AsSpan(), prefix.AsSpan(), options);
        }
 
        /// <summary>
        /// Determines whether a string starts with a specific prefix.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="prefix">The prefix to attempt to match at the start of <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the match.</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="prefix"/> occurs at the start of <paramref name="source"/>;
        /// otherwise, <see langword="false"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        public unsafe bool IsPrefix(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options = CompareOptions.None)
        {
            // The empty string is trivially a prefix of every other string. For compat with
            // earlier versions of the Framework we'll early-exit here before validating the
            // 'options' argument.
 
            if (prefix.IsEmpty)
            {
                return true;
            }
 
            if ((options & ValidIndexMaskOffFlags) == 0)
            {
                // Common case: caller is attempting to perform a linguistic search.
                // Pass the flags down to NLS or ICU unless we're running in invariant
                // mode, at which point we normalize the flags to Ordinal[IgnoreCase].
 
                if (!GlobalizationMode.Invariant)
                {
                    return StartsWithCore(source, prefix, options, matchLengthPtr: null);
                }
 
                if ((options & CompareOptions.IgnoreCase) == 0)
                {
                    return source.StartsWith(prefix);
                }
 
                return source.StartsWithOrdinalIgnoreCase(prefix);
            }
            else
            {
                // Less common case: caller is attempting to perform non-linguistic comparison,
                // or an invalid combination of flags was supplied.
 
                if (options == CompareOptions.Ordinal)
                {
                    return source.StartsWith(prefix);
                }
 
                if (options == CompareOptions.OrdinalIgnoreCase)
                {
                    return source.StartsWithOrdinalIgnoreCase(prefix);
                }
 
                ThrowCompareOptionsCheckFailed(options);
 
                return false; // make the compiler happy;
            }
        }
 
        /// <summary>
        /// Determines whether a string starts with a specific prefix.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="prefix">The prefix to attempt to match at the start of <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the match.</param>
        /// <param name="matchLength">When this method returns, contains the number of characters of
        /// <paramref name="source"/> that matched the desired prefix. This may be different than the
        /// length of <paramref name="prefix"/> if a linguistic comparison is performed. Set to 0
        /// if the prefix did not match.</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="prefix"/> occurs at the start of <paramref name="source"/>;
        /// otherwise, <see langword="false"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        /// <remarks>
        /// This method has greater overhead than other <see cref="IsPrefix"/> overloads which don't
        /// take a <paramref name="matchLength"/> argument. Call this overload only if you require
        /// the match length information.
        /// </remarks>
        public unsafe bool IsPrefix(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options, out int matchLength)
        {
            bool matched;
 
            if (GlobalizationMode.Invariant || prefix.IsEmpty || (options & ValidIndexMaskOffFlags) != 0)
            {
                // Non-linguistic (ordinal) comparison requested, or options are invalid.
                // Delegate to other overload, which validates options and throws on failure.
                // If success, non-linguistic matches will always preserve prefix length.
 
                matched = IsPrefix(source, prefix, options);
                matchLength = (matched) ? prefix.Length : 0;
            }
            else
            {
                // Linguistic comparison requested and we don't need to special-case any args.
#if TARGET_BROWSER || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS
                if (GlobalizationMode.Hybrid)
                {
                    throw new PlatformNotSupportedException(SR.PlatformNotSupported_HybridGlobalizationWithMatchLength);
                }
#endif
                int tempMatchLength = 0;
                matched = StartsWithCore(source, prefix, options, &tempMatchLength);
                matchLength = tempMatchLength;
            }
 
            return matched;
        }
 
        private unsafe bool StartsWithCore(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options, int* matchLengthPtr) =>
            GlobalizationMode.UseNls ?
                NlsStartsWith(source, prefix, options, matchLengthPtr) :
#if TARGET_BROWSER
            GlobalizationMode.Hybrid ?
                JsStartsWith(source, prefix, options) :
#endif
                IcuStartsWith(source, prefix, options, matchLengthPtr);
 
        public bool IsPrefix(string source, string prefix)
        {
            return IsPrefix(source, prefix, CompareOptions.None);
        }
 
        /// <summary>
        /// Determines whether suffix is a suffix of string.  If suffix equals
        /// string.Empty, true is returned.
        /// </summary>
        public bool IsSuffix(string source, string suffix, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
            if (suffix == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.suffix);
            }
 
            return IsSuffix(source.AsSpan(), suffix.AsSpan(), options);
        }
 
        /// <summary>
        /// Determines whether a string ends with a specific suffix.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="suffix">The suffix to attempt to match at the end of <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the match.</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="suffix"/> occurs at the end of <paramref name="source"/>;
        /// otherwise, <see langword="false"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        public unsafe bool IsSuffix(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options = CompareOptions.None)
        {
            // The empty string is trivially a suffix of every other string. For compat with
            // earlier versions of the Framework we'll early-exit here before validating the
            // 'options' argument.
 
            if (suffix.IsEmpty)
            {
                return true;
            }
 
            if ((options & ValidIndexMaskOffFlags) == 0)
            {
                // Common case: caller is attempting to perform a linguistic search.
                // Pass the flags down to NLS or ICU unless we're running in invariant
                // mode, at which point we normalize the flags to Ordinal[IgnoreCase].
 
                if (!GlobalizationMode.Invariant)
                {
                    return EndsWithCore(source, suffix, options, matchLengthPtr: null);
                }
 
                if ((options & CompareOptions.IgnoreCase) == 0)
                {
                    return source.EndsWith(suffix);
                }
 
                return source.EndsWithOrdinalIgnoreCase(suffix);
            }
            else
            {
                // Less common case: caller is attempting to perform non-linguistic comparison,
                // or an invalid combination of flags was supplied.
 
                if (options == CompareOptions.Ordinal)
                {
                    return source.EndsWith(suffix);
                }
 
                if (options == CompareOptions.OrdinalIgnoreCase)
                {
                    return source.EndsWithOrdinalIgnoreCase(suffix);
                }
 
                ThrowCompareOptionsCheckFailed(options);
 
                return false; // make the compiler happy;
            }
        }
 
        /// <summary>
        /// Determines whether a string ends with a specific suffix.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="suffix">The suffix to attempt to match at the end of <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the match.</param>
        /// <param name="matchLength">When this method returns, contains the number of characters of
        /// <paramref name="source"/> that matched the desired suffix. This may be different than the
        /// length of <paramref name="suffix"/> if a linguistic comparison is performed. Set to 0
        /// if the suffix did not match.</param>
        /// <returns>
        /// <see langword="true"/> if <paramref name="suffix"/> occurs at the end of <paramref name="source"/>;
        /// otherwise, <see langword="false"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        /// <remarks>
        /// This method has greater overhead than other <see cref="IsSuffix"/> overloads which don't
        /// take a <paramref name="matchLength"/> argument. Call this overload only if you require
        /// the match length information.
        /// </remarks>
        public unsafe bool IsSuffix(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options, out int matchLength)
        {
            bool matched;
 
            if (GlobalizationMode.Invariant || suffix.IsEmpty || (options & ValidIndexMaskOffFlags) != 0)
            {
                // Non-linguistic (ordinal) comparison requested, or options are invalid.
                // Delegate to other overload, which validates options and throws on failure.
                // If success, non-linguistic matches will always preserve prefix length.
 
                matched = IsSuffix(source, suffix, options);
                matchLength = (matched) ? suffix.Length : 0;
            }
            else
            {
                // Linguistic comparison requested and we don't need to special-case any args.
#if TARGET_BROWSER || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS
                if (GlobalizationMode.Hybrid)
                {
                    throw new PlatformNotSupportedException(SR.PlatformNotSupported_HybridGlobalizationWithMatchLength);
                }
#endif
                int tempMatchLength = 0;
                matched = EndsWithCore(source, suffix, options, &tempMatchLength);
                matchLength = tempMatchLength;
            }
 
            return matched;
        }
 
        public bool IsSuffix(string source, string suffix)
        {
            return IsSuffix(source, suffix, CompareOptions.None);
        }
 
        private unsafe bool EndsWithCore(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options, int* matchLengthPtr) =>
            GlobalizationMode.UseNls ?
                NlsEndsWith(source, suffix, options, matchLengthPtr) :
#if TARGET_BROWSER
            GlobalizationMode.Hybrid ?
                JsEndsWith(source, suffix, options) :
#endif
                IcuEndsWith(source, suffix, options, matchLengthPtr);
 
        /// <summary>
        /// Returns the first index where value is found in string.  The
        /// search starts from startIndex and ends at endIndex.  Returns -1 if
        /// the specified value is not found.  If value equals string.Empty,
        /// startIndex is returned.  Throws IndexOutOfRange if startIndex or
        /// endIndex is less than zero or greater than the length of string.
        /// Throws ArgumentException if value (as a string) is null.
        /// </summary>
        public int IndexOf(string source, char value)
        {
            return IndexOf(source, value, CompareOptions.None);
        }
 
        public int IndexOf(string source, string value)
        {
            return IndexOf(source, value, CompareOptions.None);
        }
 
        public int IndexOf(string source, char value, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
 
            return IndexOf(source, new ReadOnlySpan<char>(in value), options);
        }
 
        public int IndexOf(string source, string value, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
            if (value == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value);
            }
 
            return IndexOf(source.AsSpan(), value.AsSpan(), options);
        }
 
        public int IndexOf(string source, char value, int startIndex)
        {
            return IndexOf(source, value, startIndex, CompareOptions.None);
        }
 
        public int IndexOf(string source, string value, int startIndex)
        {
            return IndexOf(source, value, startIndex, CompareOptions.None);
        }
 
        public int IndexOf(string source, char value, int startIndex, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
 
            return IndexOf(source, value, startIndex, source.Length - startIndex, options);
 
        }
 
        public int IndexOf(string source, string value, int startIndex, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
 
            return IndexOf(source, value, startIndex, source.Length - startIndex, options);
        }
 
        public int IndexOf(string source, char value, int startIndex, int count)
        {
            return IndexOf(source, value, startIndex, count, CompareOptions.None);
        }
 
        public int IndexOf(string source, string value, int startIndex, int count)
        {
            return IndexOf(source, value, startIndex, count, CompareOptions.None);
        }
 
        public unsafe int IndexOf(string source, char value, int startIndex, int count, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
 
            if (!source.TryGetSpan(startIndex, count, out ReadOnlySpan<char> sourceSpan))
            {
                // Bounds check failed - figure out exactly what went wrong so that we can
                // surface the correct argument exception.
 
                if ((uint)startIndex > (uint)source.Length)
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_IndexMustBeLessOrEqual);
                }
                else
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_Count);
                }
            }
 
            int result = IndexOf(sourceSpan, new ReadOnlySpan<char>(in value), options);
            if (result >= 0)
            {
                result += startIndex;
            }
            return result;
        }
 
        public unsafe int IndexOf(string source, string value, int startIndex, int count, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
            if (value == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value);
            }
 
            if (!source.TryGetSpan(startIndex, count, out ReadOnlySpan<char> sourceSpan))
            {
                // Bounds check failed - figure out exactly what went wrong so that we can
                // surface the correct argument exception.
 
                if ((uint)startIndex > (uint)source.Length)
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_IndexMustBeLessOrEqual);
                }
                else
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_Count);
                }
            }
 
            int result = IndexOf(sourceSpan, value, options);
            if (result >= 0)
            {
                result += startIndex;
            }
            return result;
        }
 
        /// <summary>
        /// Searches for the first occurrence of a substring within a source string.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="value">The substring to locate within <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
        /// <returns>
        /// The zero-based index into <paramref name="source"/> where the substring <paramref name="value"/>
        /// first appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        public unsafe int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options = CompareOptions.None)
        {
            if ((options & ValidIndexMaskOffFlags) == 0)
            {
                // Common case: caller is attempting to perform a linguistic search.
                // Pass the flags down to NLS or ICU unless we're running in invariant
                // mode, at which point we normalize the flags to Ordinal[IgnoreCase].
 
                if (!GlobalizationMode.Invariant)
                {
                    if (value.IsEmpty)
                    {
                        return 0; // Empty target string trivially occurs at index 0 of every search space.
                    }
                    else
                    {
                        return IndexOfCore(source, value, options, matchLengthPtr: null, fromBeginning: true);
                    }
                }
 
                if ((options & CompareOptions.IgnoreCase) == 0)
                {
                    return source.IndexOf(value);
                }
 
                return Ordinal.IndexOfOrdinalIgnoreCase(source, value);
            }
            else
            {
                // Less common case: caller is attempting to perform non-linguistic comparison,
                // or an invalid combination of flags was supplied.
 
                if (options == CompareOptions.Ordinal)
                {
                    return source.IndexOf(value);
                }
 
                if (options == CompareOptions.OrdinalIgnoreCase)
                {
                    return Ordinal.IndexOfOrdinalIgnoreCase(source, value);
                }
 
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options);
 
                return -1; // make the compiler happy;
            }
        }
 
        /// <summary>
        /// Searches for the first occurrence of a substring within a source string.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="value">The substring to locate within <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
        /// <param name="matchLength">When this method returns, contains the number of characters of
        /// <paramref name="source"/> that matched the desired value. This may be different than the
        /// length of <paramref name="value"/> if a linguistic comparison is performed. Set to 0
        /// if <paramref name="value"/> is not found within <paramref name="source"/>.</param>
        /// <returns>
        /// The zero-based index into <paramref name="source"/> where the substring <paramref name="value"/>
        /// first appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        /// <remarks>
        /// This method has greater overhead than other <see cref="IndexOf"/> overloads which don't
        /// take a <paramref name="matchLength"/> argument. Call this overload only if you require
        /// the match length information.
        /// </remarks>
        public unsafe int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options, out int matchLength)
        {
            int tempMatchLength;
            int retVal = IndexOf(source, value, &tempMatchLength, options, fromBeginning: true);
            matchLength = tempMatchLength;
            return retVal;
        }
 
        /// <summary>
        /// Searches for the first occurrence of a <see cref="Rune"/> within a source string.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="value">The <see cref="Rune"/> to locate within <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
        /// <returns>
        /// The zero-based index into <paramref name="source"/> where <paramref name="value"/>
        /// first appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        public int IndexOf(ReadOnlySpan<char> source, Rune value, CompareOptions options = CompareOptions.None)
        {
            Span<char> valueAsUtf16 = stackalloc char[Rune.MaxUtf16CharsPerRune];
            int charCount = value.EncodeToUtf16(valueAsUtf16);
            return IndexOf(source, valueAsUtf16.Slice(0, charCount), options);
        }
 
        /// <summary>
        /// IndexOf overload used when the caller needs the length of the matching substring.
        /// Caller needs to ensure <paramref name="matchLengthPtr"/> is non-null and points
        /// to a valid address. This method will validate <paramref name="options"/>.
        /// </summary>
        private unsafe int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, int* matchLengthPtr, CompareOptions options, bool fromBeginning)
        {
            Debug.Assert(matchLengthPtr != null);
            *matchLengthPtr = 0;
 
            int retVal = 0;
 
            if ((options & ValidIndexMaskOffFlags) == 0)
            {
                // Common case: caller is attempting to perform a linguistic search.
                // Pass the flags down to NLS or ICU unless we're running in invariant
                // mode, at which point we normalize the flags to Ordinal[IgnoreCase].
 
                if (!GlobalizationMode.Invariant)
                {
                    if (value.IsEmpty)
                    {
                        // empty target substring trivially occurs at beginning / end of search space
                        return (fromBeginning) ? 0 : source.Length;
                    }
                    else
                    {
                        return IndexOfCore(source, value, options, matchLengthPtr, fromBeginning);
                    }
                }
 
                if ((options & CompareOptions.IgnoreCase) == 0)
                {
                    retVal = (fromBeginning) ? source.IndexOf(value) : source.LastIndexOf(value);
                }
                else
                {
                    retVal = fromBeginning ? Ordinal.IndexOfOrdinalIgnoreCase(source, value) : Ordinal.LastIndexOfOrdinalIgnoreCase(source, value);
                }
            }
            else
            {
                // Less common case: caller is attempting to perform non-linguistic comparison,
                // or an invalid combination of flags was supplied.
 
                if (options == CompareOptions.Ordinal)
                {
                    retVal = (fromBeginning) ? source.IndexOf(value) : source.LastIndexOf(value);
                }
                else if (options == CompareOptions.OrdinalIgnoreCase)
                {
                    retVal = fromBeginning ? Ordinal.IndexOfOrdinalIgnoreCase(source, value) : Ordinal.LastIndexOfOrdinalIgnoreCase(source, value);
                }
                else
                {
                    ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options);
                }
            }
 
            // Both Ordinal and OrdinalIgnoreCase match by individual code points in a non-linguistic manner.
            // Non-BMP code points will never match BMP code points, so given UTF-16 inputs the match length
            // will always be equivalent to the target string length.
 
            if (retVal >= 0)
            {
                *matchLengthPtr = value.Length;
            }
            return retVal;
        }
 
        private unsafe int IndexOfCore(ReadOnlySpan<char> source, ReadOnlySpan<char> target, CompareOptions options, int* matchLengthPtr, bool fromBeginning) =>
            GlobalizationMode.UseNls ?
                NlsIndexOfCore(source, target, options, matchLengthPtr, fromBeginning) :
#if TARGET_BROWSER
            GlobalizationMode.Hybrid ?
                JsIndexOfCore(source, target, options, matchLengthPtr, fromBeginning) :
#endif
                IcuIndexOfCore(source, target, options, matchLengthPtr, fromBeginning);
 
        /// <summary>
        /// Returns the last index where value is found in string.  The
        /// search starts from startIndex and ends at endIndex.  Returns -1 if
        /// the specified value is not found.  If value equals string.Empty,
        /// endIndex is returned.  Throws IndexOutOfRange if startIndex or
        /// endIndex is less than zero or greater than the length of string.
        /// Throws ArgumentException if value (as a string) is null.
        /// </summary>
        public int LastIndexOf(string source, char value)
        {
            return LastIndexOf(source, value, CompareOptions.None);
        }
 
        public int LastIndexOf(string source, string value)
        {
            return LastIndexOf(source, value, CompareOptions.None);
        }
 
        public int LastIndexOf(string source, char value, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
 
            return LastIndexOf(source, new ReadOnlySpan<char>(in value), options);
        }
 
        public int LastIndexOf(string source, string value, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
            if (value == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value);
            }
 
            return LastIndexOf(source.AsSpan(), value.AsSpan(), options);
        }
 
        public int LastIndexOf(string source, char value, int startIndex)
        {
            return LastIndexOf(source, value, startIndex, startIndex + 1, CompareOptions.None);
        }
 
        public int LastIndexOf(string source, string value, int startIndex)
        {
            return LastIndexOf(source, value, startIndex, startIndex + 1, CompareOptions.None);
        }
 
        public int LastIndexOf(string source, char value, int startIndex, CompareOptions options)
        {
            return LastIndexOf(source, value, startIndex, startIndex + 1, options);
        }
 
        public int LastIndexOf(string source, string value, int startIndex, CompareOptions options)
        {
            return LastIndexOf(source, value, startIndex, startIndex + 1, options);
        }
 
        public int LastIndexOf(string source, char value, int startIndex, int count)
        {
            return LastIndexOf(source, value, startIndex, count, CompareOptions.None);
        }
 
        public int LastIndexOf(string source, string value, int startIndex, int count)
        {
            return LastIndexOf(source, value, startIndex, count, CompareOptions.None);
        }
 
        public int LastIndexOf(string source, char value, int startIndex, int count, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
 
        TryAgain:
 
            // Previous versions of the Framework special-cased empty 'source' to allow startIndex = -1 or startIndex = 0,
            // ignoring 'count' and short-circuiting the entire operation. We'll silently fix up the 'count' parameter
            // if this occurs.
            //
            // See the comments just before string.IndexOf(string) for more information on how these computations are
            // performed.
 
            if ((uint)startIndex >= (uint)source.Length)
            {
                if (startIndex == -1 && source.Length == 0)
                {
                    count = 0; // normalize
                }
                else if (startIndex == source.Length)
                {
                    // The caller likely had an off-by-one error when invoking the API. The Framework has historically
                    // allowed for this and tried to fix up the parameters, so we'll continue to do so for compat.
 
                    startIndex--;
                    if (count > 0)
                    {
                        count--;
                    }
 
                    goto TryAgain; // guaranteed never to loop more than once
                }
                else
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_IndexMustBeLess);
                }
            }
 
            startIndex = startIndex - count + 1; // this will be the actual index where we begin our search
 
            if (!source.TryGetSpan(startIndex, count, out ReadOnlySpan<char> sourceSpan))
            {
                ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count();
            }
 
            int retVal = LastIndexOf(sourceSpan, new ReadOnlySpan<char>(in value), options);
            if (retVal >= 0)
            {
                retVal += startIndex;
            }
            return retVal;
        }
 
        public int LastIndexOf(string source, string value, int startIndex, int count, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
            if (value == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value);
            }
 
        TryAgain:
 
            // Previous versions of the Framework special-cased empty 'source' to allow startIndex = -1 or startIndex = 0,
            // ignoring 'count' and short-circuiting the entire operation. We'll silently fix up the 'count' parameter
            // if this occurs.
            //
            // See the comments just before string.IndexOf(string) for more information on how these computations are
            // performed.
 
            if ((uint)startIndex >= (uint)source.Length)
            {
                if (startIndex == -1 && source.Length == 0)
                {
                    count = 0; // normalize
                }
                else if (startIndex == source.Length)
                {
                    // The caller likely had an off-by-one error when invoking the API. The Framework has historically
                    // allowed for this and tried to fix up the parameters, so we'll continue to do so for compat.
 
                    startIndex--;
                    if (count > 0)
                    {
                        count--;
                    }
 
                    goto TryAgain; // guaranteed never to loop more than once
                }
                else
                {
                    ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_IndexMustBeLess);
                }
            }
 
            startIndex = startIndex - count + 1; // this will be the actual index where we begin our search
 
            if (!source.TryGetSpan(startIndex, count, out ReadOnlySpan<char> sourceSpan))
            {
                ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count();
            }
 
            int retVal = LastIndexOf(sourceSpan, value, options);
            if (retVal >= 0)
            {
                retVal += startIndex;
            }
            return retVal;
        }
 
        /// <summary>
        /// Searches for the last occurrence of a substring within a source string.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="value">The substring to locate within <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
        /// <returns>
        /// The zero-based index into <paramref name="source"/> where the substring <paramref name="value"/>
        /// last appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        public unsafe int LastIndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options = CompareOptions.None)
        {
            if ((options & ValidIndexMaskOffFlags) == 0)
            {
                // Common case: caller is attempting to perform a linguistic search.
                // Pass the flags down to NLS or ICU unless we're running in invariant
                // mode, at which point we normalize the flags to Ordinal[IgnoreCase].
 
                if (!GlobalizationMode.Invariant)
                {
                    if (value.IsEmpty)
                    {
                        return source.Length; // Empty target string trivially occurs at the last index of every search space.
                    }
                    else
                    {
                        return IndexOfCore(source, value, options, matchLengthPtr: null, fromBeginning: false);
                    }
                }
 
                if ((options & CompareOptions.IgnoreCase) == 0)
                {
                    return source.LastIndexOf(value);
                }
 
                return Ordinal.LastIndexOfOrdinalIgnoreCase(source, value);
            }
            else
            {
                // Less common case: caller is attempting to perform non-linguistic comparison,
                // or an invalid combination of flags was supplied.
 
                if (options == CompareOptions.Ordinal)
                {
                    return source.LastIndexOf(value);
                }
 
                if (options == CompareOptions.OrdinalIgnoreCase)
                {
                    return Ordinal.LastIndexOfOrdinalIgnoreCase(source, value);
                }
 
                throw new ArgumentException(paramName: nameof(options), message: SR.Argument_InvalidFlag);
            }
        }
 
        /// <summary>
        /// Searches for the last occurrence of a substring within a source string.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="value">The substring to locate within <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
        /// <param name="matchLength">When this method returns, contains the number of characters of
        /// <paramref name="source"/> that matched the desired value. This may be different than the
        /// length of <paramref name="value"/> if a linguistic comparison is performed. Set to 0
        /// if <paramref name="value"/> is not found within <paramref name="source"/>.</param>
        /// <returns>
        /// The zero-based index into <paramref name="source"/> where the substring <paramref name="value"/>
        /// last appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        /// <remarks>
        /// This method has greater overhead than other <see cref="IndexOf"/> overloads which don't
        /// take a <paramref name="matchLength"/> argument. Call this overload only if you require
        /// the match length information.
        /// </remarks>
        public unsafe int LastIndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options, out int matchLength)
        {
            int tempMatchLength;
            int retVal = IndexOf(source, value, &tempMatchLength, options, fromBeginning: false);
            matchLength = tempMatchLength;
            return retVal;
        }
 
        /// <summary>
        /// Searches for the last occurrence of a <see cref="Rune"/> within a source string.
        /// </summary>
        /// <param name="source">The string to search within.</param>
        /// <param name="value">The <see cref="Rune"/> to locate within <paramref name="source"/>.</param>
        /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
        /// <returns>
        /// The zero-based index into <paramref name="source"/> where <paramref name="value"/>
        /// last appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
        /// </returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported combination of flags.
        /// </exception>
        public unsafe int LastIndexOf(ReadOnlySpan<char> source, Rune value, CompareOptions options = CompareOptions.None)
        {
            Span<char> valueAsUtf16 = stackalloc char[Rune.MaxUtf16CharsPerRune];
            int charCount = value.EncodeToUtf16(valueAsUtf16);
            return LastIndexOf(source, valueAsUtf16.Slice(0, charCount), options);
        }
 
        /// <summary>
        /// Gets the SortKey for the given string with the given options.
        /// </summary>
        public SortKey GetSortKey(string source, CompareOptions options)
        {
            if (GlobalizationMode.Invariant)
            {
                return InvariantCreateSortKey(source, options);
            }
 
            return CreateSortKeyCore(source, options);
        }
 
        public SortKey GetSortKey(string source)
        {
            if (GlobalizationMode.Invariant)
            {
                return InvariantCreateSortKey(source, CompareOptions.None);
            }
 
            return CreateSortKeyCore(source, CompareOptions.None);
        }
 
        private SortKey CreateSortKeyCore(string source, CompareOptions options) =>
            GlobalizationMode.UseNls ?
                NlsCreateSortKey(source, options) :
                IcuCreateSortKey(source, options);
 
        /// <summary>
        /// Computes a sort key over the specified input.
        /// </summary>
        /// <param name="source">The text over which to compute the sort key.</param>
        /// <param name="destination">The buffer into which to write the resulting sort key bytes.</param>
        /// <param name="options">The <see cref="CompareOptions"/> used for computing the sort key.</param>
        /// <returns>The number of bytes written to <paramref name="destination"/>.</returns>
        /// <remarks>
        /// Use <see cref="GetSortKeyLength"/> to query the required size of <paramref name="destination"/>.
        /// It is acceptable to provide a larger-than-necessary output buffer to this method.
        /// </remarks>
        /// <exception cref="ArgumentException">
        /// <paramref name="destination"/> is too small to contain the resulting sort key;
        /// or <paramref name="options"/> contains an unsupported flag;
        /// or <paramref name="source"/> cannot be processed using the desired <see cref="CompareOptions"/>
        /// under the current <see cref="CompareInfo"/>.
        /// </exception>
        public int GetSortKey(ReadOnlySpan<char> source, Span<byte> destination, CompareOptions options = CompareOptions.None)
        {
            if ((options & ValidCompareMaskOffFlags) != 0)
            {
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options);
            }
 
            if (GlobalizationMode.Invariant)
            {
                return InvariantGetSortKey(source, destination, options);
            }
            else
            {
                return GetSortKeyCore(source, destination, options);
            }
        }
 
        private int GetSortKeyCore(ReadOnlySpan<char> source, Span<byte> destination, CompareOptions options) =>
            GlobalizationMode.UseNls ?
                NlsGetSortKey(source, destination, options) :
                IcuGetSortKey(source, destination, options);
 
        /// <summary>
        /// Returns the length (in bytes) of the sort key that would be produced from the specified input.
        /// </summary>
        /// <param name="source">The text over which to compute the sort key.</param>
        /// <param name="options">The <see cref="CompareOptions"/> used for computing the sort key.</param>
        /// <returns>The length (in bytes) of the sort key.</returns>
        /// <exception cref="ArgumentException">
        /// <paramref name="options"/> contains an unsupported flag;
        /// or <paramref name="source"/> cannot be processed using the desired <see cref="CompareOptions"/>
        /// under the current <see cref="CompareInfo"/>.
        /// </exception>
        public int GetSortKeyLength(ReadOnlySpan<char> source, CompareOptions options = CompareOptions.None)
        {
            if ((options & ValidCompareMaskOffFlags) != 0)
            {
                ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options);
            }
 
            if (GlobalizationMode.Invariant)
            {
                return InvariantGetSortKeyLength(source, options);
            }
            else
            {
                return GetSortKeyLengthCore(source, options);
            }
        }
 
        private int GetSortKeyLengthCore(ReadOnlySpan<char> source, CompareOptions options) =>
            GlobalizationMode.UseNls ?
              NlsGetSortKeyLength(source, options) :
              IcuGetSortKeyLength(source, options);
 
        public override bool Equals([NotNullWhen(true)] object? value)
        {
            return value is CompareInfo otherCompareInfo
                && Name == otherCompareInfo.Name;
        }
 
        public override int GetHashCode() => Name.GetHashCode();
 
        /// <summary>
        /// This method performs the equivalent of of creating a Sortkey for a string from CompareInfo,
        /// then generates a randomized hashcode value from the sort key.
        ///
        /// The hash code is guaranteed to be the same for string A and B where A.Equals(B) is true and both
        /// the CompareInfo and the CompareOptions are the same. If two different CompareInfo objects
        /// treat the string the same way, this implementation will treat them differently (the same way that
        /// Sortkey does at the moment).
        /// </summary>
        public int GetHashCode(string source, CompareOptions options)
        {
            if (source == null)
            {
                ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
            }
 
            return GetHashCode(source.AsSpan(), options);
        }
 
        public int GetHashCode(ReadOnlySpan<char> source, CompareOptions options)
        {
            if ((options & ValidCompareMaskOffFlags) == 0)
            {
                // Common case: caller is attempting to get a linguistic sort key.
                // Pass the flags down to NLS or ICU unless we're running in invariant
                // mode, at which point we normalize the flags to Ordinal[IgnoreCase].
 
                if (!GlobalizationMode.Invariant)
                {
                    return GetHashCodeOfStringCore(source, options);
                }
 
                return InvariantGetHashCode(source, options);
            }
            else
            {
                // Less common case: caller is attempting to get a non-linguistic sort key,
                // or an invalid combination of flags was supplied.
 
                if (options == CompareOptions.Ordinal)
                {
                    return string.GetHashCode(source);
                }
 
                if (options == CompareOptions.OrdinalIgnoreCase)
                {
                    return string.GetHashCodeOrdinalIgnoreCase(source);
                }
 
                ThrowCompareOptionsCheckFailed(options);
 
                return -1; // make the compiler happy;
            }
        }
 
        private unsafe int GetHashCodeOfStringCore(ReadOnlySpan<char> source, CompareOptions options) =>
            GlobalizationMode.UseNls ?
                NlsGetHashCodeOfString(source, options) :
                IcuGetHashCodeOfString(source, options);
 
        public override string ToString() => "CompareInfo - " + Name;
 
        public SortVersion Version
        {
            get
            {
                if (m_SortVersion == null)
                {
                    if (GlobalizationMode.Invariant)
                    {
                        m_SortVersion = new SortVersion(0, CultureInfo.LOCALE_INVARIANT, new Guid(0, 0, 0, 0, 0, 0, 0,
                                                                        (byte)(CultureInfo.LOCALE_INVARIANT >> 24),
                                                                        (byte)((CultureInfo.LOCALE_INVARIANT & 0x00FF0000) >> 16),
                                                                        (byte)((CultureInfo.LOCALE_INVARIANT & 0x0000FF00) >> 8),
                                                                        (byte)(CultureInfo.LOCALE_INVARIANT & 0xFF)));
                    }
                    else
                    {
#if TARGET_BROWSER || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS
                if (GlobalizationMode.Hybrid)
                {
                    throw new PlatformNotSupportedException(GetPNSEText("SortVersion"));
                }
#endif
                        m_SortVersion = GlobalizationMode.UseNls ? NlsGetSortVersion() : IcuGetSortVersion();
                    }
                }
 
                return m_SortVersion;
            }
        }
 
        public int LCID => CultureInfo.GetCultureInfo(Name).LCID;
 
#if TARGET_BROWSER || TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS
        private static string GetPNSEText(string funcName) => SR.Format(SR.PlatformNotSupported_HybridGlobalization, funcName);
        private static string GetPNSEWithReason(string funcName, string reason) => SR.Format(SR.PlatformNotSupportedWithReason_HybridGlobalization, funcName, reason);
#endif
    }
}