String.Comparison.cs 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933
  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.
  4. using System.Diagnostics;
  5. using System.Globalization;
  6. using System.Numerics;
  7. using System.Runtime.CompilerServices;
  8. using System.Runtime.InteropServices;
  9. using Internal.Runtime.CompilerServices;
  10. #pragma warning disable SA1121 // explicitly using type aliases instead of built-in types
  11. #if BIT64
  12. using nuint = System.UInt64;
  13. #else
  14. using nuint = System.UInt32;
  15. #endif
  16. namespace System
  17. {
  18. public partial class String
  19. {
  20. //
  21. // Search/Query methods
  22. //
  23. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  24. private static bool EqualsHelper(string strA, string strB)
  25. {
  26. Debug.Assert(strA != null);
  27. Debug.Assert(strB != null);
  28. Debug.Assert(strA.Length == strB.Length);
  29. return SpanHelpers.SequenceEqual(
  30. ref Unsafe.As<char, byte>(ref strA.GetRawStringData()),
  31. ref Unsafe.As<char, byte>(ref strB.GetRawStringData()),
  32. ((nuint)strA.Length) * 2);
  33. }
  34. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  35. private static int CompareOrdinalHelper(string strA, int indexA, int countA, string strB, int indexB, int countB)
  36. {
  37. Debug.Assert(strA != null);
  38. Debug.Assert(strB != null);
  39. Debug.Assert(indexA >= 0 && indexB >= 0);
  40. Debug.Assert(countA >= 0 && countB >= 0);
  41. Debug.Assert(indexA + countA <= strA.Length && indexB + countB <= strB.Length);
  42. return SpanHelpers.SequenceCompareTo(ref Unsafe.Add(ref strA.GetRawStringData(), indexA), countA, ref Unsafe.Add(ref strB.GetRawStringData(), indexB), countB);
  43. }
  44. private static bool EqualsOrdinalIgnoreCase(string strA, string strB)
  45. {
  46. Debug.Assert(strA.Length == strB.Length);
  47. return CompareInfo.EqualsOrdinalIgnoreCase(ref strA.GetRawStringData(), ref strB.GetRawStringData(), strB.Length);
  48. }
  49. private static unsafe int CompareOrdinalHelper(string strA, string strB)
  50. {
  51. Debug.Assert(strA != null);
  52. Debug.Assert(strB != null);
  53. // NOTE: This may be subject to change if eliminating the check
  54. // in the callers makes them small enough to be inlined
  55. Debug.Assert(strA._firstChar == strB._firstChar,
  56. "For performance reasons, callers of this method should " +
  57. "check/short-circuit beforehand if the first char is the same.");
  58. int length = Math.Min(strA.Length, strB.Length);
  59. fixed (char* ap = &strA._firstChar) fixed (char* bp = &strB._firstChar)
  60. {
  61. char* a = ap;
  62. char* b = bp;
  63. // Check if the second chars are different here
  64. // The reason we check if _firstChar is different is because
  65. // it's the most common case and allows us to avoid a method call
  66. // to here.
  67. // The reason we check if the second char is different is because
  68. // if the first two chars the same we can increment by 4 bytes,
  69. // leaving us word-aligned on both 32-bit (12 bytes into the string)
  70. // and 64-bit (16 bytes) platforms.
  71. // For empty strings, the second char will be null due to padding.
  72. // The start of the string is the type pointer + string length, which
  73. // takes up 8 bytes on 32-bit, 12 on x64. For empty strings the null
  74. // terminator immediately follows, leaving us with an object
  75. // 10/14 bytes in size. Since everything needs to be a multiple
  76. // of 4/8, this will get padded and zeroed out.
  77. // For one-char strings the second char will be the null terminator.
  78. // NOTE: If in the future there is a way to read the second char
  79. // without pinning the string (e.g. System.Runtime.CompilerServices.Unsafe
  80. // is exposed to mscorlib, or a future version of C# allows inline IL),
  81. // then do that and short-circuit before the fixed.
  82. if (*(a + 1) != *(b + 1)) goto DiffOffset1;
  83. // Since we know that the first two chars are the same,
  84. // we can increment by 2 here and skip 4 bytes.
  85. // This leaves us 8-byte aligned, which results
  86. // on better perf for 64-bit platforms.
  87. length -= 2; a += 2; b += 2;
  88. // unroll the loop
  89. #if BIT64
  90. while (length >= 12)
  91. {
  92. if (*(long*)a != *(long*)b) goto DiffOffset0;
  93. if (*(long*)(a + 4) != *(long*)(b + 4)) goto DiffOffset4;
  94. if (*(long*)(a + 8) != *(long*)(b + 8)) goto DiffOffset8;
  95. length -= 12; a += 12; b += 12;
  96. }
  97. #else // BIT64
  98. while (length >= 10)
  99. {
  100. if (*(int*)a != *(int*)b) goto DiffOffset0;
  101. if (*(int*)(a + 2) != *(int*)(b + 2)) goto DiffOffset2;
  102. if (*(int*)(a + 4) != *(int*)(b + 4)) goto DiffOffset4;
  103. if (*(int*)(a + 6) != *(int*)(b + 6)) goto DiffOffset6;
  104. if (*(int*)(a + 8) != *(int*)(b + 8)) goto DiffOffset8;
  105. length -= 10; a += 10; b += 10;
  106. }
  107. #endif // BIT64
  108. // Fallback loop:
  109. // go back to slower code path and do comparison on 4 bytes at a time.
  110. // This depends on the fact that the String objects are
  111. // always zero terminated and that the terminating zero is not included
  112. // in the length. For odd string sizes, the last compare will include
  113. // the zero terminator.
  114. while (length > 0)
  115. {
  116. if (*(int*)a != *(int*)b) goto DiffNextInt;
  117. length -= 2;
  118. a += 2;
  119. b += 2;
  120. }
  121. // At this point, we have compared all the characters in at least one string.
  122. // The longer string will be larger.
  123. return strA.Length - strB.Length;
  124. #if BIT64
  125. DiffOffset8: a += 4; b += 4;
  126. DiffOffset4: a += 4; b += 4;
  127. #else // BIT64
  128. // Use jumps instead of falling through, since
  129. // otherwise going to DiffOffset8 will involve
  130. // 8 add instructions before getting to DiffNextInt
  131. DiffOffset8: a += 8; b += 8; goto DiffOffset0;
  132. DiffOffset6: a += 6; b += 6; goto DiffOffset0;
  133. DiffOffset4: a += 2; b += 2;
  134. DiffOffset2: a += 2; b += 2;
  135. #endif // BIT64
  136. DiffOffset0:
  137. // If we reached here, we already see a difference in the unrolled loop above
  138. #if BIT64
  139. if (*(int*)a == *(int*)b)
  140. {
  141. a += 2; b += 2;
  142. }
  143. #endif // BIT64
  144. DiffNextInt:
  145. if (*a != *b) return *a - *b;
  146. DiffOffset1:
  147. Debug.Assert(*(a + 1) != *(b + 1), "This char must be different if we reach here!");
  148. return *(a + 1) - *(b + 1);
  149. }
  150. }
  151. // Provides a culture-correct string comparison. StrA is compared to StrB
  152. // to determine whether it is lexicographically less, equal, or greater, and then returns
  153. // either a negative integer, 0, or a positive integer; respectively.
  154. //
  155. public static int Compare(string? strA, string? strB)
  156. {
  157. return Compare(strA, strB, StringComparison.CurrentCulture);
  158. }
  159. // Provides a culture-correct string comparison. strA is compared to strB
  160. // to determine whether it is lexicographically less, equal, or greater, and then a
  161. // negative integer, 0, or a positive integer is returned; respectively.
  162. // The case-sensitive option is set by ignoreCase
  163. //
  164. public static int Compare(string? strA, string? strB, bool ignoreCase)
  165. {
  166. StringComparison comparisonType = ignoreCase ? StringComparison.CurrentCultureIgnoreCase : StringComparison.CurrentCulture;
  167. return Compare(strA, strB, comparisonType);
  168. }
  169. // Provides a more flexible function for string comparison. See StringComparison
  170. // for meaning of different comparisonType.
  171. public static int Compare(string? strA, string? strB, StringComparison comparisonType)
  172. {
  173. if (object.ReferenceEquals(strA, strB))
  174. {
  175. CheckStringComparison(comparisonType);
  176. return 0;
  177. }
  178. // They can't both be null at this point.
  179. if (strA == null)
  180. {
  181. CheckStringComparison(comparisonType);
  182. return -1;
  183. }
  184. if (strB == null)
  185. {
  186. CheckStringComparison(comparisonType);
  187. return 1;
  188. }
  189. switch (comparisonType)
  190. {
  191. case StringComparison.CurrentCulture:
  192. case StringComparison.CurrentCultureIgnoreCase:
  193. return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, strB, GetCaseCompareOfComparisonCulture(comparisonType));
  194. case StringComparison.InvariantCulture:
  195. case StringComparison.InvariantCultureIgnoreCase:
  196. return CompareInfo.Invariant.Compare(strA, strB, GetCaseCompareOfComparisonCulture(comparisonType));
  197. case StringComparison.Ordinal:
  198. // Most common case: first character is different.
  199. // Returns false for empty strings.
  200. if (strA._firstChar != strB._firstChar)
  201. {
  202. return strA._firstChar - strB._firstChar;
  203. }
  204. return CompareOrdinalHelper(strA, strB);
  205. case StringComparison.OrdinalIgnoreCase:
  206. return CompareInfo.CompareOrdinalIgnoreCase(strA, strB);
  207. default:
  208. throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
  209. }
  210. }
  211. // Provides a culture-correct string comparison. strA is compared to strB
  212. // to determine whether it is lexicographically less, equal, or greater, and then a
  213. // negative integer, 0, or a positive integer is returned; respectively.
  214. //
  215. public static int Compare(string? strA, string? strB, CultureInfo? culture, CompareOptions options)
  216. {
  217. CultureInfo compareCulture = culture ?? CultureInfo.CurrentCulture;
  218. return compareCulture.CompareInfo.Compare(strA, strB, options);
  219. }
  220. // Provides a culture-correct string comparison. strA is compared to strB
  221. // to determine whether it is lexicographically less, equal, or greater, and then a
  222. // negative integer, 0, or a positive integer is returned; respectively.
  223. // The case-sensitive option is set by ignoreCase, and the culture is set
  224. // by culture
  225. //
  226. public static int Compare(string? strA, string? strB, bool ignoreCase, CultureInfo? culture)
  227. {
  228. CompareOptions options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None;
  229. return Compare(strA, strB, culture, options);
  230. }
  231. // Determines whether two string regions match. The substring of strA beginning
  232. // at indexA of given length is compared with the substring of strB
  233. // beginning at indexB of the same length.
  234. //
  235. public static int Compare(string? strA, int indexA, string? strB, int indexB, int length)
  236. {
  237. // NOTE: It's important we call the boolean overload, and not the StringComparison
  238. // one. The two have some subtly different behavior (see notes in the former).
  239. return Compare(strA, indexA, strB, indexB, length, ignoreCase: false);
  240. }
  241. // Determines whether two string regions match. The substring of strA beginning
  242. // at indexA of given length is compared with the substring of strB
  243. // beginning at indexB of the same length. Case sensitivity is determined by the ignoreCase boolean.
  244. //
  245. public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, bool ignoreCase)
  246. {
  247. // Ideally we would just forward to the string.Compare overload that takes
  248. // a StringComparison parameter, and just pass in CurrentCulture/CurrentCultureIgnoreCase.
  249. // That function will return early if an optimization can be applied, e.g. if
  250. // (object)strA == strB && indexA == indexB then it will return 0 straightaway.
  251. // There are a couple of subtle behavior differences that prevent us from doing so
  252. // however:
  253. // - string.Compare(null, -1, null, -1, -1, StringComparison.CurrentCulture) works
  254. // since that method also returns early for nulls before validation. It shouldn't
  255. // for this overload.
  256. // - Since we originally forwarded to CompareInfo.Compare for all of the argument
  257. // validation logic, the ArgumentOutOfRangeExceptions thrown will contain different
  258. // parameter names.
  259. // Therefore, we have to duplicate some of the logic here.
  260. int lengthA = length;
  261. int lengthB = length;
  262. if (strA != null)
  263. {
  264. lengthA = Math.Min(lengthA, strA.Length - indexA);
  265. }
  266. if (strB != null)
  267. {
  268. lengthB = Math.Min(lengthB, strB.Length - indexB);
  269. }
  270. CompareOptions options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None;
  271. return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, options);
  272. }
  273. // Determines whether two string regions match. The substring of strA beginning
  274. // at indexA of length length is compared with the substring of strB
  275. // beginning at indexB of the same length. Case sensitivity is determined by the ignoreCase boolean,
  276. // and the culture is set by culture.
  277. //
  278. public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, bool ignoreCase, CultureInfo? culture)
  279. {
  280. CompareOptions options = ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None;
  281. return Compare(strA, indexA, strB, indexB, length, culture, options);
  282. }
  283. // Determines whether two string regions match. The substring of strA beginning
  284. // at indexA of length length is compared with the substring of strB
  285. // beginning at indexB of the same length.
  286. //
  287. public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, CultureInfo? culture, CompareOptions options)
  288. {
  289. CultureInfo compareCulture = culture ?? CultureInfo.CurrentCulture;
  290. int lengthA = length;
  291. int lengthB = length;
  292. if (strA != null)
  293. {
  294. lengthA = Math.Min(lengthA, strA.Length - indexA);
  295. }
  296. if (strB != null)
  297. {
  298. lengthB = Math.Min(lengthB, strB.Length - indexB);
  299. }
  300. return compareCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, options);
  301. }
  302. public static int Compare(string? strA, int indexA, string? strB, int indexB, int length, StringComparison comparisonType)
  303. {
  304. CheckStringComparison(comparisonType);
  305. if (strA == null || strB == null)
  306. {
  307. if (object.ReferenceEquals(strA, strB))
  308. {
  309. // They're both null
  310. return 0;
  311. }
  312. return strA == null ? -1 : 1;
  313. }
  314. if (length < 0)
  315. {
  316. throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NegativeLength);
  317. }
  318. if (indexA < 0 || indexB < 0)
  319. {
  320. string paramName = indexA < 0 ? nameof(indexA) : nameof(indexB);
  321. throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_Index);
  322. }
  323. if (strA.Length - indexA < 0 || strB.Length - indexB < 0)
  324. {
  325. string paramName = strA.Length - indexA < 0 ? nameof(indexA) : nameof(indexB);
  326. throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_Index);
  327. }
  328. if (length == 0 || (object.ReferenceEquals(strA, strB) && indexA == indexB))
  329. {
  330. return 0;
  331. }
  332. int lengthA = Math.Min(length, strA.Length - indexA);
  333. int lengthB = Math.Min(length, strB.Length - indexB);
  334. switch (comparisonType)
  335. {
  336. case StringComparison.CurrentCulture:
  337. case StringComparison.CurrentCultureIgnoreCase:
  338. return CultureInfo.CurrentCulture.CompareInfo.Compare(strA, indexA, lengthA, strB, indexB, lengthB, GetCaseCompareOfComparisonCulture(comparisonType));
  339. case StringComparison.InvariantCulture:
  340. case StringComparison.InvariantCultureIgnoreCase:
  341. return CompareInfo.Invariant.Compare(strA, indexA, lengthA, strB, indexB, lengthB, GetCaseCompareOfComparisonCulture(comparisonType));
  342. case StringComparison.Ordinal:
  343. return CompareOrdinalHelper(strA, indexA, lengthA, strB, indexB, lengthB);
  344. default:
  345. Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase); // CheckStringComparison validated these earlier
  346. return CompareInfo.CompareOrdinalIgnoreCase(strA, indexA, lengthA, strB, indexB, lengthB);
  347. }
  348. }
  349. // Compares strA and strB using an ordinal (code-point) comparison.
  350. //
  351. public static int CompareOrdinal(string? strA, string? strB)
  352. {
  353. if (object.ReferenceEquals(strA, strB))
  354. {
  355. return 0;
  356. }
  357. // They can't both be null at this point.
  358. if (strA == null)
  359. {
  360. return -1;
  361. }
  362. if (strB == null)
  363. {
  364. return 1;
  365. }
  366. // Most common case, first character is different.
  367. // This will return false for empty strings.
  368. if (strA._firstChar != strB._firstChar)
  369. {
  370. return strA._firstChar - strB._firstChar;
  371. }
  372. return CompareOrdinalHelper(strA, strB);
  373. }
  374. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  375. internal static int CompareOrdinal(ReadOnlySpan<char> strA, ReadOnlySpan<char> strB)
  376. => SpanHelpers.SequenceCompareTo(ref MemoryMarshal.GetReference(strA), strA.Length, ref MemoryMarshal.GetReference(strB), strB.Length);
  377. // Compares strA and strB using an ordinal (code-point) comparison.
  378. //
  379. public static int CompareOrdinal(string? strA, int indexA, string? strB, int indexB, int length)
  380. {
  381. if (strA == null || strB == null)
  382. {
  383. if (object.ReferenceEquals(strA, strB))
  384. {
  385. // They're both null
  386. return 0;
  387. }
  388. return strA == null ? -1 : 1;
  389. }
  390. // COMPAT: Checking for nulls should become before the arguments are validated,
  391. // but other optimizations which allow us to return early should come after.
  392. if (length < 0)
  393. {
  394. throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NegativeCount);
  395. }
  396. if (indexA < 0 || indexB < 0)
  397. {
  398. string paramName = indexA < 0 ? nameof(indexA) : nameof(indexB);
  399. throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_Index);
  400. }
  401. int lengthA = Math.Min(length, strA.Length - indexA);
  402. int lengthB = Math.Min(length, strB.Length - indexB);
  403. if (lengthA < 0 || lengthB < 0)
  404. {
  405. string paramName = lengthA < 0 ? nameof(indexA) : nameof(indexB);
  406. throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_Index);
  407. }
  408. if (length == 0 || (object.ReferenceEquals(strA, strB) && indexA == indexB))
  409. {
  410. return 0;
  411. }
  412. return CompareOrdinalHelper(strA, indexA, lengthA, strB, indexB, lengthB);
  413. }
  414. // Compares this String to another String (cast as object), returning an integer that
  415. // indicates the relationship. This method returns a value less than 0 if this is less than value, 0
  416. // if this is equal to value, or a value greater than 0 if this is greater than value.
  417. //
  418. public int CompareTo(object? value)
  419. {
  420. if (value == null)
  421. {
  422. return 1;
  423. }
  424. if (!(value is string other))
  425. {
  426. throw new ArgumentException(SR.Arg_MustBeString);
  427. }
  428. return CompareTo(other); // will call the string-based overload
  429. }
  430. // Determines the sorting relation of StrB to the current instance.
  431. //
  432. public int CompareTo(string? strB)
  433. {
  434. return string.Compare(this, strB, StringComparison.CurrentCulture);
  435. }
  436. // Determines whether a specified string is a suffix of the current instance.
  437. //
  438. // The case-sensitive and culture-sensitive option is set by options,
  439. // and the default culture is used.
  440. //
  441. public bool EndsWith(string value)
  442. {
  443. return EndsWith(value, StringComparison.CurrentCulture);
  444. }
  445. public bool EndsWith(string value, StringComparison comparisonType)
  446. {
  447. if ((object)value == null)
  448. {
  449. throw new ArgumentNullException(nameof(value));
  450. }
  451. if ((object)this == (object)value)
  452. {
  453. CheckStringComparison(comparisonType);
  454. return true;
  455. }
  456. if (value.Length == 0)
  457. {
  458. CheckStringComparison(comparisonType);
  459. return true;
  460. }
  461. switch (comparisonType)
  462. {
  463. case StringComparison.CurrentCulture:
  464. case StringComparison.CurrentCultureIgnoreCase:
  465. return CultureInfo.CurrentCulture.CompareInfo.IsSuffix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
  466. case StringComparison.InvariantCulture:
  467. case StringComparison.InvariantCultureIgnoreCase:
  468. return CompareInfo.Invariant.IsSuffix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
  469. case StringComparison.Ordinal:
  470. int offset = this.Length - value.Length;
  471. return (uint)offset <= (uint)this.Length && this.AsSpan(offset).SequenceEqual(value);
  472. case StringComparison.OrdinalIgnoreCase:
  473. return this.Length < value.Length ? false : (CompareInfo.CompareOrdinalIgnoreCase(this, this.Length - value.Length, value.Length, value, 0, value.Length) == 0);
  474. default:
  475. throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
  476. }
  477. }
  478. public bool EndsWith(string value, bool ignoreCase, CultureInfo? culture)
  479. {
  480. if (null == value)
  481. {
  482. throw new ArgumentNullException(nameof(value));
  483. }
  484. if ((object)this == (object)value)
  485. {
  486. return true;
  487. }
  488. CultureInfo referenceCulture = culture ?? CultureInfo.CurrentCulture;
  489. return referenceCulture.CompareInfo.IsSuffix(this, value, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
  490. }
  491. public bool EndsWith(char value)
  492. {
  493. int lastPos = Length - 1;
  494. return ((uint)lastPos < (uint)Length) && this[lastPos] == value;
  495. }
  496. // Determines whether two strings match.
  497. public override bool Equals(object? obj)
  498. {
  499. if (object.ReferenceEquals(this, obj))
  500. return true;
  501. if (!(obj is string str))
  502. return false;
  503. if (this.Length != str.Length)
  504. return false;
  505. return EqualsHelper(this, str);
  506. }
  507. // Determines whether two strings match.
  508. public bool Equals(string? value)
  509. {
  510. if (object.ReferenceEquals(this, value))
  511. return true;
  512. // NOTE: No need to worry about casting to object here.
  513. // If either side of an == comparison between strings
  514. // is null, Roslyn generates a simple ceq instruction
  515. // instead of calling string.op_Equality.
  516. if (value == null)
  517. return false;
  518. if (this.Length != value.Length)
  519. return false;
  520. return EqualsHelper(this, value);
  521. }
  522. public bool Equals(string? value, StringComparison comparisonType)
  523. {
  524. if (object.ReferenceEquals(this, value))
  525. {
  526. CheckStringComparison(comparisonType);
  527. return true;
  528. }
  529. if (value is null)
  530. {
  531. CheckStringComparison(comparisonType);
  532. return false;
  533. }
  534. switch (comparisonType)
  535. {
  536. case StringComparison.CurrentCulture:
  537. case StringComparison.CurrentCultureIgnoreCase:
  538. return CultureInfo.CurrentCulture.CompareInfo.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType)) == 0;
  539. case StringComparison.InvariantCulture:
  540. case StringComparison.InvariantCultureIgnoreCase:
  541. return CompareInfo.Invariant.Compare(this, value, GetCaseCompareOfComparisonCulture(comparisonType)) == 0;
  542. case StringComparison.Ordinal:
  543. if (this.Length != value.Length)
  544. return false;
  545. return EqualsHelper(this, value);
  546. case StringComparison.OrdinalIgnoreCase:
  547. if (this.Length != value.Length)
  548. return false;
  549. return EqualsOrdinalIgnoreCase(this, value);
  550. default:
  551. throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
  552. }
  553. }
  554. // Determines whether two Strings match.
  555. public static bool Equals(string? a, string? b)
  556. {
  557. if (object.ReferenceEquals(a, b))
  558. {
  559. return true;
  560. }
  561. if (a is null || b is null || a.Length != b.Length)
  562. {
  563. return false;
  564. }
  565. return EqualsHelper(a, b);
  566. }
  567. public static bool Equals(string? a, string? b, StringComparison comparisonType)
  568. {
  569. if (object.ReferenceEquals(a, b))
  570. {
  571. CheckStringComparison(comparisonType);
  572. return true;
  573. }
  574. if (a is null || b is null)
  575. {
  576. CheckStringComparison(comparisonType);
  577. return false;
  578. }
  579. switch (comparisonType)
  580. {
  581. case StringComparison.CurrentCulture:
  582. case StringComparison.CurrentCultureIgnoreCase:
  583. return CultureInfo.CurrentCulture.CompareInfo.Compare(a, b, GetCaseCompareOfComparisonCulture(comparisonType)) == 0;
  584. case StringComparison.InvariantCulture:
  585. case StringComparison.InvariantCultureIgnoreCase:
  586. return CompareInfo.Invariant.Compare(a, b, GetCaseCompareOfComparisonCulture(comparisonType)) == 0;
  587. case StringComparison.Ordinal:
  588. if (a.Length != b.Length)
  589. return false;
  590. return EqualsHelper(a, b);
  591. case StringComparison.OrdinalIgnoreCase:
  592. if (a.Length != b.Length)
  593. return false;
  594. return EqualsOrdinalIgnoreCase(a, b);
  595. default:
  596. throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
  597. }
  598. }
  599. public static bool operator ==(string? a, string? b) => string.Equals(a, b);
  600. public static bool operator !=(string? a, string? b) => !string.Equals(a, b);
  601. // Gets a hash code for this string. If strings A and B are such that A.Equals(B), then
  602. // they will return the same hash code.
  603. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  604. public override int GetHashCode()
  605. {
  606. ulong seed = Marvin.DefaultSeed;
  607. // Multiplication below will not overflow since going from positive Int32 to UInt32.
  608. return Marvin.ComputeHash32(ref Unsafe.As<char, byte>(ref _firstChar), (uint)_stringLength * 2 /* in bytes, not chars */, (uint)seed, (uint)(seed >> 32));
  609. }
  610. // Gets a hash code for this string and this comparison. If strings A and B and comparison C are such
  611. // that string.Equals(A, B, C), then they will return the same hash code with this comparison C.
  612. public int GetHashCode(StringComparison comparisonType) => StringComparer.FromComparison(comparisonType).GetHashCode(this);
  613. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  614. internal int GetHashCodeOrdinalIgnoreCase()
  615. {
  616. ulong seed = Marvin.DefaultSeed;
  617. return Marvin.ComputeHash32OrdinalIgnoreCase(ref _firstChar, _stringLength /* in chars, not bytes */, (uint)seed, (uint)(seed >> 32));
  618. }
  619. // A span-based equivalent of String.GetHashCode(). Computes an ordinal hash code.
  620. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  621. public static int GetHashCode(ReadOnlySpan<char> value)
  622. {
  623. ulong seed = Marvin.DefaultSeed;
  624. // Multiplication below will not overflow since going from positive Int32 to UInt32.
  625. 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));
  626. }
  627. // A span-based equivalent of String.GetHashCode(StringComparison). Uses the specified comparison type.
  628. public static int GetHashCode(ReadOnlySpan<char> value, StringComparison comparisonType)
  629. {
  630. switch (comparisonType)
  631. {
  632. case StringComparison.CurrentCulture:
  633. case StringComparison.CurrentCultureIgnoreCase:
  634. return CultureInfo.CurrentCulture.CompareInfo.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType));
  635. case StringComparison.InvariantCulture:
  636. case StringComparison.InvariantCultureIgnoreCase:
  637. return CultureInfo.InvariantCulture.CompareInfo.GetHashCode(value, GetCaseCompareOfComparisonCulture(comparisonType));
  638. case StringComparison.Ordinal:
  639. return GetHashCode(value);
  640. case StringComparison.OrdinalIgnoreCase:
  641. return GetHashCodeOrdinalIgnoreCase(value);
  642. default:
  643. ThrowHelper.ThrowArgumentException(ExceptionResource.NotSupported_StringComparison, ExceptionArgument.comparisonType);
  644. Debug.Fail("Should not reach this point.");
  645. return default;
  646. }
  647. }
  648. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  649. internal static int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> value)
  650. {
  651. ulong seed = Marvin.DefaultSeed;
  652. return Marvin.ComputeHash32OrdinalIgnoreCase(ref MemoryMarshal.GetReference(value), value.Length /* in chars, not bytes */, (uint)seed, (uint)(seed >> 32));
  653. }
  654. // Use this if and only if 'Denial of Service' attacks are not a concern (i.e. never used for free-form user input),
  655. // or are otherwise mitigated
  656. internal unsafe int GetNonRandomizedHashCode()
  657. {
  658. fixed (char* src = &_firstChar)
  659. {
  660. Debug.Assert(src[this.Length] == '\0', "src[this.Length] == '\\0'");
  661. Debug.Assert(((int)src) % 4 == 0, "Managed string should start at 4 bytes boundary");
  662. uint hash1 = (5381 << 16) + 5381;
  663. uint hash2 = hash1;
  664. uint* ptr = (uint*)src;
  665. int length = this.Length;
  666. while (length > 2)
  667. {
  668. length -= 4;
  669. // Where length is 4n-1 (e.g. 3,7,11,15,19) this additionally consumes the null terminator
  670. hash1 = (BitOperations.RotateLeft(hash1, 5) + hash1) ^ ptr[0];
  671. hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptr[1];
  672. ptr += 2;
  673. }
  674. if (length > 0)
  675. {
  676. // Where length is 4n-3 (e.g. 1,5,9,13,17) this additionally consumes the null terminator
  677. hash2 = (BitOperations.RotateLeft(hash2, 5) + hash2) ^ ptr[0];
  678. }
  679. return (int)(hash1 + (hash2 * 1566083941));
  680. }
  681. }
  682. // Determines whether a specified string is a prefix of the current instance
  683. //
  684. public bool StartsWith(string value)
  685. {
  686. if (value is null)
  687. {
  688. throw new ArgumentNullException(nameof(value));
  689. }
  690. return StartsWith(value, StringComparison.CurrentCulture);
  691. }
  692. public bool StartsWith(string value, StringComparison comparisonType)
  693. {
  694. if (value is null)
  695. {
  696. throw new ArgumentNullException(nameof(value));
  697. }
  698. if ((object)this == (object)value)
  699. {
  700. CheckStringComparison(comparisonType);
  701. return true;
  702. }
  703. if (value.Length == 0)
  704. {
  705. CheckStringComparison(comparisonType);
  706. return true;
  707. }
  708. switch (comparisonType)
  709. {
  710. case StringComparison.CurrentCulture:
  711. case StringComparison.CurrentCultureIgnoreCase:
  712. return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
  713. case StringComparison.InvariantCulture:
  714. case StringComparison.InvariantCultureIgnoreCase:
  715. return CompareInfo.Invariant.IsPrefix(this, value, GetCaseCompareOfComparisonCulture(comparisonType));
  716. case StringComparison.Ordinal:
  717. if (this.Length < value.Length || _firstChar != value._firstChar)
  718. {
  719. return false;
  720. }
  721. return (value.Length == 1) ?
  722. true : // First char is the same and thats all there is to compare
  723. SpanHelpers.SequenceEqual(
  724. ref Unsafe.As<char, byte>(ref this.GetRawStringData()),
  725. ref Unsafe.As<char, byte>(ref value.GetRawStringData()),
  726. ((nuint)value.Length) * 2);
  727. case StringComparison.OrdinalIgnoreCase:
  728. if (this.Length < value.Length)
  729. {
  730. return false;
  731. }
  732. return CompareInfo.EqualsOrdinalIgnoreCase(ref this.GetRawStringData(), ref value.GetRawStringData(), value.Length);
  733. default:
  734. throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
  735. }
  736. }
  737. public bool StartsWith(string value, bool ignoreCase, CultureInfo? culture)
  738. {
  739. if (null == value)
  740. {
  741. throw new ArgumentNullException(nameof(value));
  742. }
  743. if ((object)this == (object)value)
  744. {
  745. return true;
  746. }
  747. CultureInfo referenceCulture = culture ?? CultureInfo.CurrentCulture;
  748. return referenceCulture.CompareInfo.IsPrefix(this, value, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
  749. }
  750. public bool StartsWith(char value) => Length != 0 && _firstChar == value;
  751. internal static void CheckStringComparison(StringComparison comparisonType)
  752. {
  753. // Single comparison to check if comparisonType is within [CurrentCulture .. OrdinalIgnoreCase]
  754. if ((uint)comparisonType > (uint)StringComparison.OrdinalIgnoreCase)
  755. {
  756. ThrowHelper.ThrowArgumentException(ExceptionResource.NotSupported_StringComparison, ExceptionArgument.comparisonType);
  757. }
  758. }
  759. internal static CompareOptions GetCaseCompareOfComparisonCulture(StringComparison comparisonType)
  760. {
  761. Debug.Assert((uint)comparisonType <= (uint)StringComparison.OrdinalIgnoreCase);
  762. // Culture enums can be & with CompareOptions.IgnoreCase 0x01 to extract if IgnoreCase or CompareOptions.None 0x00
  763. //
  764. // CompareOptions.None 0x00
  765. // CompareOptions.IgnoreCase 0x01
  766. //
  767. // StringComparison.CurrentCulture: 0x00
  768. // StringComparison.InvariantCulture: 0x02
  769. // StringComparison.Ordinal 0x04
  770. //
  771. // StringComparison.CurrentCultureIgnoreCase: 0x01
  772. // StringComparison.InvariantCultureIgnoreCase: 0x03
  773. // StringComparison.OrdinalIgnoreCase 0x05
  774. return (CompareOptions)((int)comparisonType & (int)CompareOptions.IgnoreCase);
  775. }
  776. }
  777. }