String.Comparison.cs 38 KB

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