1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 ////////////////////////////////////////////////////////////////////////////
9 // Purpose: This class implements a set of methods for comparing
13 ////////////////////////////////////////////////////////////////////////////
15 using System.Reflection;
16 using System.Diagnostics;
17 using System.Runtime.InteropServices;
18 using System.Runtime.Serialization;
20 namespace System.Globalization
23 public enum CompareOptions
26 IgnoreCase = 0x00000001,
27 IgnoreNonSpace = 0x00000002,
28 IgnoreSymbols = 0x00000004,
29 IgnoreKanaType = 0x00000008, // ignore kanatype
30 IgnoreWidth = 0x00000010, // ignore width
31 OrdinalIgnoreCase = 0x10000000, // This flag can not be used with other flags.
32 StringSort = 0x20000000, // use string sort method
33 Ordinal = 0x40000000, // This flag can not be used with other flags.
37 [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
38 public partial class CompareInfo : IDeserializationCallback
40 // Mask used to check if IndexOf()/LastIndexOf()/IsPrefix()/IsPostfix() has the right flags.
41 private const CompareOptions ValidIndexMaskOffFlags =
42 ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
43 CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType);
45 // Mask used to check if Compare() has the right flags.
46 private const CompareOptions ValidCompareMaskOffFlags =
47 ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
48 CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.StringSort);
50 // Mask used to check if GetHashCodeOfString() has the right flags.
51 private const CompareOptions ValidHashCodeOfStringMaskOffFlags =
52 ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
53 CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType);
55 // Mask used to check if we have the right flags.
56 private const CompareOptions ValidSortkeyCtorMaskOffFlags =
57 ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
58 CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.StringSort);
60 // Cache the invariant CompareInfo
61 internal static readonly CompareInfo Invariant = CultureInfo.InvariantCulture.CompareInfo;
64 // CompareInfos have an interesting identity. They are attached to the locale that created them,
65 // ie: en-US would have an en-US sort. For haw-US (custom), then we serialize it as haw-US.
66 // The interesting part is that since haw-US doesn't have its own sort, it has to point at another
67 // locale, which is what SCOMPAREINFO does.
68 [OptionalField(VersionAdded = 2)]
69 private string m_name; // The name used to construct this CompareInfo. Do not rename (binary serialization)
72 private string _sortName; // The name that defines our behavior
74 [OptionalField(VersionAdded = 3)]
75 private SortVersion m_SortVersion; // Do not rename (binary serialization)
77 // _invariantMode is defined for the perf reason as accessing the instance field is faster than access the static property GlobalizationMode.Invariant
79 private readonly bool _invariantMode = GlobalizationMode.Invariant;
81 private int culture; // Do not rename (binary serialization). The fields sole purpose is to support Desktop serialization.
83 internal CompareInfo(CultureInfo culture)
85 m_name = culture._name;
89 /*=================================GetCompareInfo==========================
90 **Action: Get the CompareInfo constructed from the data table in the specified assembly for the specified culture.
91 ** Warning: The assembly versioning mechanism is dead!
92 **Returns: The CompareInfo for the specified culture.
94 ** culture the ID of the culture
95 ** assembly the assembly which contains the sorting table.
97 ** ArgumentNullException when the assembly is null
98 ** ArgumentException if culture is invalid.
99 ============================================================================*/
100 // Assembly constructor should be deprecated, we don't act on the assembly information any more
101 public static CompareInfo GetCompareInfo(int culture, Assembly assembly)
103 // Parameter checking.
104 if (assembly == null)
106 throw new ArgumentNullException(nameof(assembly));
108 if (assembly != typeof(Object).Module.Assembly)
110 throw new ArgumentException(SR.Argument_OnlyMscorlib);
113 return GetCompareInfo(culture);
116 /*=================================GetCompareInfo==========================
117 **Action: Get the CompareInfo constructed from the data table in the specified assembly for the specified culture.
118 ** The purpose of this method is to provide version for CompareInfo tables.
119 **Returns: The CompareInfo for the specified culture.
121 ** name the name of the culture
122 ** assembly the assembly which contains the sorting table.
124 ** ArgumentNullException when the assembly is null
125 ** ArgumentException if name is invalid.
126 ============================================================================*/
127 // Assembly constructor should be deprecated, we don't act on the assembly information any more
128 public static CompareInfo GetCompareInfo(string name, Assembly assembly)
130 if (name == null || assembly == null)
132 throw new ArgumentNullException(name == null ? nameof(name) : nameof(assembly));
135 if (assembly != typeof(Object).Module.Assembly)
137 throw new ArgumentException(SR.Argument_OnlyMscorlib);
140 return GetCompareInfo(name);
143 /*=================================GetCompareInfo==========================
144 **Action: Get the CompareInfo for the specified culture.
145 ** This method is provided for ease of integration with NLS-based software.
146 **Returns: The CompareInfo for the specified culture.
148 ** culture the ID of the culture.
150 ** ArgumentException if culture is invalid.
151 ============================================================================*/
152 // People really shouldn't be calling LCID versions, no custom support
153 public static CompareInfo GetCompareInfo(int culture)
155 if (CultureData.IsCustomCultureId(culture))
157 // Customized culture cannot be created by the LCID.
158 throw new ArgumentException(SR.Argument_CustomCultureCannotBePassedByNumber, nameof(culture));
161 return CultureInfo.GetCultureInfo(culture).CompareInfo;
164 /*=================================GetCompareInfo==========================
165 **Action: Get the CompareInfo for the specified culture.
166 **Returns: The CompareInfo for the specified culture.
168 ** name the name of the culture.
170 ** ArgumentException if name is invalid.
171 ============================================================================*/
173 public static CompareInfo GetCompareInfo(string name)
177 throw new ArgumentNullException(nameof(name));
180 return CultureInfo.GetCultureInfo(name).CompareInfo;
183 public static unsafe bool IsSortable(char ch)
185 if (GlobalizationMode.Invariant)
190 return IsSortable(pChar, 1);
193 public static unsafe bool IsSortable(string text)
197 // A null param is invalid here.
198 throw new ArgumentNullException(nameof(text));
201 if (text.Length == 0)
203 // A zero length string is not invalid, but it is also not sortable.
207 if (GlobalizationMode.Invariant)
212 fixed (char *pChar = text)
214 return IsSortable(pChar, text.Length);
219 private void OnDeserializing(StreamingContext ctx)
224 void IDeserializationCallback.OnDeserialization(object sender)
230 private void OnDeserialized(StreamingContext ctx)
235 private void OnDeserialized()
237 // If we didn't have a name, use the LCID
240 // From whidbey, didn't have a name
241 CultureInfo ci = CultureInfo.GetCultureInfo(this.culture);
246 InitSort(CultureInfo.GetCultureInfo(m_name));
251 private void OnSerializing(StreamingContext ctx)
253 // This is merely for serialization compatibility with Whidbey/Orcas, it can go away when we don't want that compat any more.
254 culture = CultureInfo.GetCultureInfo(this.Name).LCID; // This is the lcid of the constructing culture (still have to dereference to get target sort)
255 Debug.Assert(m_name != null, "CompareInfo.OnSerializing - expected m_name to be set already");
258 ///////////////////////////----- Name -----/////////////////////////////////
260 // Returns the name of the culture (well actually, of the sort).
261 // Very important for providing a non-LCID way of identifying
264 // Note that this name isn't dereferenced in case the CompareInfo is a different locale
265 // which is consistent with the behaviors of earlier versions. (so if you ask for a sort
266 // and the locale's changed behavior, then you'll get changed behavior, which is like
267 // what happens for a version update)
269 ////////////////////////////////////////////////////////////////////////
271 public virtual string Name
275 Debug.Assert(m_name != null, "CompareInfo.Name Expected _name to be set");
276 if (m_name == "zh-CHT" || m_name == "zh-CHS")
285 ////////////////////////////////////////////////////////////////////////
289 // Compares the two strings with the given options. Returns 0 if the
290 // two strings are equal, a number less than 0 if string1 is less
291 // than string2, and a number greater than 0 if string1 is greater
294 ////////////////////////////////////////////////////////////////////////
296 public virtual int Compare(string string1, string string2)
298 return (Compare(string1, string2, CompareOptions.None));
301 public virtual int Compare(string string1, string string2, CompareOptions options)
303 if (options == CompareOptions.OrdinalIgnoreCase)
305 return String.Compare(string1, string2, StringComparison.OrdinalIgnoreCase);
308 // Verify the options before we do any real comparison.
309 if ((options & CompareOptions.Ordinal) != 0)
311 if (options != CompareOptions.Ordinal)
313 throw new ArgumentException(SR.Argument_CompareOptionOrdinal, nameof(options));
316 return String.CompareOrdinal(string1, string2);
319 if ((options & ValidCompareMaskOffFlags) != 0)
321 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
324 //Our paradigm is that null sorts less than any other string and
325 //that two nulls sort as equal.
332 return (-1); // null < non-null
336 return (1); // non-null > null
341 if ((options & CompareOptions.IgnoreCase) != 0)
342 return CompareOrdinalIgnoreCase(string1, 0, string1.Length, string2, 0, string2.Length);
344 return String.CompareOrdinal(string1, string2);
347 return CompareString(string1.AsSpan(), string2.AsSpan(), options);
350 // TODO https://github.com/dotnet/coreclr/issues/13827:
351 // This method shouldn't be necessary, as we should be able to just use the overload
352 // that takes two spans. But due to this issue, that's adding significant overhead.
353 internal int Compare(ReadOnlySpan<char> string1, string string2, CompareOptions options)
355 if (options == CompareOptions.OrdinalIgnoreCase)
357 return CompareOrdinalIgnoreCase(string1, string2.AsSpan());
360 // Verify the options before we do any real comparison.
361 if ((options & CompareOptions.Ordinal) != 0)
363 if (options != CompareOptions.Ordinal)
365 throw new ArgumentException(SR.Argument_CompareOptionOrdinal, nameof(options));
368 return string.CompareOrdinal(string1, string2.AsSpan());
371 if ((options & ValidCompareMaskOffFlags) != 0)
373 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
376 // null sorts less than any other string.
384 return (options & CompareOptions.IgnoreCase) != 0 ?
385 CompareOrdinalIgnoreCase(string1, string2.AsSpan()) :
386 string.CompareOrdinal(string1, string2.AsSpan());
389 return CompareString(string1, string2, options);
392 internal virtual int CompareOptionNone(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2)
394 return _invariantMode ?
395 string.CompareOrdinal(string1, string2) :
396 CompareString(string1, string2, CompareOptions.None);
399 internal virtual int CompareOptionIgnoreCase(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2)
401 return _invariantMode ?
402 CompareOrdinalIgnoreCase(string1, string2) :
403 CompareString(string1, string2, CompareOptions.IgnoreCase);
406 ////////////////////////////////////////////////////////////////////////
410 // Compares the specified regions of the two strings with the given
412 // Returns 0 if the two strings are equal, a number less than 0 if
413 // string1 is less than string2, and a number greater than 0 if
414 // string1 is greater than string2.
416 ////////////////////////////////////////////////////////////////////////
419 public virtual int Compare(string string1, int offset1, int length1, string string2, int offset2, int length2)
421 return Compare(string1, offset1, length1, string2, offset2, length2, 0);
425 public virtual int Compare(string string1, int offset1, string string2, int offset2, CompareOptions options)
427 return Compare(string1, offset1, string1 == null ? 0 : string1.Length - offset1,
428 string2, offset2, string2 == null ? 0 : string2.Length - offset2, options);
432 public virtual int Compare(string string1, int offset1, string string2, int offset2)
434 return Compare(string1, offset1, string2, offset2, 0);
438 public virtual int Compare(string string1, int offset1, int length1, string string2, int offset2, int length2, CompareOptions options)
440 if (options == CompareOptions.OrdinalIgnoreCase)
442 int result = String.Compare(string1, offset1, string2, offset2, length1 < length2 ? length1 : length2, StringComparison.OrdinalIgnoreCase);
443 if ((length1 != length2) && result == 0)
444 return (length1 > length2 ? 1 : -1);
449 if (length1 < 0 || length2 < 0)
451 throw new ArgumentOutOfRangeException((length1 < 0) ? nameof(length1) : nameof(length2), SR.ArgumentOutOfRange_NeedPosNum);
453 if (offset1 < 0 || offset2 < 0)
455 throw new ArgumentOutOfRangeException((offset1 < 0) ? nameof(offset1) : nameof(offset2), SR.ArgumentOutOfRange_NeedPosNum);
457 if (offset1 > (string1 == null ? 0 : string1.Length) - length1)
459 throw new ArgumentOutOfRangeException(nameof(string1), SR.ArgumentOutOfRange_OffsetLength);
461 if (offset2 > (string2 == null ? 0 : string2.Length) - length2)
463 throw new ArgumentOutOfRangeException(nameof(string2), SR.ArgumentOutOfRange_OffsetLength);
465 if ((options & CompareOptions.Ordinal) != 0)
467 if (options != CompareOptions.Ordinal)
469 throw new ArgumentException(SR.Argument_CompareOptionOrdinal,
473 else if ((options & ValidCompareMaskOffFlags) != 0)
475 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
479 // Check for the null case.
494 if (options == CompareOptions.Ordinal)
496 return CompareOrdinal(string1, offset1, length1,
497 string2, offset2, length2);
502 if ((options & CompareOptions.IgnoreCase) != 0)
503 return CompareOrdinalIgnoreCase(string1, offset1, length1, string2, offset2, length2);
505 return CompareOrdinal(string1, offset1, length1, string2, offset2, length2);
508 return CompareString(
509 string1.AsSpan(offset1, length1),
510 string2.AsSpan(offset2, length2),
514 private static int CompareOrdinal(string string1, int offset1, int length1, string string2, int offset2, int length2)
516 int result = String.CompareOrdinal(string1, offset1, string2, offset2,
517 (length1 < length2 ? length1 : length2));
518 if ((length1 != length2) && result == 0)
520 return (length1 > length2 ? 1 : -1);
526 // CompareOrdinalIgnoreCase compare two string ordinally with ignoring the case.
527 // it assumes the strings are Ascii string till we hit non Ascii character in strA or strB and then we continue the comparison by
530 internal static int CompareOrdinalIgnoreCase(string strA, int indexA, int lengthA, string strB, int indexB, int lengthB)
532 Debug.Assert(indexA + lengthA <= strA.Length);
533 Debug.Assert(indexB + lengthB <= strB.Length);
534 return CompareOrdinalIgnoreCase(strA.AsSpan(indexA, lengthA), strB.AsSpan(indexB, lengthB));
537 internal static unsafe int CompareOrdinalIgnoreCase(ReadOnlySpan<char> strA, ReadOnlySpan<char> strB)
539 int length = Math.Min(strA.Length, strB.Length);
542 fixed (char* ap = &MemoryMarshal.GetReference(strA))
543 fixed (char* bp = &MemoryMarshal.GetReference(strB))
548 // in InvariantMode we support all range and not only the ascii characters.
549 char maxChar = (char) (GlobalizationMode.Invariant ? 0xFFFF : 0x7F);
551 while (length != 0 && (*a <= maxChar) && (*b <= maxChar))
563 // uppercase both chars - notice that we need just one compare per char
564 if ((uint)(charA - 'a') <= 'z' - 'a') charA -= 0x20;
565 if ((uint)(charB - 'a') <= 'z' - 'a') charB -= 0x20;
567 // Return the (case-insensitive) difference between them.
569 return charA - charB;
577 return strA.Length - strB.Length;
579 Debug.Assert(!GlobalizationMode.Invariant);
583 return CompareStringOrdinalIgnoreCase(a, strA.Length - range, b, strB.Length - range);
587 ////////////////////////////////////////////////////////////////////////
591 // Determines whether prefix is a prefix of string. If prefix equals
592 // String.Empty, true is returned.
594 ////////////////////////////////////////////////////////////////////////
595 public virtual bool IsPrefix(string source, string prefix, CompareOptions options)
597 if (source == null || prefix == null)
599 throw new ArgumentNullException((source == null ? nameof(source) : nameof(prefix)),
600 SR.ArgumentNull_String);
603 if (prefix.Length == 0)
608 if (source.Length == 0)
613 if (options == CompareOptions.OrdinalIgnoreCase)
615 return source.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
618 if (options == CompareOptions.Ordinal)
620 return source.StartsWith(prefix, StringComparison.Ordinal);
623 if ((options & ValidIndexMaskOffFlags) != 0)
625 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
630 return source.StartsWith(prefix, (options & CompareOptions.IgnoreCase) != 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
633 return StartsWith(source, prefix, options);
636 internal bool IsPrefix(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
638 Debug.Assert(prefix.Length != 0);
639 Debug.Assert(source.Length != 0);
640 Debug.Assert((options & ValidIndexMaskOffFlags) == 0);
641 Debug.Assert(!_invariantMode);
642 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
644 return StartsWith(source, prefix, options);
647 public virtual bool IsPrefix(string source, string prefix)
649 return (IsPrefix(source, prefix, 0));
652 ////////////////////////////////////////////////////////////////////////
656 // Determines whether suffix is a suffix of string. If suffix equals
657 // String.Empty, true is returned.
659 ////////////////////////////////////////////////////////////////////////
660 public virtual bool IsSuffix(string source, string suffix, CompareOptions options)
662 if (source == null || suffix == null)
664 throw new ArgumentNullException((source == null ? nameof(source) : nameof(suffix)),
665 SR.ArgumentNull_String);
668 if (suffix.Length == 0)
673 if (source.Length == 0)
678 if (options == CompareOptions.OrdinalIgnoreCase)
680 return source.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
683 if (options == CompareOptions.Ordinal)
685 return source.EndsWith(suffix, StringComparison.Ordinal);
688 if ((options & ValidIndexMaskOffFlags) != 0)
690 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
695 return source.EndsWith(suffix, (options & CompareOptions.IgnoreCase) != 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
698 return EndsWith(source, suffix, options);
701 internal bool IsSuffix(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options)
703 Debug.Assert(suffix.Length != 0);
704 Debug.Assert(source.Length != 0);
705 Debug.Assert((options & ValidIndexMaskOffFlags) == 0);
706 Debug.Assert(!_invariantMode);
707 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
709 return EndsWith(source, suffix, options);
713 public virtual bool IsSuffix(string source, string suffix)
715 return (IsSuffix(source, suffix, 0));
718 ////////////////////////////////////////////////////////////////////////
722 // Returns the first index where value is found in string. The
723 // search starts from startIndex and ends at endIndex. Returns -1 if
724 // the specified value is not found. If value equals String.Empty,
725 // startIndex is returned. Throws IndexOutOfRange if startIndex or
726 // endIndex is less than zero or greater than the length of string.
727 // Throws ArgumentException if value is null.
729 ////////////////////////////////////////////////////////////////////////
732 public virtual int IndexOf(string source, char value)
735 throw new ArgumentNullException(nameof(source));
737 return IndexOf(source, value, 0, source.Length, CompareOptions.None);
741 public virtual int IndexOf(string source, string value)
744 throw new ArgumentNullException(nameof(source));
746 return IndexOf(source, value, 0, source.Length, CompareOptions.None);
750 public virtual int IndexOf(string source, char value, CompareOptions options)
753 throw new ArgumentNullException(nameof(source));
755 return IndexOf(source, value, 0, source.Length, options);
759 public virtual int IndexOf(string source, string value, CompareOptions options)
762 throw new ArgumentNullException(nameof(source));
764 return IndexOf(source, value, 0, source.Length, options);
767 public virtual int IndexOf(string source, char value, int startIndex)
770 throw new ArgumentNullException(nameof(source));
772 return IndexOf(source, value, startIndex, source.Length - startIndex, CompareOptions.None);
775 public virtual int IndexOf(string source, string value, int startIndex)
778 throw new ArgumentNullException(nameof(source));
780 return IndexOf(source, value, startIndex, source.Length - startIndex, CompareOptions.None);
783 public virtual int IndexOf(string source, char value, int startIndex, CompareOptions options)
786 throw new ArgumentNullException(nameof(source));
788 return IndexOf(source, value, startIndex, source.Length - startIndex, options);
792 public virtual int IndexOf(string source, string value, int startIndex, CompareOptions options)
795 throw new ArgumentNullException(nameof(source));
797 return IndexOf(source, value, startIndex, source.Length - startIndex, options);
801 public virtual int IndexOf(string source, char value, int startIndex, int count)
803 return IndexOf(source, value, startIndex, count, CompareOptions.None);
807 public virtual int IndexOf(string source, string value, int startIndex, int count)
809 return IndexOf(source, value, startIndex, count, CompareOptions.None);
812 public unsafe virtual int IndexOf(string source, char value, int startIndex, int count, CompareOptions options)
816 throw new ArgumentNullException(nameof(source));
818 if (startIndex < 0 || startIndex > source.Length)
819 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
821 if (count < 0 || startIndex > source.Length - count)
822 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
824 if (source.Length == 0)
829 if (options == CompareOptions.OrdinalIgnoreCase)
831 return source.IndexOf(value.ToString(), startIndex, count, StringComparison.OrdinalIgnoreCase);
834 // Validate CompareOptions
835 // Ordinal can't be selected with other flags
836 if ((options & ValidIndexMaskOffFlags) != 0 && (options != CompareOptions.Ordinal))
837 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
840 return IndexOfOrdinal(source, new string(value, 1), startIndex, count, ignoreCase: (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
842 return IndexOfCore(source, new string(value, 1), startIndex, count, options, null);
845 public unsafe virtual int IndexOf(string source, string value, int startIndex, int count, CompareOptions options)
849 throw new ArgumentNullException(nameof(source));
851 throw new ArgumentNullException(nameof(value));
853 if (startIndex > source.Length)
855 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
858 // In Everett we used to return -1 for empty string even if startIndex is negative number so we keeping same behavior here.
859 // We return 0 if both source and value are empty strings for Everett compatibility too.
860 if (source.Length == 0)
862 if (value.Length == 0)
871 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
874 if (count < 0 || startIndex > source.Length - count)
875 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
877 if (options == CompareOptions.OrdinalIgnoreCase)
879 return IndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
882 // Validate CompareOptions
883 // Ordinal can't be selected with other flags
884 if ((options & ValidIndexMaskOffFlags) != 0 && (options != CompareOptions.Ordinal))
885 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
888 return IndexOfOrdinal(source, value, startIndex, count, ignoreCase: (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
890 return IndexOfCore(source, value, startIndex, count, options, null);
893 internal virtual int IndexOfOrdinal(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool ignoreCase)
895 Debug.Assert(!_invariantMode);
896 return IndexOfOrdinalCore(source, value, ignoreCase);
899 internal unsafe virtual int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options)
901 Debug.Assert(!_invariantMode);
902 return IndexOfCore(source, value, options, null);
905 // The following IndexOf overload is mainly used by String.Replace. This overload assumes the parameters are already validated
906 // and the caller is passing a valid matchLengthPtr pointer.
907 internal unsafe int IndexOf(string source, string value, int startIndex, int count, CompareOptions options, int* matchLengthPtr)
909 Debug.Assert(source != null);
910 Debug.Assert(value != null);
911 Debug.Assert(startIndex >= 0);
912 Debug.Assert(matchLengthPtr != null);
915 if (source.Length == 0)
917 if (value.Length == 0)
924 if (startIndex >= source.Length)
929 if (options == CompareOptions.OrdinalIgnoreCase)
931 int res = IndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
934 *matchLengthPtr = value.Length;
941 int res = IndexOfOrdinal(source, value, startIndex, count, ignoreCase: (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
944 *matchLengthPtr = value.Length;
949 return IndexOfCore(source, value, startIndex, count, options, matchLengthPtr);
952 internal int IndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
956 return InvariantIndexOf(source, value, startIndex, count, ignoreCase);
959 return IndexOfOrdinalCore(source, value, startIndex, count, ignoreCase);
962 ////////////////////////////////////////////////////////////////////////
966 // Returns the last index where value is found in string. The
967 // search starts from startIndex and ends at endIndex. Returns -1 if
968 // the specified value is not found. If value equals String.Empty,
969 // endIndex is returned. Throws IndexOutOfRange if startIndex or
970 // endIndex is less than zero or greater than the length of string.
971 // Throws ArgumentException if value is null.
973 ////////////////////////////////////////////////////////////////////////
976 public virtual int LastIndexOf(String source, char value)
979 throw new ArgumentNullException(nameof(source));
981 // Can't start at negative index, so make sure we check for the length == 0 case.
982 return LastIndexOf(source, value, source.Length - 1, source.Length, CompareOptions.None);
986 public virtual int LastIndexOf(string source, string value)
989 throw new ArgumentNullException(nameof(source));
991 // Can't start at negative index, so make sure we check for the length == 0 case.
992 return LastIndexOf(source, value, source.Length - 1,
993 source.Length, CompareOptions.None);
997 public virtual int LastIndexOf(string source, char value, CompareOptions options)
1000 throw new ArgumentNullException(nameof(source));
1002 // Can't start at negative index, so make sure we check for the length == 0 case.
1003 return LastIndexOf(source, value, source.Length - 1,
1004 source.Length, options);
1007 public virtual int LastIndexOf(string source, string value, CompareOptions options)
1010 throw new ArgumentNullException(nameof(source));
1012 // Can't start at negative index, so make sure we check for the length == 0 case.
1013 return LastIndexOf(source, value, source.Length - 1, source.Length, options);
1016 public virtual int LastIndexOf(string source, char value, int startIndex)
1018 return LastIndexOf(source, value, startIndex, startIndex + 1, CompareOptions.None);
1022 public virtual int LastIndexOf(string source, string value, int startIndex)
1024 return LastIndexOf(source, value, startIndex, startIndex + 1, CompareOptions.None);
1027 public virtual int LastIndexOf(string source, char value, int startIndex, CompareOptions options)
1029 return LastIndexOf(source, value, startIndex, startIndex + 1, options);
1033 public virtual int LastIndexOf(string source, string value, int startIndex, CompareOptions options)
1035 return LastIndexOf(source, value, startIndex, startIndex + 1, options);
1039 public virtual int LastIndexOf(string source, char value, int startIndex, int count)
1041 return LastIndexOf(source, value, startIndex, count, CompareOptions.None);
1045 public virtual int LastIndexOf(string source, string value, int startIndex, int count)
1047 return LastIndexOf(source, value, startIndex, count, CompareOptions.None);
1051 public virtual int LastIndexOf(string source, char value, int startIndex, int count, CompareOptions options)
1055 throw new ArgumentNullException(nameof(source));
1057 // Validate CompareOptions
1058 // Ordinal can't be selected with other flags
1059 if ((options & ValidIndexMaskOffFlags) != 0 &&
1060 (options != CompareOptions.Ordinal) &&
1061 (options != CompareOptions.OrdinalIgnoreCase))
1062 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
1064 // Special case for 0 length input strings
1065 if (source.Length == 0 && (startIndex == -1 || startIndex == 0))
1068 // Make sure we're not out of range
1069 if (startIndex < 0 || startIndex > source.Length)
1070 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
1072 // Make sure that we allow startIndex == source.Length
1073 if (startIndex == source.Length)
1080 // 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
1081 if (count < 0 || startIndex - count + 1 < 0)
1082 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
1084 if (options == CompareOptions.OrdinalIgnoreCase)
1086 return source.LastIndexOf(value.ToString(), startIndex, count, StringComparison.OrdinalIgnoreCase);
1090 return InvariantLastIndexOf(source, new string(value, 1), startIndex, count, (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
1092 return LastIndexOfCore(source, value.ToString(), startIndex, count, options);
1096 public virtual int LastIndexOf(string source, string value, int startIndex, int count, CompareOptions options)
1100 throw new ArgumentNullException(nameof(source));
1102 throw new ArgumentNullException(nameof(value));
1104 // Validate CompareOptions
1105 // Ordinal can't be selected with other flags
1106 if ((options & ValidIndexMaskOffFlags) != 0 &&
1107 (options != CompareOptions.Ordinal) &&
1108 (options != CompareOptions.OrdinalIgnoreCase))
1109 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
1111 // Special case for 0 length input strings
1112 if (source.Length == 0 && (startIndex == -1 || startIndex == 0))
1113 return (value.Length == 0) ? 0 : -1;
1115 // Make sure we're not out of range
1116 if (startIndex < 0 || startIndex > source.Length)
1117 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
1119 // Make sure that we allow startIndex == source.Length
1120 if (startIndex == source.Length)
1126 // If we are looking for nothing, just return 0
1127 if (value.Length == 0 && count >= 0 && startIndex - count + 1 >= 0)
1131 // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
1132 if (count < 0 || startIndex - count + 1 < 0)
1133 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
1135 if (options == CompareOptions.OrdinalIgnoreCase)
1137 return LastIndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
1141 return InvariantLastIndexOf(source, value, startIndex, count, (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
1143 return LastIndexOfCore(source, value, startIndex, count, options);
1146 internal int LastIndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
1150 return InvariantLastIndexOf(source, value, startIndex, count, ignoreCase);
1153 return LastIndexOfOrdinalCore(source, value, startIndex, count, ignoreCase);
1156 ////////////////////////////////////////////////////////////////////////
1160 // Gets the SortKey for the given string with the given options.
1162 ////////////////////////////////////////////////////////////////////////
1163 public virtual SortKey GetSortKey(string source, CompareOptions options)
1166 return InvariantCreateSortKey(source, options);
1168 return CreateSortKey(source, options);
1172 public virtual SortKey GetSortKey(string source)
1175 return InvariantCreateSortKey(source, CompareOptions.None);
1177 return CreateSortKey(source, CompareOptions.None);
1180 ////////////////////////////////////////////////////////////////////////
1184 // Implements Object.Equals(). Returns a boolean indicating whether
1185 // or not object refers to the same CompareInfo as the current
1188 ////////////////////////////////////////////////////////////////////////
1191 public override bool Equals(Object value)
1193 CompareInfo that = value as CompareInfo;
1197 return this.Name == that.Name;
1204 ////////////////////////////////////////////////////////////////////////
1208 // Implements Object.GetHashCode(). Returns the hash code for the
1209 // CompareInfo. The hash code is guaranteed to be the same for
1210 // CompareInfo A and B where A.Equals(B) is true.
1212 ////////////////////////////////////////////////////////////////////////
1215 public override int GetHashCode()
1217 return (this.Name.GetHashCode());
1221 ////////////////////////////////////////////////////////////////////////
1223 // GetHashCodeOfString
1225 // This internal method allows a method that allows the equivalent of creating a Sortkey for a
1226 // string from CompareInfo, and generate a hashcode value from it. It is not very convenient
1227 // to use this method as is and it creates an unnecessary Sortkey object that will be GC'ed.
1229 // The hash code is guaranteed to be the same for string A and B where A.Equals(B) is true and both
1230 // the CompareInfo and the CompareOptions are the same. If two different CompareInfo objects
1231 // treat the string the same way, this implementation will treat them differently (the same way that
1232 // Sortkey does at the moment).
1234 // This method will never be made public itself, but public consumers of it could be created, e.g.:
1236 // string.GetHashCode(CultureInfo)
1237 // string.GetHashCode(CompareInfo)
1238 // string.GetHashCode(CultureInfo, CompareOptions)
1239 // string.GetHashCode(CompareInfo, CompareOptions)
1242 // (the methods above that take a CultureInfo would use CultureInfo.CompareInfo)
1244 ////////////////////////////////////////////////////////////////////////
1245 internal int GetHashCodeOfString(string source, CompareOptions options)
1248 // Parameter validation
1252 throw new ArgumentNullException(nameof(source));
1255 if ((options & ValidHashCodeOfStringMaskOffFlags) != 0)
1257 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
1260 return GetHashCodeOfStringCore(source, options);
1263 public virtual int GetHashCode(string source, CompareOptions options)
1267 throw new ArgumentNullException(nameof(source));
1270 if (options == CompareOptions.Ordinal)
1272 return source.GetHashCode();
1275 if (options == CompareOptions.OrdinalIgnoreCase)
1277 return TextInfo.GetHashCodeOrdinalIgnoreCase(source);
1281 // GetHashCodeOfString does more parameters validation. basically will throw when
1282 // having Ordinal, OrdinalIgnoreCase and StringSort
1285 return GetHashCodeOfString(source, options);
1288 ////////////////////////////////////////////////////////////////////////
1292 // Implements Object.ToString(). Returns a string describing the
1295 ////////////////////////////////////////////////////////////////////////
1296 public override string ToString()
1298 return ("CompareInfo - " + this.Name);
1301 public SortVersion Version
1305 if (m_SortVersion == null)
1309 m_SortVersion = new SortVersion(0, CultureInfo.LOCALE_INVARIANT, new Guid(0, 0, 0, 0, 0, 0, 0,
1310 (byte) (CultureInfo.LOCALE_INVARIANT >> 24),
1311 (byte) ((CultureInfo.LOCALE_INVARIANT & 0x00FF0000) >> 16),
1312 (byte) ((CultureInfo.LOCALE_INVARIANT & 0x0000FF00) >> 8),
1313 (byte) (CultureInfo.LOCALE_INVARIANT & 0xFF)));
1317 m_SortVersion = GetSortVersion();
1321 return m_SortVersion;
1329 return CultureInfo.GetCultureInfo(Name).LCID;