Fix StyleCop warning SA1121 (use built-in types)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / String.Comparison.cs
blob1b9849109402f0f063de859b59eb2eaeee032d19
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 using System.Diagnostics;
6 using System.Globalization;
7 using System.Numerics;
8 using System.Runtime.CompilerServices;
9 using System.Runtime.InteropServices;
11 using Internal.Runtime.CompilerServices;
13 #pragma warning disable SA1121 // explicitly using type aliases instead of built-in types
14 #if BIT64
15 using nuint = System.UInt64;
16 #else
17 using nuint = System.UInt32;
18 #endif
20 namespace System
22 public partial class String
25 // Search/Query methods
28 [MethodImpl(MethodImplOptions.AggressiveInlining)]
29 private static bool EqualsHelper(string strA, string strB)
31 Debug.Assert(strA != null);
32 Debug.Assert(strB != null);
33 Debug.Assert(strA.Length == strB.Length);
35 return SpanHelpers.SequenceEqual(
36 ref Unsafe.As<char, byte>(ref strA.GetRawStringData()),
37 ref Unsafe.As<char, byte>(ref strB.GetRawStringData()),
38 ((nuint)strA.Length) * 2);
41 [MethodImpl(MethodImplOptions.AggressiveInlining)]
42 private static int CompareOrdinalHelper(string strA, int indexA, int countA, string strB, int indexB, int countB)
44 Debug.Assert(strA != null);
45 Debug.Assert(strB != null);
46 Debug.Assert(indexA >= 0 && indexB >= 0);
47 Debug.Assert(countA >= 0 && countB >= 0);
48 Debug.Assert(indexA + countA <= strA.Length && indexB + countB <= strB.Length);
50 return SpanHelpers.SequenceCompareTo(ref Unsafe.Add(ref strA.GetRawStringData(), indexA), countA, ref Unsafe.Add(ref strB.GetRawStringData(), indexB), countB);
53 private static bool EqualsOrdinalIgnoreCase(string strA, string strB)
55 Debug.Assert(strA.Length == strB.Length);
57 return CompareInfo.EqualsOrdinalIgnoreCase(ref strA.GetRawStringData(), ref strB.GetRawStringData(), strB.Length);
59 private static unsafe int CompareOrdinalHelper(string strA, string strB)
61 Debug.Assert(strA != null);
62 Debug.Assert(strB != null);
64 // NOTE: This may be subject to change if eliminating the check
65 // in the callers makes them small enough to be inlined
66 Debug.Assert(strA._firstChar == strB._firstChar,
67 "For performance reasons, callers of this method should " +
68 "check/short-circuit beforehand if the first char is the same.");
70 int length = Math.Min(strA.Length, strB.Length);
72 fixed (char* ap = &strA._firstChar) fixed (char* bp = &strB._firstChar)
74 char* a = ap;
75 char* b = bp;
77 // Check if the second chars are different here
78 // The reason we check if _firstChar is different is because
79 // it's the most common case and allows us to avoid a method call
80 // to here.
81 // The reason we check if the second char is different is because
82 // if the first two chars the same we can increment by 4 bytes,
83 // leaving us word-aligned on both 32-bit (12 bytes into the string)
84 // and 64-bit (16 bytes) platforms.
86 // For empty strings, the second char will be null due to padding.
87 // The start of the string is the type pointer + string length, which
88 // takes up 8 bytes on 32-bit, 12 on x64. For empty strings the null
89 // terminator immediately follows, leaving us with an object
90 // 10/14 bytes in size. Since everything needs to be a multiple
91 // of 4/8, this will get padded and zeroed out.
93 // For one-char strings the second char will be the null terminator.
95 // NOTE: If in the future there is a way to read the second char
96 // without pinning the string (e.g. System.Runtime.CompilerServices.Unsafe
97 // is exposed to mscorlib, or a future version of C# allows inline IL),
98 // then do that and short-circuit before the fixed.
100 if (*(a + 1) != *(b + 1)) goto DiffOffset1;
102 // Since we know that the first two chars are the same,
103 // we can increment by 2 here and skip 4 bytes.
104 // This leaves us 8-byte aligned, which results
105 // on better perf for 64-bit platforms.
106 length -= 2; a += 2; b += 2;
108 // unroll the loop
109 #if BIT64
110 while (length >= 12)
112 if (*(long*)a != *(long*)b) goto DiffOffset0;
113 if (*(long*)(a + 4) != *(long*)(b + 4)) goto DiffOffset4;
114 if (*(long*)(a + 8) != *(long*)(b + 8)) goto DiffOffset8;
115 length -= 12; a += 12; b += 12;
117 #else // BIT64
118 while (length >= 10)
120 if (*(int*)a != *(int*)b) goto DiffOffset0;
121 if (*(int*)(a + 2) != *(int*)(b + 2)) goto DiffOffset2;
122 if (*(int*)(a + 4) != *(int*)(b + 4)) goto DiffOffset4;
123 if (*(int*)(a + 6) != *(int*)(b + 6)) goto DiffOffset6;
124 if (*(int*)(a + 8) != *(int*)(b + 8)) goto DiffOffset8;
125 length -= 10; a += 10; b += 10;
127 #endif // BIT64
129 // Fallback loop:
130 // go back to slower code path and do comparison on 4 bytes at a time.
131 // This depends on the fact that the String objects are
132 // always zero terminated and that the terminating zero is not included
133 // in the length. For odd string sizes, the last compare will include
134 // the zero terminator.
135 while (length > 0)
137 if (*(int*)a != *(int*)b) goto DiffNextInt;
138 length -= 2;
139 a += 2;
140 b += 2;
143 // At this point, we have compared all the characters in at least one string.
144 // The longer string will be larger.
145 return strA.Length - strB.Length;
147 #if BIT64
148 DiffOffset8: a += 4; b += 4;
149 DiffOffset4: a += 4; b += 4;
150 #else // BIT64
151 // Use jumps instead of falling through, since
152 // otherwise going to DiffOffset8 will involve
153 // 8 add instructions before getting to DiffNextInt
154 DiffOffset8: a += 8; b += 8; goto DiffOffset0;
155 DiffOffset6: a += 6; b += 6; goto DiffOffset0;
156 DiffOffset4: a += 2; b += 2;
157 DiffOffset2: a += 2; b += 2;
158 #endif // BIT64
160 DiffOffset0:
161 // If we reached here, we already see a difference in the unrolled loop above
162 #if BIT64
163 if (*(int*)a == *(int*)b)
165 a += 2; b += 2;
167 #endif // BIT64
169 DiffNextInt:
170 if (*a != *b) return *a - *b;
172 DiffOffset1:
173 Debug.Assert(*(a + 1) != *(b + 1), "This char must be different if we reach here!");
174 return *(a + 1) - *(b + 1);
178 // Provides a culture-correct string comparison. StrA is compared to StrB
179 // to determine whether it is lexicographically less, equal, or greater, and then returns
180 // either a negative integer, 0, or a positive integer; respectively.
182 public static int Compare(string? strA, string? strB)
184 return Compare(strA, strB, StringComparison.CurrentCulture);
188 // Provides a culture-correct string comparison. strA is compared to strB
189 // to determine whether it is lexicographically less, equal, or greater, and then a
190 // negative integer, 0, or a positive integer is returned; respectively.
191 // The case-sensitive option is set by ignoreCase
193 public static int Compare(string? strA, string? strB, bool ignoreCase)
195 var comparisonType = ignoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture;
196 return Compare(strA, strB, comparisonType);
200 // Provides a more flexible function for string comparison. See StringComparison
201 // for meaning of different comparisonType.
202 public static int Compare(string? strA, string? strB, StringComparison comparisonType)
204 if (object.ReferenceEquals(strA, strB))
206 CheckStringComparison(comparisonType);
207 return 0;
210 // They can't both be null at this point.
211 if (strA == null)
213 CheckStringComparison(comparisonType);
214 return -1;
216 if (strB == null)
218 CheckStringComparison(comparisonType);
219 return 1;
222 switch (comparisonType)
224 case StringComparison.CurrentCulture:
225 case StringComparison.CurrentCultureIgnoreCase:
226 return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, strB, GetCaseCompareOfComparisonCulture(comparisonType));
228 case StringComparison.InvariantCulture:
229 case StringComparison.InvariantCultureIgnoreCase:
230 return CompareInfo.Invariant.Compare(strA, strB, GetCaseCompareOfComparisonCulture(comparisonType));
232 case StringComparison.Ordinal:
233 // Most common case: first character is different.
234 // Returns false for empty strings.
235 if (strA._firstChar != strB._firstChar)
237 return strA._firstChar - strB._firstChar;
240 return CompareOrdinalHelper(strA, strB);
242 case StringComparison.OrdinalIgnoreCase:
243 return CompareInfo.CompareOrdinalIgnoreCase(strA, strB);
245 default:
246 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
251 // Provides a culture-correct string comparison. strA is compared to strB
252 // to determine whether it is lexicographically less, equal, or greater, and then a
253 // negative integer, 0, or a positive integer is returned; respectively.
255 public static int Compare(string? strA, string? strB, CultureInfo? culture, CompareOptions options)
257 CultureInfo compareCulture = culture ?? CultureInfo.CurrentCulture;
258 return compareCulture.CompareInfo.Compare(strA, strB, options);
263 // Provides a culture-correct string comparison. strA is compared to strB
264 // to determine whether it is lexicographically less, equal, or greater, and then a
265 // negative integer, 0, or a positive integer is returned; respectively.
266 // The case-sensitive option is set by ignoreCase, and the culture is set
267 // by culture
269 public static int Compare(string? strA, string? strB, bool ignoreCase, CultureInfo? culture)
271 var options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None;
272 return Compare(strA, strB, culture, options);
275 // Determines whether two string regions match. The substring of strA beginning
276 // at indexA of given length is compared with the substring of strB
277 // beginning at indexB of the same length.
279 public static int Compare(string? strA, int indexA, string? strB, int indexB, int length)
281 // NOTE: It's important we call the boolean overload, and not the StringComparison
282 // one. The two have some subtly different behavior (see notes in the former).
283 return Compare(strA, indexA, strB, indexB, length, ignoreCase: false);
286 // Determines whether two string regions match. The substring of strA beginning
287 // at indexA of given length is compared with the substring of strB
288 // beginning at indexB of the same length. Case sensitivity is determined by the ignoreCase boolean.
290 public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, bool ignoreCase)
292 // Ideally we would just forward to the string.Compare overload that takes
293 // a StringComparison parameter, and just pass in CurrentCulture/CurrentCultureIgnoreCase.
294 // That function will return early if an optimization can be applied, e.g. if
295 // (object)strA == strB && indexA == indexB then it will return 0 straightaway.
296 // There are a couple of subtle behavior differences that prevent us from doing so
297 // however:
298 // - string.Compare(null, -1, null, -1, -1, StringComparison.CurrentCulture) works
299 // since that method also returns early for nulls before validation. It shouldn't
300 // for this overload.
301 // - Since we originally forwarded to CompareInfo.Compare for all of the argument
302 // validation logic, the ArgumentOutOfRangeExceptions thrown will contain different
303 // parameter names.
304 // Therefore, we have to duplicate some of the logic here.
306 int lengthA = length;
307 int lengthB = length;
309 if (strA != null)
311 lengthA = Math.Min(lengthA, strA.Length - indexA);
314 if (strB != null)
316 lengthB = Math.Min(lengthB, strB.Length - indexB);
319 var options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None;
320 return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, options);
323 // Determines whether two string regions match. The substring of strA beginning
324 // at indexA of length length is compared with the substring of strB
325 // beginning at indexB of the same length. Case sensitivity is determined by the ignoreCase boolean,
326 // and the culture is set by culture.
328 public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, bool ignoreCase, CultureInfo? culture)
330 var options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None;
331 return Compare(strA, indexA, strB, indexB, length, culture, options);
335 // Determines whether two string regions match. The substring of strA beginning
336 // at indexA of length length is compared with the substring of strB
337 // beginning at indexB of the same length.
339 public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, CultureInfo? culture, CompareOptions options)
341 CultureInfo compareCulture = culture ?? CultureInfo.CurrentCulture;
342 int lengthA = length;
343 int lengthB = length;
345 if (strA != null)
347 lengthA = Math.Min(lengthA, strA.Length - indexA);
350 if (strB != null)
352 lengthB = Math.Min(lengthB, strB.Length - indexB);
355 return compareCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, options);
358 public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, StringComparison comparisonType)
360 CheckStringComparison(comparisonType);
362 if (strA == null || strB == null)
365 if (object.ReferenceEquals(strA, strB))
367 // They're both null
368 return 0;
371 return strA == null ? -1 : 1;
374 if (length < 0)
376 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NegativeLength);
379 if (indexA < 0 || indexB < 0)
381 string paramName = indexA < 0 ? nameof(indexA) : nameof(indexB);
382 throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_Index);
385 if (strA.Length - indexA < 0 || strB.Length - indexB < 0)
387 string paramName = strA.Length - indexA < 0 ? nameof(indexA) : nameof(indexB);
388 throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_Index);
391 if (length == 0 || (object.ReferenceEquals(strA, strB) && indexA == indexB))
393 return 0;
396 int lengthA = Math.Min(length, strA.Length - indexA);
397 int lengthB = Math.Min(length, strB.Length - indexB);
399 switch (comparisonType)
401 case StringComparison.CurrentCulture:
402 case StringComparison.CurrentCultureIgnoreCase:
403 return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, GetCaseCompareOfComparisonCulture(comparisonType));
405 case StringComparison.InvariantCulture:
406 case StringComparison.InvariantCultureIgnoreCase:
407 return CompareInfo.Invariant.Compare(strA, indexA, lengthA, strB, indexB, lengthB, GetCaseCompareOfComparisonCulture(comparisonType));
409 case StringComparison.Ordinal:
410 return CompareOrdinalHelper(strA, indexA, lengthA, strB, indexB, lengthB);
412 default:
413 Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase); // CheckStringComparison validated these earlier
414 return CompareInfo.CompareOrdinalIgnoreCase(strA, indexA, lengthA, strB, indexB, lengthB);
418 // Compares strA and strB using an ordinal (code-point) comparison.
420 public static int CompareOrdinal(string? strA, string? strB)
422 if (object.ReferenceEquals(strA, strB))
424 return 0;
427 // They can't both be null at this point.
428 if (strA == null)
430 return -1;
432 if (strB == null)
434 return 1;
437 // Most common case, first character is different.
438 // This will return false for empty strings.
439 if (strA._firstChar != strB._firstChar)
441 return strA._firstChar - strB._firstChar;
444 return CompareOrdinalHelper(strA, strB);
447 [MethodImpl(MethodImplOptions.AggressiveInlining)]
448 internal static int CompareOrdinal(ReadOnlySpan<char> strA, ReadOnlySpan<char> strB)
449 => SpanHelpers.SequenceCompareTo(ref MemoryMarshal.GetReference(strA), strA.Length, ref MemoryMarshal.GetReference(strB), strB.Length);
451 // Compares strA and strB using an ordinal (code-point) comparison.
453 public static int CompareOrdinal(string? strA, int indexA, string? strB, int indexB, int length)
455 if (strA == null || strB == null)
457 if (object.ReferenceEquals(strA, strB))
459 // They're both null
460 return 0;
463 return strA == null ? -1 : 1;
466 // COMPAT: Checking for nulls should become before the arguments are validated,
467 // but other optimizations which allow us to return early should come after.
469 if (length < 0)
471 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NegativeCount);
474 if (indexA < 0 || indexB < 0)
476 string paramName = indexA < 0 ? nameof(indexA) : nameof(indexB);
477 throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_Index);
480 int lengthA = Math.Min(length, strA.Length - indexA);
481 int lengthB = Math.Min(length, strB.Length - indexB);
483 if (lengthA < 0 || lengthB < 0)
485 string paramName = lengthA < 0 ? nameof(indexA) : nameof(indexB);
486 throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_Index);
489 if (length == 0 || (object.ReferenceEquals(strA, strB) && indexA == indexB))
491 return 0;
494 return CompareOrdinalHelper(strA, indexA, lengthA, strB, indexB, lengthB);
497 // Compares this String to another String (cast as object), returning an integer that
498 // indicates the relationship. This method returns a value less than 0 if this is less than value, 0
499 // if this is equal to value, or a value greater than 0 if this is greater than value.
501 public int CompareTo(object? value)
503 if (value == null)
505 return 1;
508 if (!(value is string other))
510 throw new ArgumentException(SR.Arg_MustBeString);
513 return CompareTo(other); // will call the string-based overload
516 // Determines the sorting relation of StrB to the current instance.
518 public int CompareTo(string? strB)
520 return string.Compare(this, strB, StringComparison.CurrentCulture);
523 // Determines whether a specified string is a suffix of the current instance.
525 // The case-sensitive and culture-sensitive option is set by options,
526 // and the default culture is used.
528 public bool EndsWith(string value)
530 return EndsWith(value, StringComparison.CurrentCulture);
533 public bool EndsWith(string value, StringComparison comparisonType)
535 if ((object)value == null)
537 throw new ArgumentNullException(nameof(value));
540 if ((object)this == (object)value)
542 CheckStringComparison(comparisonType);
543 return true;
546 if (value.Length == 0)
548 CheckStringComparison(comparisonType);
549 return true;
552 switch (comparisonType)
554 case StringComparison.CurrentCulture:
555 case StringComparison.CurrentCultureIgnoreCase:
556 return CultureInfo.CurrentCulture.CompareInfo.IsSuffix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
558 case StringComparison.InvariantCulture:
559 case StringComparison.InvariantCultureIgnoreCase:
560 return CompareInfo.Invariant.IsSuffix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
562 case StringComparison.Ordinal:
563 int offset = this.Length - value.Length;
564 return (uint)offset <= (uint)this.Length && this.AsSpan(offset).SequenceEqual(value);
566 case StringComparison.OrdinalIgnoreCase:
567 return this.Length < value.Length ? false : (CompareInfo.CompareOrdinalIgnoreCase(this, this.Length - value.Length, value.Length, value, 0, value.Length) == 0);
569 default:
570 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
574 public bool EndsWith(string value, bool ignoreCase, CultureInfo? culture)
576 if (null == value)
578 throw new ArgumentNullException(nameof(value));
581 if ((object)this == (object)value)
583 return true;
586 CultureInfo referenceCulture = culture ?? CultureInfo.CurrentCulture;
587 return referenceCulture.CompareInfo.IsSuffix(this, value, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
590 public bool EndsWith(char value)
592 int lastPos = Length - 1;
593 return ((uint)lastPos < (uint)Length) && this[lastPos] == value;
596 // Determines whether two strings match.
597 public override bool Equals(object? obj)
599 if (object.ReferenceEquals(this, obj))
600 return true;
602 if (!(obj is string str))
603 return false;
605 if (this.Length != str.Length)
606 return false;
608 return EqualsHelper(this, str);
611 // Determines whether two strings match.
612 public bool Equals(string? value)
614 if (object.ReferenceEquals(this, value))
615 return true;
617 // NOTE: No need to worry about casting to object here.
618 // If either side of an == comparison between strings
619 // is null, Roslyn generates a simple ceq instruction
620 // instead of calling string.op_Equality.
621 if (value == null)
622 return false;
624 if (this.Length != value.Length)
625 return false;
627 return EqualsHelper(this, value);
630 public bool Equals(string? value, StringComparison comparisonType)
632 if (object.ReferenceEquals(this, value))
634 CheckStringComparison(comparisonType);
635 return true;
638 if (value is null)
640 CheckStringComparison(comparisonType);
641 return false;
644 switch (comparisonType)
646 case StringComparison.CurrentCulture:
647 case StringComparison.CurrentCultureIgnoreCase:
648 return (CultureInfo.CurrentCulture.CompareInfo.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType)) == 0);
650 case StringComparison.InvariantCulture:
651 case StringComparison.InvariantCultureIgnoreCase:
652 return (CompareInfo.Invariant.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType)) == 0);
654 case StringComparison.Ordinal:
655 if (this.Length != value.Length)
656 return false;
657 return EqualsHelper(this, value);
659 case StringComparison.OrdinalIgnoreCase:
660 if (this.Length != value.Length)
661 return false;
663 return EqualsOrdinalIgnoreCase(this, value);
665 default:
666 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
671 // Determines whether two Strings match.
672 public static bool Equals(string? a, string? b)
674 if (object.ReferenceEquals(a,b))
676 return true;
679 if (a is null || b is null || a.Length != b.Length)
681 return false;
684 return EqualsHelper(a, b);
687 public static bool Equals(string? a, string? b, StringComparison comparisonType)
689 if (object.ReferenceEquals(a, b))
691 CheckStringComparison(comparisonType);
692 return true;
695 if (a is null || b is null)
697 CheckStringComparison(comparisonType);
698 return false;
701 switch (comparisonType)
703 case StringComparison.CurrentCulture:
704 case StringComparison.CurrentCultureIgnoreCase:
705 return (CultureInfo.CurrentCulture.CompareInfo.Compare(a, b, GetCaseCompareOfComparisonCulture(comparisonType)) == 0);
707 case StringComparison.InvariantCulture:
708 case StringComparison.InvariantCultureIgnoreCase:
709 return (CompareInfo.Invariant.Compare(a, b, GetCaseCompareOfComparisonCulture(comparisonType)) == 0);
711 case StringComparison.Ordinal:
712 if (a.Length != b.Length)
713 return false;
714 return EqualsHelper(a, b);
716 case StringComparison.OrdinalIgnoreCase:
717 if (a.Length != b.Length)
718 return false;
720 return EqualsOrdinalIgnoreCase(a, b);
722 default:
723 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
727 public static bool operator ==(string? a, string? b)
729 return string.Equals(a, b);
732 public static bool operator !=(string? a, string? b)
734 return !string.Equals(a, b);
737 // Gets a hash code for this string. If strings A and B are such that A.Equals(B), then
738 // they will return the same hash code.
739 [MethodImpl(MethodImplOptions.AggressiveInlining)]
740 public override int GetHashCode()
742 ulong seed = Marvin.DefaultSeed;
744 // Multiplication below will not overflow since going from positive Int32 to UInt32.
745 return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref _firstChar), (uint)_stringLength * 2 /* in bytes, not chars */, (uint)seed, (uint)(seed >> 32));
748 // Gets a hash code for this string and this comparison. If strings A and B and comparison C are such
749 // that string.Equals(A, B, C), then they will return the same hash code with this comparison C.
750 public int GetHashCode(StringComparison comparisonType) => StringComparer.FromComparison(comparisonType).GetHashCode(this);
752 [MethodImpl(MethodImplOptions.AggressiveInlining)]
753 internal int GetHashCodeOrdinalIgnoreCase()
755 ulong seed = Marvin.DefaultSeed;
756 return Marvin.ComputeHash32OrdinalIgnoreCase(ref _firstChar, _stringLength /* in chars, not bytes */, (uint)seed, (uint)(seed >> 32));
759 // A span-based equivalent of String.GetHashCode(). Computes an ordinal hash code.
760 [MethodImpl(MethodImplOptions.AggressiveInlining)]
761 public static int GetHashCode(ReadOnlySpan<char> value)
763 ulong seed = Marvin.DefaultSeed;
765 // Multiplication below will not overflow since going from positive Int32 to UInt32.
766 return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref MemoryMarshal.GetReference(value)), (uint)value.Length * 2 /* in bytes, not chars */, (uint)seed, (uint)(seed >> 32));
769 // A span-based equivalent of String.GetHashCode(StringComparison). Uses the specified comparison type.
770 public static int GetHashCode(ReadOnlySpan<char> value, StringComparison comparisonType)
772 switch (comparisonType)
774 case StringComparison.CurrentCulture:
775 case StringComparison.CurrentCultureIgnoreCase:
776 return CultureInfo.CurrentCulture.CompareInfo.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType));
778 case StringComparison.InvariantCulture:
779 case StringComparison.InvariantCultureIgnoreCase:
780 return CultureInfo.InvariantCulture.CompareInfo.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType));
782 case StringComparison.Ordinal:
783 return GetHashCode(value);
785 case StringComparison.OrdinalIgnoreCase:
786 return GetHashCodeOrdinalIgnoreCase(value);
788 default:
789 ThrowHelper.ThrowArgumentException(ExceptionResource.NotSupported_StringComparison, ExceptionArgument.comparisonType);
790 Debug.Fail("Should not reach this point.");
791 return default;
795 [MethodImpl(MethodImplOptions.AggressiveInlining)]
796 internal static int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> value)
798 ulong seed = Marvin.DefaultSeed;
799 return Marvin.ComputeHash32OrdinalIgnoreCase(ref MemoryMarshal.GetReference(value), value.Length /* in chars, not bytes */, (uint)seed, (uint)(seed >> 32));
802 // Use this if and only if 'Denial of Service' attacks are not a concern (i.e. never used for free-form user input),
803 // or are otherwise mitigated
804 internal unsafe int GetNonRandomizedHashCode()
806 fixed (char* src = &_firstChar)
808 Debug.Assert(src[this.Length] == '\0', "src[this.Length] == '\\0'");
809 Debug.Assert(((int)src) % 4 == 0, "Managed string should start at 4 bytes boundary");
811 uint hash1 = (5381 << 16) + 5381;
812 uint hash2 = hash1;
814 uint* ptr = (uint*)src;
815 int length = this.Length;
817 while (length > 2)
819 length -= 4;
820 // Where length is 4n-1 (e.g. 3,7,11,15,19) this additionally consumes the null terminator
821 hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ ptr[0];
822 hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptr[1];
823 ptr += 2;
826 if (length > 0)
828 // Where length is 4n-3 (e.g. 1,5,9,13,17) this additionally consumes the null terminator
829 hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptr[0];
832 return (int)(hash1 + (hash2 * 1566083941));
836 // Determines whether a specified string is a prefix of the current instance
838 public bool StartsWith(string value)
840 if (value is null)
842 throw new ArgumentNullException(nameof(value));
844 return StartsWith(value, StringComparison.CurrentCulture);
847 public bool StartsWith(string value, StringComparison comparisonType)
849 if (value is null)
851 throw new ArgumentNullException(nameof(value));
854 if ((object)this == (object)value)
856 CheckStringComparison(comparisonType);
857 return true;
860 if (value.Length == 0)
862 CheckStringComparison(comparisonType);
863 return true;
866 switch (comparisonType)
868 case StringComparison.CurrentCulture:
869 case StringComparison.CurrentCultureIgnoreCase:
870 return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
872 case StringComparison.InvariantCulture:
873 case StringComparison.InvariantCultureIgnoreCase:
874 return CompareInfo.Invariant.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
876 case StringComparison.Ordinal:
877 if (this.Length < value.Length || _firstChar != value._firstChar)
879 return false;
881 return (value.Length == 1) ?
882 true : // First char is the same and thats all there is to compare
883 SpanHelpers.SequenceEqual(
884 ref Unsafe.As<char, byte>(ref this.GetRawStringData()),
885 ref Unsafe.As<char, byte>(ref value.GetRawStringData()),
886 ((nuint)value.Length) * 2);
888 case StringComparison.OrdinalIgnoreCase:
889 if (this.Length < value.Length)
891 return false;
893 return CompareInfo.EqualsOrdinalIgnoreCase(ref this.GetRawStringData(), ref value.GetRawStringData(), value.Length);
895 default:
896 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
900 public bool StartsWith(string value, bool ignoreCase, CultureInfo? culture)
902 if (null == value)
904 throw new ArgumentNullException(nameof(value));
907 if ((object)this == (object)value)
909 return true;
912 CultureInfo referenceCulture = culture ?? CultureInfo.CurrentCulture;
913 return referenceCulture.CompareInfo.IsPrefix(this, value, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
916 public bool StartsWith(char value) => Length != 0 && _firstChar == value;
918 internal static void CheckStringComparison(StringComparison comparisonType)
920 // Single comparison to check if comparisonType is within [CurrentCulture .. OrdinalIgnoreCase]
921 if ((uint)comparisonType > (uint)StringComparison.OrdinalIgnoreCase)
923 ThrowHelper.ThrowArgumentException(ExceptionResource.NotSupported_StringComparison, ExceptionArgument.comparisonType);
927 internal static CompareOptions GetCaseCompareOfComparisonCulture(StringComparison comparisonType)
929 Debug.Assert((uint)comparisonType <= (uint)StringComparison.OrdinalIgnoreCase);
931 // Culture enums can be & with CompareOptions.IgnoreCase 0x01 to extract if IgnoreCase or CompareOptions.None 0x00
933 // CompareOptions.None 0x00
934 // CompareOptions.IgnoreCase 0x01
936 // StringComparison.CurrentCulture: 0x00
937 // StringComparison.InvariantCulture: 0x02
938 // StringComparison.Ordinal 0x04
940 // StringComparison.CurrentCultureIgnoreCase: 0x01
941 // StringComparison.InvariantCultureIgnoreCase: 0x03
942 // StringComparison.OrdinalIgnoreCase 0x05
944 return (CompareOptions)((int)comparisonType & (int)CompareOptions.IgnoreCase);