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 // TODO https://github.com/dotnet/corefx/issues/21395: Expose this publicly?
393 internal virtual int Compare(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options)
395 if (options == CompareOptions.OrdinalIgnoreCase)
397 return CompareOrdinalIgnoreCase(string1, string2);
400 // Verify the options before we do any real comparison.
401 if ((options & CompareOptions.Ordinal) != 0)
403 if (options != CompareOptions.Ordinal)
405 throw new ArgumentException(SR.Argument_CompareOptionOrdinal, nameof(options));
408 return string.CompareOrdinal(string1, string2);
411 if ((options & ValidCompareMaskOffFlags) != 0)
413 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
418 return (options & CompareOptions.IgnoreCase) != 0 ?
419 CompareOrdinalIgnoreCase(string1, string2) :
420 string.CompareOrdinal(string1, string2);
423 return CompareString(string1, string2, options);
426 ////////////////////////////////////////////////////////////////////////
430 // Compares the specified regions of the two strings with the given
432 // Returns 0 if the two strings are equal, a number less than 0 if
433 // string1 is less than string2, and a number greater than 0 if
434 // string1 is greater than string2.
436 ////////////////////////////////////////////////////////////////////////
439 public virtual int Compare(string string1, int offset1, int length1, string string2, int offset2, int length2)
441 return Compare(string1, offset1, length1, string2, offset2, length2, 0);
445 public virtual int Compare(string string1, int offset1, string string2, int offset2, CompareOptions options)
447 return Compare(string1, offset1, string1 == null ? 0 : string1.Length - offset1,
448 string2, offset2, string2 == null ? 0 : string2.Length - offset2, options);
452 public virtual int Compare(string string1, int offset1, string string2, int offset2)
454 return Compare(string1, offset1, string2, offset2, 0);
458 public virtual int Compare(string string1, int offset1, int length1, string string2, int offset2, int length2, CompareOptions options)
460 if (options == CompareOptions.OrdinalIgnoreCase)
462 int result = String.Compare(string1, offset1, string2, offset2, length1 < length2 ? length1 : length2, StringComparison.OrdinalIgnoreCase);
463 if ((length1 != length2) && result == 0)
464 return (length1 > length2 ? 1 : -1);
469 if (length1 < 0 || length2 < 0)
471 throw new ArgumentOutOfRangeException((length1 < 0) ? nameof(length1) : nameof(length2), SR.ArgumentOutOfRange_NeedPosNum);
473 if (offset1 < 0 || offset2 < 0)
475 throw new ArgumentOutOfRangeException((offset1 < 0) ? nameof(offset1) : nameof(offset2), SR.ArgumentOutOfRange_NeedPosNum);
477 if (offset1 > (string1 == null ? 0 : string1.Length) - length1)
479 throw new ArgumentOutOfRangeException(nameof(string1), SR.ArgumentOutOfRange_OffsetLength);
481 if (offset2 > (string2 == null ? 0 : string2.Length) - length2)
483 throw new ArgumentOutOfRangeException(nameof(string2), SR.ArgumentOutOfRange_OffsetLength);
485 if ((options & CompareOptions.Ordinal) != 0)
487 if (options != CompareOptions.Ordinal)
489 throw new ArgumentException(SR.Argument_CompareOptionOrdinal,
493 else if ((options & ValidCompareMaskOffFlags) != 0)
495 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
499 // Check for the null case.
514 if (options == CompareOptions.Ordinal)
516 return CompareOrdinal(string1, offset1, length1,
517 string2, offset2, length2);
522 if ((options & CompareOptions.IgnoreCase) != 0)
523 return CompareOrdinalIgnoreCase(string1, offset1, length1, string2, offset2, length2);
525 return CompareOrdinal(string1, offset1, length1, string2, offset2, length2);
528 return CompareString(
529 string1.AsSpan().Slice(offset1, length1),
530 string2.AsSpan().Slice(offset2, length2),
534 private static int CompareOrdinal(string string1, int offset1, int length1, string string2, int offset2, int length2)
536 int result = String.CompareOrdinal(string1, offset1, string2, offset2,
537 (length1 < length2 ? length1 : length2));
538 if ((length1 != length2) && result == 0)
540 return (length1 > length2 ? 1 : -1);
546 // CompareOrdinalIgnoreCase compare two string ordinally with ignoring the case.
547 // it assumes the strings are Ascii string till we hit non Ascii character in strA or strB and then we continue the comparison by
550 internal static int CompareOrdinalIgnoreCase(string strA, int indexA, int lengthA, string strB, int indexB, int lengthB)
552 Debug.Assert(indexA + lengthA <= strA.Length);
553 Debug.Assert(indexB + lengthB <= strB.Length);
554 return CompareOrdinalIgnoreCase(strA.AsSpan().Slice(indexA, lengthA), strB.AsSpan().Slice(indexB, lengthB));
557 internal static unsafe int CompareOrdinalIgnoreCase(ReadOnlySpan<char> strA, ReadOnlySpan<char> strB)
559 int length = Math.Min(strA.Length, strB.Length);
562 fixed (char* ap = &MemoryMarshal.GetReference(strA))
563 fixed (char* bp = &MemoryMarshal.GetReference(strB))
568 // in InvariantMode we support all range and not only the ascii characters.
569 char maxChar = (char) (GlobalizationMode.Invariant ? 0xFFFF : 0x80);
571 while (length != 0 && (*a <= maxChar) && (*b <= maxChar))
583 // uppercase both chars - notice that we need just one compare per char
584 if ((uint)(charA - 'a') <= 'z' - 'a') charA -= 0x20;
585 if ((uint)(charB - 'a') <= 'z' - 'a') charB -= 0x20;
587 // Return the (case-insensitive) difference between them.
589 return charA - charB;
597 return strA.Length - strB.Length;
599 Debug.Assert(!GlobalizationMode.Invariant);
603 return CompareStringOrdinalIgnoreCase(a, strA.Length - range, b, strB.Length - range);
607 ////////////////////////////////////////////////////////////////////////
611 // Determines whether prefix is a prefix of string. If prefix equals
612 // String.Empty, true is returned.
614 ////////////////////////////////////////////////////////////////////////
615 public virtual bool IsPrefix(string source, string prefix, CompareOptions options)
617 if (source == null || prefix == null)
619 throw new ArgumentNullException((source == null ? nameof(source) : nameof(prefix)),
620 SR.ArgumentNull_String);
623 if (prefix.Length == 0)
628 if (source.Length == 0)
633 if (options == CompareOptions.OrdinalIgnoreCase)
635 return source.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
638 if (options == CompareOptions.Ordinal)
640 return source.StartsWith(prefix, StringComparison.Ordinal);
643 if ((options & ValidIndexMaskOffFlags) != 0)
645 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
650 return source.StartsWith(prefix, (options & CompareOptions.IgnoreCase) != 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
653 return StartsWith(source, prefix, options);
656 internal bool IsPrefix(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
658 Debug.Assert(prefix.Length != 0);
659 Debug.Assert(source.Length != 0);
660 Debug.Assert((options & ValidIndexMaskOffFlags) == 0);
661 Debug.Assert(!_invariantMode);
662 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
664 return StartsWith(source, prefix, options);
667 public virtual bool IsPrefix(string source, string prefix)
669 return (IsPrefix(source, prefix, 0));
672 ////////////////////////////////////////////////////////////////////////
676 // Determines whether suffix is a suffix of string. If suffix equals
677 // String.Empty, true is returned.
679 ////////////////////////////////////////////////////////////////////////
680 public virtual bool IsSuffix(string source, string suffix, CompareOptions options)
682 if (source == null || suffix == null)
684 throw new ArgumentNullException((source == null ? nameof(source) : nameof(suffix)),
685 SR.ArgumentNull_String);
688 if (suffix.Length == 0)
693 if (source.Length == 0)
698 if (options == CompareOptions.OrdinalIgnoreCase)
700 return source.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
703 if (options == CompareOptions.Ordinal)
705 return source.EndsWith(suffix, StringComparison.Ordinal);
708 if ((options & ValidIndexMaskOffFlags) != 0)
710 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
715 return source.EndsWith(suffix, (options & CompareOptions.IgnoreCase) != 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
718 return EndsWith(source, suffix, options);
721 internal bool IsSuffix(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options)
723 Debug.Assert(suffix.Length != 0);
724 Debug.Assert(source.Length != 0);
725 Debug.Assert((options & ValidIndexMaskOffFlags) == 0);
726 Debug.Assert(!_invariantMode);
727 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
729 return EndsWith(source, suffix, options);
733 public virtual bool IsSuffix(string source, string suffix)
735 return (IsSuffix(source, suffix, 0));
738 ////////////////////////////////////////////////////////////////////////
742 // Returns the first index where value is found in string. The
743 // search starts from startIndex and ends at endIndex. Returns -1 if
744 // the specified value is not found. If value equals String.Empty,
745 // startIndex is returned. Throws IndexOutOfRange if startIndex or
746 // endIndex is less than zero or greater than the length of string.
747 // Throws ArgumentException if value is null.
749 ////////////////////////////////////////////////////////////////////////
752 public virtual int IndexOf(string source, char value)
755 throw new ArgumentNullException(nameof(source));
757 return IndexOf(source, value, 0, source.Length, CompareOptions.None);
761 public virtual int IndexOf(string source, string value)
764 throw new ArgumentNullException(nameof(source));
766 return IndexOf(source, value, 0, source.Length, CompareOptions.None);
770 public virtual int IndexOf(string source, char value, CompareOptions options)
773 throw new ArgumentNullException(nameof(source));
775 return IndexOf(source, value, 0, source.Length, options);
779 public virtual int IndexOf(string source, string value, CompareOptions options)
782 throw new ArgumentNullException(nameof(source));
784 return IndexOf(source, value, 0, source.Length, options);
787 public virtual int IndexOf(string source, char value, int startIndex)
790 throw new ArgumentNullException(nameof(source));
792 return IndexOf(source, value, startIndex, source.Length - startIndex, CompareOptions.None);
795 public virtual int IndexOf(string source, string value, int startIndex)
798 throw new ArgumentNullException(nameof(source));
800 return IndexOf(source, value, startIndex, source.Length - startIndex, CompareOptions.None);
803 public virtual int IndexOf(string source, char value, int startIndex, CompareOptions options)
806 throw new ArgumentNullException(nameof(source));
808 return IndexOf(source, value, startIndex, source.Length - startIndex, options);
812 public virtual int IndexOf(string source, string value, int startIndex, CompareOptions options)
815 throw new ArgumentNullException(nameof(source));
817 return IndexOf(source, value, startIndex, source.Length - startIndex, options);
821 public virtual int IndexOf(string source, char value, int startIndex, int count)
823 return IndexOf(source, value, startIndex, count, CompareOptions.None);
827 public virtual int IndexOf(string source, string value, int startIndex, int count)
829 return IndexOf(source, value, startIndex, count, CompareOptions.None);
832 public unsafe virtual int IndexOf(string source, char value, int startIndex, int count, CompareOptions options)
836 throw new ArgumentNullException(nameof(source));
838 if (startIndex < 0 || startIndex > source.Length)
839 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
841 if (count < 0 || startIndex > source.Length - count)
842 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
844 if (source.Length == 0)
849 if (options == CompareOptions.OrdinalIgnoreCase)
851 return source.IndexOf(value.ToString(), startIndex, count, StringComparison.OrdinalIgnoreCase);
854 // Validate CompareOptions
855 // Ordinal can't be selected with other flags
856 if ((options & ValidIndexMaskOffFlags) != 0 && (options != CompareOptions.Ordinal))
857 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
860 return IndexOfOrdinal(source, new string(value, 1), startIndex, count, ignoreCase: (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
862 return IndexOfCore(source, new string(value, 1), startIndex, count, options, null);
865 public unsafe virtual int IndexOf(string source, string value, int startIndex, int count, CompareOptions options)
869 throw new ArgumentNullException(nameof(source));
871 throw new ArgumentNullException(nameof(value));
873 if (startIndex > source.Length)
875 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
878 // In Everett we used to return -1 for empty string even if startIndex is negative number so we keeping same behavior here.
879 // We return 0 if both source and value are empty strings for Everett compatibility too.
880 if (source.Length == 0)
882 if (value.Length == 0)
891 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
894 if (count < 0 || startIndex > source.Length - count)
895 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
897 if (options == CompareOptions.OrdinalIgnoreCase)
899 return IndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
902 // Validate CompareOptions
903 // Ordinal can't be selected with other flags
904 if ((options & ValidIndexMaskOffFlags) != 0 && (options != CompareOptions.Ordinal))
905 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
908 return IndexOfOrdinal(source, value, startIndex, count, ignoreCase: (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
910 return IndexOfCore(source, value, startIndex, count, options, null);
913 internal virtual int IndexOfOrdinal(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool ignoreCase)
915 Debug.Assert(!_invariantMode);
916 return IndexOfOrdinalCore(source, value, ignoreCase);
919 internal unsafe virtual int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options)
921 Debug.Assert(!_invariantMode);
922 return IndexOfCore(source, value, options, null);
925 // The following IndexOf overload is mainly used by String.Replace. This overload assumes the parameters are already validated
926 // and the caller is passing a valid matchLengthPtr pointer.
927 internal unsafe int IndexOf(string source, string value, int startIndex, int count, CompareOptions options, int* matchLengthPtr)
929 Debug.Assert(source != null);
930 Debug.Assert(value != null);
931 Debug.Assert(startIndex >= 0);
932 Debug.Assert(matchLengthPtr != null);
935 if (source.Length == 0)
937 if (value.Length == 0)
944 if (startIndex >= source.Length)
949 if (options == CompareOptions.OrdinalIgnoreCase)
951 int res = IndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
954 *matchLengthPtr = value.Length;
961 int res = IndexOfOrdinal(source, value, startIndex, count, ignoreCase: (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
964 *matchLengthPtr = value.Length;
969 return IndexOfCore(source, value, startIndex, count, options, matchLengthPtr);
972 internal int IndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
976 return InvariantIndexOf(source, value, startIndex, count, ignoreCase);
979 return IndexOfOrdinalCore(source, value, startIndex, count, ignoreCase);
982 ////////////////////////////////////////////////////////////////////////
986 // Returns the last index where value is found in string. The
987 // search starts from startIndex and ends at endIndex. Returns -1 if
988 // the specified value is not found. If value equals String.Empty,
989 // endIndex is returned. Throws IndexOutOfRange if startIndex or
990 // endIndex is less than zero or greater than the length of string.
991 // Throws ArgumentException if value is null.
993 ////////////////////////////////////////////////////////////////////////
996 public virtual int LastIndexOf(String source, char value)
999 throw new ArgumentNullException(nameof(source));
1001 // Can't start at negative index, so make sure we check for the length == 0 case.
1002 return LastIndexOf(source, value, source.Length - 1, source.Length, CompareOptions.None);
1006 public virtual int LastIndexOf(string source, string value)
1009 throw new ArgumentNullException(nameof(source));
1011 // Can't start at negative index, so make sure we check for the length == 0 case.
1012 return LastIndexOf(source, value, source.Length - 1,
1013 source.Length, CompareOptions.None);
1017 public virtual int LastIndexOf(string source, char value, CompareOptions options)
1020 throw new ArgumentNullException(nameof(source));
1022 // Can't start at negative index, so make sure we check for the length == 0 case.
1023 return LastIndexOf(source, value, source.Length - 1,
1024 source.Length, options);
1027 public virtual int LastIndexOf(string source, string value, CompareOptions options)
1030 throw new ArgumentNullException(nameof(source));
1032 // Can't start at negative index, so make sure we check for the length == 0 case.
1033 return LastIndexOf(source, value, source.Length - 1, source.Length, options);
1036 public virtual int LastIndexOf(string source, char value, int startIndex)
1038 return LastIndexOf(source, value, startIndex, startIndex + 1, CompareOptions.None);
1042 public virtual int LastIndexOf(string source, string value, int startIndex)
1044 return LastIndexOf(source, value, startIndex, startIndex + 1, CompareOptions.None);
1047 public virtual int LastIndexOf(string source, char value, int startIndex, CompareOptions options)
1049 return LastIndexOf(source, value, startIndex, startIndex + 1, options);
1053 public virtual int LastIndexOf(string source, string value, int startIndex, CompareOptions options)
1055 return LastIndexOf(source, value, startIndex, startIndex + 1, options);
1059 public virtual int LastIndexOf(string source, char value, int startIndex, int count)
1061 return LastIndexOf(source, value, startIndex, count, CompareOptions.None);
1065 public virtual int LastIndexOf(string source, string value, int startIndex, int count)
1067 return LastIndexOf(source, value, startIndex, count, CompareOptions.None);
1071 public virtual int LastIndexOf(string source, char value, int startIndex, int count, CompareOptions options)
1075 throw new ArgumentNullException(nameof(source));
1077 // Validate CompareOptions
1078 // Ordinal can't be selected with other flags
1079 if ((options & ValidIndexMaskOffFlags) != 0 &&
1080 (options != CompareOptions.Ordinal) &&
1081 (options != CompareOptions.OrdinalIgnoreCase))
1082 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
1084 // Special case for 0 length input strings
1085 if (source.Length == 0 && (startIndex == -1 || startIndex == 0))
1088 // Make sure we're not out of range
1089 if (startIndex < 0 || startIndex > source.Length)
1090 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
1092 // Make sure that we allow startIndex == source.Length
1093 if (startIndex == source.Length)
1100 // 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
1101 if (count < 0 || startIndex - count + 1 < 0)
1102 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
1104 if (options == CompareOptions.OrdinalIgnoreCase)
1106 return source.LastIndexOf(value.ToString(), startIndex, count, StringComparison.OrdinalIgnoreCase);
1110 return InvariantLastIndexOf(source, new string(value, 1), startIndex, count, (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
1112 return LastIndexOfCore(source, value.ToString(), startIndex, count, options);
1116 public virtual int LastIndexOf(string source, string value, int startIndex, int count, CompareOptions options)
1120 throw new ArgumentNullException(nameof(source));
1122 throw new ArgumentNullException(nameof(value));
1124 // Validate CompareOptions
1125 // Ordinal can't be selected with other flags
1126 if ((options & ValidIndexMaskOffFlags) != 0 &&
1127 (options != CompareOptions.Ordinal) &&
1128 (options != CompareOptions.OrdinalIgnoreCase))
1129 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
1131 // Special case for 0 length input strings
1132 if (source.Length == 0 && (startIndex == -1 || startIndex == 0))
1133 return (value.Length == 0) ? 0 : -1;
1135 // Make sure we're not out of range
1136 if (startIndex < 0 || startIndex > source.Length)
1137 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
1139 // Make sure that we allow startIndex == source.Length
1140 if (startIndex == source.Length)
1146 // If we are looking for nothing, just return 0
1147 if (value.Length == 0 && count >= 0 && startIndex - count + 1 >= 0)
1151 // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
1152 if (count < 0 || startIndex - count + 1 < 0)
1153 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
1155 if (options == CompareOptions.OrdinalIgnoreCase)
1157 return LastIndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
1161 return InvariantLastIndexOf(source, value, startIndex, count, (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
1163 return LastIndexOfCore(source, value, startIndex, count, options);
1166 internal int LastIndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
1170 return InvariantLastIndexOf(source, value, startIndex, count, ignoreCase);
1173 return LastIndexOfOrdinalCore(source, value, startIndex, count, ignoreCase);
1176 ////////////////////////////////////////////////////////////////////////
1180 // Gets the SortKey for the given string with the given options.
1182 ////////////////////////////////////////////////////////////////////////
1183 public virtual SortKey GetSortKey(string source, CompareOptions options)
1186 return InvariantCreateSortKey(source, options);
1188 return CreateSortKey(source, options);
1192 public virtual SortKey GetSortKey(string source)
1195 return InvariantCreateSortKey(source, CompareOptions.None);
1197 return CreateSortKey(source, CompareOptions.None);
1200 ////////////////////////////////////////////////////////////////////////
1204 // Implements Object.Equals(). Returns a boolean indicating whether
1205 // or not object refers to the same CompareInfo as the current
1208 ////////////////////////////////////////////////////////////////////////
1211 public override bool Equals(Object value)
1213 CompareInfo that = value as CompareInfo;
1217 return this.Name == that.Name;
1224 ////////////////////////////////////////////////////////////////////////
1228 // Implements Object.GetHashCode(). Returns the hash code for the
1229 // CompareInfo. The hash code is guaranteed to be the same for
1230 // CompareInfo A and B where A.Equals(B) is true.
1232 ////////////////////////////////////////////////////////////////////////
1235 public override int GetHashCode()
1237 return (this.Name.GetHashCode());
1241 ////////////////////////////////////////////////////////////////////////
1243 // GetHashCodeOfString
1245 // This internal method allows a method that allows the equivalent of creating a Sortkey for a
1246 // string from CompareInfo, and generate a hashcode value from it. It is not very convenient
1247 // to use this method as is and it creates an unnecessary Sortkey object that will be GC'ed.
1249 // The hash code is guaranteed to be the same for string A and B where A.Equals(B) is true and both
1250 // the CompareInfo and the CompareOptions are the same. If two different CompareInfo objects
1251 // treat the string the same way, this implementation will treat them differently (the same way that
1252 // Sortkey does at the moment).
1254 // This method will never be made public itself, but public consumers of it could be created, e.g.:
1256 // string.GetHashCode(CultureInfo)
1257 // string.GetHashCode(CompareInfo)
1258 // string.GetHashCode(CultureInfo, CompareOptions)
1259 // string.GetHashCode(CompareInfo, CompareOptions)
1262 // (the methods above that take a CultureInfo would use CultureInfo.CompareInfo)
1264 ////////////////////////////////////////////////////////////////////////
1265 internal int GetHashCodeOfString(string source, CompareOptions options)
1268 // Parameter validation
1272 throw new ArgumentNullException(nameof(source));
1275 if ((options & ValidHashCodeOfStringMaskOffFlags) != 0)
1277 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
1280 return GetHashCodeOfStringCore(source, options);
1283 public virtual int GetHashCode(string source, CompareOptions options)
1287 throw new ArgumentNullException(nameof(source));
1290 if (options == CompareOptions.Ordinal)
1292 return source.GetHashCode();
1295 if (options == CompareOptions.OrdinalIgnoreCase)
1297 return TextInfo.GetHashCodeOrdinalIgnoreCase(source);
1301 // GetHashCodeOfString does more parameters validation. basically will throw when
1302 // having Ordinal, OrdinalIgnoreCase and StringSort
1305 return GetHashCodeOfString(source, options);
1308 ////////////////////////////////////////////////////////////////////////
1312 // Implements Object.ToString(). Returns a string describing the
1315 ////////////////////////////////////////////////////////////////////////
1316 public override string ToString()
1318 return ("CompareInfo - " + this.Name);
1321 public SortVersion Version
1325 if (m_SortVersion == null)
1329 m_SortVersion = new SortVersion(0, CultureInfo.LOCALE_INVARIANT, new Guid(0, 0, 0, 0, 0, 0, 0,
1330 (byte) (CultureInfo.LOCALE_INVARIANT >> 24),
1331 (byte) ((CultureInfo.LOCALE_INVARIANT & 0x00FF0000) >> 16),
1332 (byte) ((CultureInfo.LOCALE_INVARIANT & 0x0000FF00) >> 8),
1333 (byte) (CultureInfo.LOCALE_INVARIANT & 0xFF)));
1337 m_SortVersion = GetSortVersion();
1341 return m_SortVersion;
1349 return CultureInfo.GetCultureInfo(Name).LCID;