// 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 | CompareOptions.NumericOrdering); // 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 unsafe 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 int CompareStringCore(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options) => GlobalizationMode.UseNls ? NlsCompareString(string1, string2, options) : #if 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_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) : 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_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) : 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 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 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 unsafe 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) : 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 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_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_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 } }