CompareInfo.cs 55 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467
  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.Reflection;
  6. using System.Runtime.CompilerServices;
  7. using System.Runtime.InteropServices;
  8. using System.Runtime.Serialization;
  9. using System.Text.Unicode;
  10. using Internal.Runtime.CompilerServices;
  11. namespace System.Globalization
  12. {
  13. /// <summary>
  14. /// This class implements a set of methods for comparing strings.
  15. /// </summary>
  16. [Serializable]
  17. [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
  18. public partial class CompareInfo : IDeserializationCallback
  19. {
  20. // Mask used to check if IndexOf()/LastIndexOf()/IsPrefix()/IsPostfix() has the right flags.
  21. private const CompareOptions ValidIndexMaskOffFlags =
  22. ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
  23. CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType);
  24. // Mask used to check if Compare() has the right flags.
  25. private const CompareOptions ValidCompareMaskOffFlags =
  26. ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
  27. CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.StringSort);
  28. // Mask used to check if GetHashCodeOfString() has the right flags.
  29. private const CompareOptions ValidHashCodeOfStringMaskOffFlags =
  30. ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
  31. CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType);
  32. // Mask used to check if we have the right flags.
  33. private const CompareOptions ValidSortkeyCtorMaskOffFlags =
  34. ~(CompareOptions.IgnoreCase | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreNonSpace |
  35. CompareOptions.IgnoreWidth | CompareOptions.IgnoreKanaType | CompareOptions.StringSort);
  36. // Cache the invariant CompareInfo
  37. internal static readonly CompareInfo Invariant = CultureInfo.InvariantCulture.CompareInfo;
  38. // CompareInfos have an interesting identity. They are attached to the locale that created them,
  39. // ie: en-US would have an en-US sort. For haw-US (custom), then we serialize it as haw-US.
  40. // The interesting part is that since haw-US doesn't have its own sort, it has to point at another
  41. // locale, which is what SCOMPAREINFO does.
  42. [OptionalField(VersionAdded = 2)]
  43. private string m_name; // The name used to construct this CompareInfo. Do not rename (binary serialization)
  44. [NonSerialized]
  45. private string _sortName = null!; // The name that defines our behavior
  46. [OptionalField(VersionAdded = 3)]
  47. private SortVersion? m_SortVersion; // Do not rename (binary serialization)
  48. private int culture; // Do not rename (binary serialization). The fields sole purpose is to support Desktop serialization.
  49. internal CompareInfo(CultureInfo culture)
  50. {
  51. m_name = culture._name;
  52. InitSort(culture);
  53. }
  54. /// <summary>
  55. /// Get the CompareInfo constructed from the data table in the specified
  56. /// assembly for the specified culture.
  57. /// Warning: The assembly versioning mechanism is dead!
  58. /// </summary>
  59. public static CompareInfo GetCompareInfo(int culture, Assembly assembly)
  60. {
  61. // Parameter checking.
  62. if (assembly == null)
  63. {
  64. throw new ArgumentNullException(nameof(assembly));
  65. }
  66. if (assembly != typeof(object).Module.Assembly)
  67. {
  68. throw new ArgumentException(SR.Argument_OnlyMscorlib, nameof(assembly));
  69. }
  70. return GetCompareInfo(culture);
  71. }
  72. /// <summary>
  73. /// Get the CompareInfo constructed from the data table in the specified
  74. /// assembly for the specified culture.
  75. /// The purpose of this method is to provide version for CompareInfo tables.
  76. /// </summary>
  77. public static CompareInfo GetCompareInfo(string name, Assembly assembly)
  78. {
  79. if (name == null)
  80. {
  81. throw new ArgumentNullException(nameof(name));
  82. }
  83. if (assembly == null)
  84. {
  85. throw new ArgumentNullException(nameof(assembly));
  86. }
  87. if (assembly != typeof(object).Module.Assembly)
  88. {
  89. throw new ArgumentException(SR.Argument_OnlyMscorlib, nameof(assembly));
  90. }
  91. return GetCompareInfo(name);
  92. }
  93. /// <summary>
  94. /// Get the CompareInfo for the specified culture.
  95. /// This method is provided for ease of integration with NLS-based software.
  96. /// </summary>
  97. public static CompareInfo GetCompareInfo(int culture)
  98. {
  99. if (CultureData.IsCustomCultureId(culture))
  100. {
  101. throw new ArgumentException(SR.Argument_CustomCultureCannotBePassedByNumber, nameof(culture));
  102. }
  103. return CultureInfo.GetCultureInfo(culture).CompareInfo;
  104. }
  105. /// <summary>
  106. /// Get the CompareInfo for the specified culture.
  107. /// </summary>
  108. public static CompareInfo GetCompareInfo(string name)
  109. {
  110. if (name == null)
  111. {
  112. throw new ArgumentNullException(nameof(name));
  113. }
  114. return CultureInfo.GetCultureInfo(name).CompareInfo;
  115. }
  116. public static unsafe bool IsSortable(char ch)
  117. {
  118. if (GlobalizationMode.Invariant)
  119. {
  120. return true;
  121. }
  122. char *pChar = &ch;
  123. return IsSortable(pChar, 1);
  124. }
  125. public static unsafe bool IsSortable(string text)
  126. {
  127. if (text == null)
  128. {
  129. throw new ArgumentNullException(nameof(text));
  130. }
  131. if (text.Length == 0)
  132. {
  133. return false;
  134. }
  135. if (GlobalizationMode.Invariant)
  136. {
  137. return true;
  138. }
  139. fixed (char *pChar = text)
  140. {
  141. return IsSortable(pChar, text.Length);
  142. }
  143. }
  144. [OnDeserializing]
  145. private void OnDeserializing(StreamingContext ctx)
  146. {
  147. // this becomes null for a brief moment before deserialization
  148. // after serialization is finished it is never null.
  149. m_name = null!;
  150. }
  151. void IDeserializationCallback.OnDeserialization(object? sender)
  152. {
  153. OnDeserialized();
  154. }
  155. [OnDeserialized]
  156. private void OnDeserialized(StreamingContext ctx)
  157. {
  158. OnDeserialized();
  159. }
  160. private void OnDeserialized()
  161. {
  162. // If we didn't have a name, use the LCID
  163. if (m_name == null)
  164. {
  165. // From whidbey, didn't have a name
  166. m_name = CultureInfo.GetCultureInfo(culture)._name;
  167. }
  168. else
  169. {
  170. InitSort(CultureInfo.GetCultureInfo(m_name));
  171. }
  172. }
  173. [OnSerializing]
  174. private void OnSerializing(StreamingContext ctx)
  175. {
  176. // This is merely for serialization compatibility with Whidbey/Orcas, it can go away when we don't want that compat any more.
  177. culture = CultureInfo.GetCultureInfo(Name).LCID; // This is the lcid of the constructing culture (still have to dereference to get target sort)
  178. Debug.Assert(m_name != null, "CompareInfo.OnSerializing - expected m_name to be set already");
  179. }
  180. /// <summary>
  181. /// Returns the name of the culture (well actually, of the sort).
  182. /// Very important for providing a non-LCID way of identifying
  183. /// what the sort is.
  184. ///
  185. /// Note that this name isn't dereferenced in case the CompareInfo is a different locale
  186. /// which is consistent with the behaviors of earlier versions. (so if you ask for a sort
  187. /// and the locale's changed behavior, then you'll get changed behavior, which is like
  188. /// what happens for a version update)
  189. /// </summary>
  190. public virtual string Name
  191. {
  192. get
  193. {
  194. Debug.Assert(m_name != null, "CompareInfo.Name Expected _name to be set");
  195. if (m_name == "zh-CHT" || m_name == "zh-CHS")
  196. {
  197. return m_name;
  198. }
  199. return _sortName;
  200. }
  201. }
  202. /// <summary>
  203. /// Compares the two strings with the given options. Returns 0 if the
  204. /// two strings are equal, a number less than 0 if string1 is less
  205. /// than string2, and a number greater than 0 if string1 is greater
  206. /// than string2.
  207. /// </summary>
  208. public virtual int Compare(string? string1, string? string2)
  209. {
  210. return Compare(string1, string2, CompareOptions.None);
  211. }
  212. public virtual int Compare(string? string1, string? string2, CompareOptions options)
  213. {
  214. if (options == CompareOptions.OrdinalIgnoreCase)
  215. {
  216. return string.Compare(string1, string2, StringComparison.OrdinalIgnoreCase);
  217. }
  218. // Verify the options before we do any real comparison.
  219. if ((options & CompareOptions.Ordinal) != 0)
  220. {
  221. if (options != CompareOptions.Ordinal)
  222. {
  223. throw new ArgumentException(SR.Argument_CompareOptionOrdinal, nameof(options));
  224. }
  225. return string.CompareOrdinal(string1, string2);
  226. }
  227. if ((options & ValidCompareMaskOffFlags) != 0)
  228. {
  229. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  230. }
  231. // Our paradigm is that null sorts less than any other string and
  232. // that two nulls sort as equal.
  233. if (string1 == null)
  234. {
  235. if (string2 == null)
  236. {
  237. return 0;
  238. }
  239. return -1; // null < non-null
  240. }
  241. if (string2 == null)
  242. {
  243. return 1; // non-null > null
  244. }
  245. if (GlobalizationMode.Invariant)
  246. {
  247. if ((options & CompareOptions.IgnoreCase) != 0)
  248. {
  249. return CompareOrdinalIgnoreCase(string1, string2);
  250. }
  251. return string.CompareOrdinal(string1, string2);
  252. }
  253. return CompareString(string1.AsSpan(), string2.AsSpan(), options);
  254. }
  255. // TODO https://github.com/dotnet/coreclr/issues/13827:
  256. // This method shouldn't be necessary, as we should be able to just use the overload
  257. // that takes two spans. But due to this issue, that's adding significant overhead.
  258. internal int Compare(ReadOnlySpan<char> string1, string? string2, CompareOptions options)
  259. {
  260. if (options == CompareOptions.OrdinalIgnoreCase)
  261. {
  262. return CompareOrdinalIgnoreCase(string1, string2.AsSpan());
  263. }
  264. // Verify the options before we do any real comparison.
  265. if ((options & CompareOptions.Ordinal) != 0)
  266. {
  267. if (options != CompareOptions.Ordinal)
  268. {
  269. throw new ArgumentException(SR.Argument_CompareOptionOrdinal, nameof(options));
  270. }
  271. return string.CompareOrdinal(string1, string2.AsSpan());
  272. }
  273. if ((options & ValidCompareMaskOffFlags) != 0)
  274. {
  275. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  276. }
  277. // null sorts less than any other string.
  278. if (string2 == null)
  279. {
  280. return 1;
  281. }
  282. if (GlobalizationMode.Invariant)
  283. {
  284. return (options & CompareOptions.IgnoreCase) != 0 ?
  285. CompareOrdinalIgnoreCase(string1, string2.AsSpan()) :
  286. string.CompareOrdinal(string1, string2.AsSpan());
  287. }
  288. return CompareString(string1, string2, options);
  289. }
  290. internal int CompareOptionNone(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2)
  291. {
  292. // Check for empty span or span from a null string
  293. if (string1.Length == 0 || string2.Length == 0)
  294. {
  295. return string1.Length - string2.Length;
  296. }
  297. return GlobalizationMode.Invariant ?
  298. string.CompareOrdinal(string1, string2) :
  299. CompareString(string1, string2, CompareOptions.None);
  300. }
  301. internal int CompareOptionIgnoreCase(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2)
  302. {
  303. // Check for empty span or span from a null string
  304. if (string1.Length == 0 || string2.Length == 0)
  305. {
  306. return string1.Length - string2.Length;
  307. }
  308. return GlobalizationMode.Invariant ?
  309. CompareOrdinalIgnoreCase(string1, string2) :
  310. CompareString(string1, string2, CompareOptions.IgnoreCase);
  311. }
  312. /// <summary>
  313. /// Compares the specified regions of the two strings with the given
  314. /// options.
  315. /// Returns 0 if the two strings are equal, a number less than 0 if
  316. /// string1 is less than string2, and a number greater than 0 if
  317. /// string1 is greater than string2.
  318. /// </summary>
  319. public virtual int Compare(string? string1, int offset1, int length1, string? string2, int offset2, int length2)
  320. {
  321. return Compare(string1, offset1, length1, string2, offset2, length2, 0);
  322. }
  323. public virtual int Compare(string? string1, int offset1, string? string2, int offset2, CompareOptions options)
  324. {
  325. return Compare(string1, offset1, string1 == null ? 0 : string1.Length - offset1,
  326. string2, offset2, string2 == null ? 0 : string2.Length - offset2, options);
  327. }
  328. public virtual int Compare(string? string1, int offset1, string? string2, int offset2)
  329. {
  330. return Compare(string1, offset1, string2, offset2, 0);
  331. }
  332. public virtual int Compare(string? string1, int offset1, int length1, string? string2, int offset2, int length2, CompareOptions options)
  333. {
  334. if (options == CompareOptions.OrdinalIgnoreCase)
  335. {
  336. int result = string.Compare(string1, offset1, string2, offset2, length1 < length2 ? length1 : length2, StringComparison.OrdinalIgnoreCase);
  337. if ((length1 != length2) && result == 0)
  338. {
  339. return length1 > length2 ? 1 : -1;
  340. }
  341. return result;
  342. }
  343. if (length1 < 0 || length2 < 0)
  344. {
  345. throw new ArgumentOutOfRangeException((length1 < 0) ? nameof(length1) : nameof(length2), SR.ArgumentOutOfRange_NeedPosNum);
  346. }
  347. if (offset1 < 0 || offset2 < 0)
  348. {
  349. throw new ArgumentOutOfRangeException((offset1 < 0) ? nameof(offset1) : nameof(offset2), SR.ArgumentOutOfRange_NeedPosNum);
  350. }
  351. if (offset1 > (string1 == null ? 0 : string1.Length) - length1)
  352. {
  353. throw new ArgumentOutOfRangeException(nameof(string1), SR.ArgumentOutOfRange_OffsetLength);
  354. }
  355. if (offset2 > (string2 == null ? 0 : string2.Length) - length2)
  356. {
  357. throw new ArgumentOutOfRangeException(nameof(string2), SR.ArgumentOutOfRange_OffsetLength);
  358. }
  359. if ((options & CompareOptions.Ordinal) != 0)
  360. {
  361. if (options != CompareOptions.Ordinal)
  362. {
  363. throw new ArgumentException(SR.Argument_CompareOptionOrdinal,
  364. nameof(options));
  365. }
  366. }
  367. else if ((options & ValidCompareMaskOffFlags) != 0)
  368. {
  369. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  370. }
  371. if (string1 == null)
  372. {
  373. if (string2 == null)
  374. {
  375. return 0;
  376. }
  377. return -1;
  378. }
  379. if (string2 == null)
  380. {
  381. return 1;
  382. }
  383. ReadOnlySpan<char> span1 = string1.AsSpan(offset1, length1);
  384. ReadOnlySpan<char> span2 = string2.AsSpan(offset2, length2);
  385. if (options == CompareOptions.Ordinal)
  386. {
  387. return string.CompareOrdinal(span1, span2);
  388. }
  389. if (GlobalizationMode.Invariant)
  390. {
  391. if ((options & CompareOptions.IgnoreCase) != 0)
  392. {
  393. return CompareOrdinalIgnoreCase(span1, span2);
  394. }
  395. return string.CompareOrdinal(span1, span2);
  396. }
  397. return CompareString(span1, span2, options);
  398. }
  399. /// <summary>
  400. /// CompareOrdinalIgnoreCase compare two string ordinally with ignoring the case.
  401. /// it assumes the strings are Ascii string till we hit non Ascii character in strA or strB and then we continue the comparison by
  402. /// calling the OS.
  403. /// </summary>
  404. internal static int CompareOrdinalIgnoreCase(string strA, int indexA, int lengthA, string strB, int indexB, int lengthB)
  405. {
  406. Debug.Assert(indexA + lengthA <= strA.Length);
  407. Debug.Assert(indexB + lengthB <= strB.Length);
  408. return CompareOrdinalIgnoreCase(
  409. ref Unsafe.Add(ref strA.GetRawStringData(), indexA),
  410. lengthA,
  411. ref Unsafe.Add(ref strB.GetRawStringData(), indexB),
  412. lengthB);
  413. }
  414. internal static int CompareOrdinalIgnoreCase(ReadOnlySpan<char> strA, ReadOnlySpan<char> strB)
  415. {
  416. return CompareOrdinalIgnoreCase(ref MemoryMarshal.GetReference(strA), strA.Length, ref MemoryMarshal.GetReference(strB), strB.Length);
  417. }
  418. internal static int CompareOrdinalIgnoreCase(string strA, string strB)
  419. {
  420. return CompareOrdinalIgnoreCase(ref strA.GetRawStringData(), strA.Length, ref strB.GetRawStringData(), strB.Length);
  421. }
  422. internal static int CompareOrdinalIgnoreCase(ref char strA, int lengthA, ref char strB, int lengthB)
  423. {
  424. int length = Math.Min(lengthA, lengthB);
  425. int range = length;
  426. ref char charA = ref strA;
  427. ref char charB = ref strB;
  428. // in InvariantMode we support all range and not only the ascii characters.
  429. char maxChar = (GlobalizationMode.Invariant ? (char)0xFFFF : (char)0x7F);
  430. while (length != 0 && charA <= maxChar && charB <= maxChar)
  431. {
  432. // Ordinal equals or lowercase equals if the result ends up in the a-z range
  433. if (charA == charB ||
  434. ((charA | 0x20) == (charB | 0x20) &&
  435. (uint)((charA | 0x20) - 'a') <= (uint)('z' - 'a')))
  436. {
  437. length--;
  438. charA = ref Unsafe.Add(ref charA, 1);
  439. charB = ref Unsafe.Add(ref charB, 1);
  440. }
  441. else
  442. {
  443. int currentA = charA;
  444. int currentB = charB;
  445. // Uppercase both chars if needed
  446. if ((uint)(charA - 'a') <= 'z' - 'a')
  447. {
  448. currentA -= 0x20;
  449. }
  450. if ((uint)(charB - 'a') <= 'z' - 'a')
  451. {
  452. currentB -= 0x20;
  453. }
  454. // Return the (case-insensitive) difference between them.
  455. return currentA - currentB;
  456. }
  457. }
  458. if (length == 0)
  459. {
  460. return lengthA - lengthB;
  461. }
  462. Debug.Assert(!GlobalizationMode.Invariant);
  463. range -= length;
  464. return CompareStringOrdinalIgnoreCase(ref charA, lengthA - range, ref charB, lengthB - range);
  465. }
  466. internal static bool EqualsOrdinalIgnoreCase(ref char charA, ref char charB, int length)
  467. {
  468. IntPtr byteOffset = IntPtr.Zero;
  469. #if BIT64
  470. // Read 4 chars (64 bits) at a time from each string
  471. while ((uint)length >= 4)
  472. {
  473. ulong valueA = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref Unsafe.AddByteOffset(ref charA, byteOffset)));
  474. ulong valueB = Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<char, byte>(ref Unsafe.AddByteOffset(ref charB, byteOffset)));
  475. // A 32-bit test - even with the bit-twiddling here - is more efficient than a 64-bit test.
  476. ulong temp = valueA | valueB;
  477. if (!Utf16Utility.AllCharsInUInt32AreAscii((uint)temp | (uint)(temp >> 32)))
  478. {
  479. goto NonAscii; // one of the inputs contains non-ASCII data
  480. }
  481. // Generally, the caller has likely performed a first-pass check that the input strings
  482. // are likely equal. Consider a dictionary which computes the hash code of its key before
  483. // performing a proper deep equality check of the string contents. We want to optimize for
  484. // the case where the equality check is likely to succeed, which means that we want to avoid
  485. // branching within this loop unless we're about to exit the loop, either due to failure or
  486. // due to us running out of input data.
  487. if (!Utf16Utility.UInt64OrdinalIgnoreCaseAscii(valueA, valueB))
  488. {
  489. return false;
  490. }
  491. byteOffset += 8;
  492. length -= 4;
  493. }
  494. #endif
  495. // Read 2 chars (32 bits) at a time from each string
  496. #if BIT64
  497. if ((uint)length >= 2)
  498. #else
  499. while ((uint)length >= 2)
  500. #endif
  501. {
  502. uint valueA = Unsafe.ReadUnaligned<uint>(ref Unsafe.As<char, byte>(ref Unsafe.AddByteOffset(ref charA, byteOffset)));
  503. uint valueB = Unsafe.ReadUnaligned<uint>(ref Unsafe.As<char, byte>(ref Unsafe.AddByteOffset(ref charB, byteOffset)));
  504. if (!Utf16Utility.AllCharsInUInt32AreAscii(valueA | valueB))
  505. {
  506. goto NonAscii; // one of the inputs contains non-ASCII data
  507. }
  508. // Generally, the caller has likely performed a first-pass check that the input strings
  509. // are likely equal. Consider a dictionary which computes the hash code of its key before
  510. // performing a proper deep equality check of the string contents. We want to optimize for
  511. // the case where the equality check is likely to succeed, which means that we want to avoid
  512. // branching within this loop unless we're about to exit the loop, either due to failure or
  513. // due to us running out of input data.
  514. if (!Utf16Utility.UInt32OrdinalIgnoreCaseAscii(valueA, valueB))
  515. {
  516. return false;
  517. }
  518. byteOffset += 4;
  519. length -= 2;
  520. }
  521. if (length != 0)
  522. {
  523. Debug.Assert(length == 1);
  524. uint valueA = Unsafe.AddByteOffset(ref charA, byteOffset);
  525. uint valueB = Unsafe.AddByteOffset(ref charB, byteOffset);
  526. if ((valueA | valueB) > 0x7Fu)
  527. {
  528. goto NonAscii; // one of the inputs contains non-ASCII data
  529. }
  530. if (valueA == valueB)
  531. {
  532. return true; // exact match
  533. }
  534. valueA |= 0x20u;
  535. if ((uint)(valueA - 'a') > (uint)('z' - 'a'))
  536. {
  537. return false; // not exact match, and first input isn't in [A-Za-z]
  538. }
  539. // The ternary operator below seems redundant but helps RyuJIT generate more optimal code.
  540. // See https://github.com/dotnet/coreclr/issues/914.
  541. return (valueA == (valueB | 0x20u)) ? true : false;
  542. }
  543. Debug.Assert(length == 0);
  544. return true;
  545. NonAscii:
  546. // The non-ASCII case is factored out into its own helper method so that the JIT
  547. // doesn't need to emit a complex prolog for its caller (this method).
  548. return EqualsOrdinalIgnoreCaseNonAscii(ref Unsafe.AddByteOffset(ref charA, byteOffset), ref Unsafe.AddByteOffset(ref charB, byteOffset), length);
  549. }
  550. private static bool EqualsOrdinalIgnoreCaseNonAscii(ref char charA, ref char charB, int length)
  551. {
  552. if (!GlobalizationMode.Invariant)
  553. {
  554. return CompareStringOrdinalIgnoreCase(ref charA, length, ref charB, length) == 0;
  555. }
  556. else
  557. {
  558. // If we don't have localization tables to consult, we'll still perform a case-insensitive
  559. // check for ASCII characters, but if we see anything outside the ASCII range we'll immediately
  560. // fail if it doesn't have true bitwise equality.
  561. IntPtr byteOffset = IntPtr.Zero;
  562. while (length != 0)
  563. {
  564. // Ordinal equals or lowercase equals if the result ends up in the a-z range
  565. uint valueA = Unsafe.AddByteOffset(ref charA, byteOffset);
  566. uint valueB = Unsafe.AddByteOffset(ref charB, byteOffset);
  567. if (valueA == valueB ||
  568. ((valueA | 0x20) == (valueB | 0x20) &&
  569. (uint)((valueA | 0x20) - 'a') <= (uint)('z' - 'a')))
  570. {
  571. byteOffset += 2;
  572. length--;
  573. }
  574. else
  575. {
  576. return false;
  577. }
  578. }
  579. return true;
  580. }
  581. }
  582. /// <summary>
  583. /// Determines whether prefix is a prefix of string. If prefix equals
  584. /// string.Empty, true is returned.
  585. /// </summary>
  586. public virtual bool IsPrefix(string source, string prefix, CompareOptions options)
  587. {
  588. if (source == null)
  589. {
  590. throw new ArgumentNullException(nameof(source));
  591. }
  592. if (prefix == null)
  593. {
  594. throw new ArgumentNullException(nameof(prefix));
  595. }
  596. if (prefix.Length == 0)
  597. {
  598. return true;
  599. }
  600. if (source.Length == 0)
  601. {
  602. return false;
  603. }
  604. if (options == CompareOptions.OrdinalIgnoreCase)
  605. {
  606. return source.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
  607. }
  608. if (options == CompareOptions.Ordinal)
  609. {
  610. return source.StartsWith(prefix, StringComparison.Ordinal);
  611. }
  612. if ((options & ValidIndexMaskOffFlags) != 0)
  613. {
  614. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  615. }
  616. if (GlobalizationMode.Invariant)
  617. {
  618. return source.StartsWith(prefix, (options & CompareOptions.IgnoreCase) != 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
  619. }
  620. return StartsWith(source, prefix, options);
  621. }
  622. internal bool IsPrefix(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
  623. {
  624. Debug.Assert(prefix.Length != 0);
  625. Debug.Assert(source.Length != 0);
  626. Debug.Assert((options & ValidIndexMaskOffFlags) == 0);
  627. Debug.Assert(!GlobalizationMode.Invariant);
  628. Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
  629. return StartsWith(source, prefix, options);
  630. }
  631. public virtual bool IsPrefix(string source, string prefix)
  632. {
  633. return IsPrefix(source, prefix, 0);
  634. }
  635. /// <summary>
  636. /// Determines whether suffix is a suffix of string. If suffix equals
  637. /// string.Empty, true is returned.
  638. /// </summary>
  639. public virtual bool IsSuffix(string source, string suffix, CompareOptions options)
  640. {
  641. if (source == null)
  642. {
  643. throw new ArgumentNullException(nameof(source));
  644. }
  645. if (suffix == null)
  646. {
  647. throw new ArgumentNullException(nameof(suffix));
  648. }
  649. if (suffix.Length == 0)
  650. {
  651. return true;
  652. }
  653. if (source.Length == 0)
  654. {
  655. return false;
  656. }
  657. if (options == CompareOptions.OrdinalIgnoreCase)
  658. {
  659. return source.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
  660. }
  661. if (options == CompareOptions.Ordinal)
  662. {
  663. return source.EndsWith(suffix, StringComparison.Ordinal);
  664. }
  665. if ((options & ValidIndexMaskOffFlags) != 0)
  666. {
  667. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  668. }
  669. if (GlobalizationMode.Invariant)
  670. {
  671. return source.EndsWith(suffix, (options & CompareOptions.IgnoreCase) != 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
  672. }
  673. return EndsWith(source, suffix, options);
  674. }
  675. internal bool IsSuffix(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options)
  676. {
  677. Debug.Assert(suffix.Length != 0);
  678. Debug.Assert(source.Length != 0);
  679. Debug.Assert((options & ValidIndexMaskOffFlags) == 0);
  680. Debug.Assert(!GlobalizationMode.Invariant);
  681. Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
  682. return EndsWith(source, suffix, options);
  683. }
  684. public virtual bool IsSuffix(string source, string suffix)
  685. {
  686. return IsSuffix(source, suffix, 0);
  687. }
  688. /// <summary>
  689. /// Returns the first index where value is found in string. The
  690. /// search starts from startIndex and ends at endIndex. Returns -1 if
  691. /// the specified value is not found. If value equals string.Empty,
  692. /// startIndex is returned. Throws IndexOutOfRange if startIndex or
  693. /// endIndex is less than zero or greater than the length of string.
  694. /// Throws ArgumentException if value is null.
  695. /// </summary>
  696. public virtual int IndexOf(string source, char value)
  697. {
  698. if (source == null)
  699. {
  700. throw new ArgumentNullException(nameof(source));
  701. }
  702. return IndexOf(source, value, 0, source.Length, CompareOptions.None);
  703. }
  704. public virtual int IndexOf(string source, string value)
  705. {
  706. if (source == null)
  707. throw new ArgumentNullException(nameof(source));
  708. return IndexOf(source, value, 0, source.Length, CompareOptions.None);
  709. }
  710. public virtual int IndexOf(string source, char value, CompareOptions options)
  711. {
  712. if (source == null)
  713. {
  714. throw new ArgumentNullException(nameof(source));
  715. }
  716. return IndexOf(source, value, 0, source.Length, options);
  717. }
  718. public virtual int IndexOf(string source, string value, CompareOptions options)
  719. {
  720. if (source == null)
  721. {
  722. throw new ArgumentNullException(nameof(source));
  723. }
  724. return IndexOf(source, value, 0, source.Length, options);
  725. }
  726. public virtual int IndexOf(string source, char value, int startIndex)
  727. {
  728. if (source == null)
  729. {
  730. throw new ArgumentNullException(nameof(source));
  731. }
  732. return IndexOf(source, value, startIndex, source.Length - startIndex, CompareOptions.None);
  733. }
  734. public virtual int IndexOf(string source, string value, int startIndex)
  735. {
  736. if (source == null)
  737. throw new ArgumentNullException(nameof(source));
  738. return IndexOf(source, value, startIndex, source.Length - startIndex, CompareOptions.None);
  739. }
  740. public virtual int IndexOf(string source, char value, int startIndex, CompareOptions options)
  741. {
  742. if (source == null)
  743. {
  744. throw new ArgumentNullException(nameof(source));
  745. }
  746. return IndexOf(source, value, startIndex, source.Length - startIndex, options);
  747. }
  748. public virtual int IndexOf(string source, string value, int startIndex, CompareOptions options)
  749. {
  750. if (source == null)
  751. {
  752. throw new ArgumentNullException(nameof(source));
  753. }
  754. return IndexOf(source, value, startIndex, source.Length - startIndex, options);
  755. }
  756. public virtual int IndexOf(string source, char value, int startIndex, int count)
  757. {
  758. return IndexOf(source, value, startIndex, count, CompareOptions.None);
  759. }
  760. public virtual int IndexOf(string source, string value, int startIndex, int count)
  761. {
  762. return IndexOf(source, value, startIndex, count, CompareOptions.None);
  763. }
  764. public unsafe virtual int IndexOf(string source, char value, int startIndex, int count, CompareOptions options)
  765. {
  766. if (source == null)
  767. {
  768. throw new ArgumentNullException(nameof(source));
  769. }
  770. if (startIndex < 0 || startIndex > source.Length)
  771. {
  772. throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
  773. }
  774. if (count < 0 || startIndex > source.Length - count)
  775. {
  776. throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
  777. }
  778. if (source.Length == 0)
  779. {
  780. return -1;
  781. }
  782. // Validate CompareOptions
  783. // Ordinal can't be selected with other flags
  784. if ((options & ValidIndexMaskOffFlags) != 0 && (options != CompareOptions.Ordinal && options != CompareOptions.OrdinalIgnoreCase))
  785. {
  786. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  787. }
  788. return IndexOf(source, char.ToString(value), startIndex, count, options, null);
  789. }
  790. public unsafe virtual int IndexOf(string source, string value, int startIndex, int count, CompareOptions options)
  791. {
  792. if (source == null)
  793. {
  794. throw new ArgumentNullException(nameof(source));
  795. }
  796. if (value == null)
  797. {
  798. throw new ArgumentNullException(nameof(value));
  799. }
  800. if (startIndex > source.Length)
  801. {
  802. throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
  803. }
  804. // In Everett we used to return -1 for empty string even if startIndex is negative number so we keeping same behavior here.
  805. // We return 0 if both source and value are empty strings for Everett compatibility too.
  806. if (source.Length == 0)
  807. {
  808. if (value.Length == 0)
  809. {
  810. return 0;
  811. }
  812. return -1;
  813. }
  814. if (startIndex < 0)
  815. {
  816. throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
  817. }
  818. if (count < 0 || startIndex > source.Length - count)
  819. {
  820. throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
  821. }
  822. // Validate CompareOptions
  823. // Ordinal can't be selected with other flags
  824. if ((options & ValidIndexMaskOffFlags) != 0 && (options != CompareOptions.Ordinal && options != CompareOptions.OrdinalIgnoreCase))
  825. {
  826. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  827. }
  828. return IndexOf(source, value, startIndex, count, options, null);
  829. }
  830. internal int IndexOfOrdinalIgnoreCase(ReadOnlySpan<char> source, ReadOnlySpan<char> value)
  831. {
  832. Debug.Assert(!GlobalizationMode.Invariant);
  833. Debug.Assert(!source.IsEmpty);
  834. Debug.Assert(!value.IsEmpty);
  835. return IndexOfOrdinalCore(source, value, ignoreCase: true, fromBeginning: true);
  836. }
  837. internal int LastIndexOfOrdinal(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool ignoreCase)
  838. {
  839. Debug.Assert(!GlobalizationMode.Invariant);
  840. Debug.Assert(!source.IsEmpty);
  841. Debug.Assert(!value.IsEmpty);
  842. return IndexOfOrdinalCore(source, value, ignoreCase, fromBeginning: false);
  843. }
  844. internal unsafe int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options)
  845. {
  846. Debug.Assert(!GlobalizationMode.Invariant);
  847. Debug.Assert(!source.IsEmpty);
  848. Debug.Assert(!value.IsEmpty);
  849. return IndexOfCore(source, value, options, null, fromBeginning: true);
  850. }
  851. internal unsafe int LastIndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options)
  852. {
  853. Debug.Assert(!GlobalizationMode.Invariant);
  854. Debug.Assert(!source.IsEmpty);
  855. Debug.Assert(!value.IsEmpty);
  856. return IndexOfCore(source, value, options, null, fromBeginning: false);
  857. }
  858. /// <summary>
  859. /// The following IndexOf overload is mainly used by String.Replace. This overload assumes the parameters are already validated
  860. /// and the caller is passing a valid matchLengthPtr pointer.
  861. /// </summary>
  862. internal unsafe int IndexOf(string source, string value, int startIndex, int count, CompareOptions options, int* matchLengthPtr)
  863. {
  864. Debug.Assert(source != null);
  865. Debug.Assert(value != null);
  866. Debug.Assert(startIndex >= 0);
  867. if (matchLengthPtr != null)
  868. {
  869. *matchLengthPtr = 0;
  870. }
  871. if (value.Length == 0)
  872. {
  873. return startIndex;
  874. }
  875. if (startIndex >= source.Length)
  876. {
  877. return -1;
  878. }
  879. if (options == CompareOptions.OrdinalIgnoreCase)
  880. {
  881. int res = IndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
  882. if (res >= 0 && matchLengthPtr != null)
  883. {
  884. *matchLengthPtr = value.Length;
  885. }
  886. return res;
  887. }
  888. if (GlobalizationMode.Invariant)
  889. {
  890. int res = IndexOfOrdinal(source, value, startIndex, count, ignoreCase: (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
  891. if (res >= 0 && matchLengthPtr != null)
  892. {
  893. *matchLengthPtr = value.Length;
  894. }
  895. return res;
  896. }
  897. if (options == CompareOptions.Ordinal)
  898. {
  899. int retValue = SpanHelpers.IndexOf(
  900. ref Unsafe.Add(ref source.GetRawStringData(), startIndex),
  901. count,
  902. ref value.GetRawStringData(),
  903. value.Length);
  904. if (retValue >= 0)
  905. {
  906. retValue += startIndex;
  907. if (matchLengthPtr != null)
  908. {
  909. *matchLengthPtr = value.Length;
  910. }
  911. }
  912. return retValue;
  913. }
  914. else
  915. {
  916. return IndexOfCore(source, value, startIndex, count, options, matchLengthPtr);
  917. }
  918. }
  919. internal int IndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
  920. {
  921. if (!ignoreCase)
  922. {
  923. int result = SpanHelpers.IndexOf(
  924. ref Unsafe.Add(ref source.GetRawStringData(), startIndex),
  925. count,
  926. ref value.GetRawStringData(),
  927. value.Length);
  928. return (result >= 0 ? startIndex : 0) + result;
  929. }
  930. if (GlobalizationMode.Invariant)
  931. {
  932. return InvariantIndexOf(source, value, startIndex, count, ignoreCase);
  933. }
  934. return IndexOfOrdinalCore(source, value, startIndex, count, ignoreCase);
  935. }
  936. /// <summary>
  937. /// Returns the last index where value is found in string. The
  938. /// search starts from startIndex and ends at endIndex. Returns -1 if
  939. /// the specified value is not found. If value equals string.Empty,
  940. /// endIndex is returned. Throws IndexOutOfRange if startIndex or
  941. /// endIndex is less than zero or greater than the length of string.
  942. /// Throws ArgumentException if value is null.
  943. /// </summary>
  944. public virtual int LastIndexOf(string source, char value)
  945. {
  946. if (source == null)
  947. {
  948. throw new ArgumentNullException(nameof(source));
  949. }
  950. // Can't start at negative index, so make sure we check for the length == 0 case.
  951. return LastIndexOf(source, value, source.Length - 1, source.Length, CompareOptions.None);
  952. }
  953. public virtual int LastIndexOf(string source, string value)
  954. {
  955. if (source == null)
  956. {
  957. throw new ArgumentNullException(nameof(source));
  958. }
  959. // Can't start at negative index, so make sure we check for the length == 0 case.
  960. return LastIndexOf(source, value, source.Length - 1,
  961. source.Length, CompareOptions.None);
  962. }
  963. public virtual int LastIndexOf(string source, char value, CompareOptions options)
  964. {
  965. if (source == null)
  966. {
  967. throw new ArgumentNullException(nameof(source));
  968. }
  969. // Can't start at negative index, so make sure we check for the length == 0 case.
  970. return LastIndexOf(source, value, source.Length - 1, source.Length, options);
  971. }
  972. public virtual int LastIndexOf(string source, string value, CompareOptions options)
  973. {
  974. if (source == null)
  975. {
  976. throw new ArgumentNullException(nameof(source));
  977. }
  978. // Can't start at negative index, so make sure we check for the length == 0 case.
  979. return LastIndexOf(source, value, source.Length - 1, source.Length, options);
  980. }
  981. public virtual int LastIndexOf(string source, char value, int startIndex)
  982. {
  983. return LastIndexOf(source, value, startIndex, startIndex + 1, CompareOptions.None);
  984. }
  985. public virtual int LastIndexOf(string source, string value, int startIndex)
  986. {
  987. return LastIndexOf(source, value, startIndex, startIndex + 1, CompareOptions.None);
  988. }
  989. public virtual int LastIndexOf(string source, char value, int startIndex, CompareOptions options)
  990. {
  991. return LastIndexOf(source, value, startIndex, startIndex + 1, options);
  992. }
  993. public virtual int LastIndexOf(string source, string value, int startIndex, CompareOptions options)
  994. {
  995. return LastIndexOf(source, value, startIndex, startIndex + 1, options);
  996. }
  997. public virtual int LastIndexOf(string source, char value, int startIndex, int count)
  998. {
  999. return LastIndexOf(source, value, startIndex, count, CompareOptions.None);
  1000. }
  1001. public virtual int LastIndexOf(string source, string value, int startIndex, int count)
  1002. {
  1003. return LastIndexOf(source, value, startIndex, count, CompareOptions.None);
  1004. }
  1005. public virtual int LastIndexOf(string source, char value, int startIndex, int count, CompareOptions options)
  1006. {
  1007. if (source == null)
  1008. {
  1009. throw new ArgumentNullException(nameof(source));
  1010. }
  1011. // Validate CompareOptions
  1012. // Ordinal can't be selected with other flags
  1013. if ((options & ValidIndexMaskOffFlags) != 0 &&
  1014. (options != CompareOptions.Ordinal) &&
  1015. (options != CompareOptions.OrdinalIgnoreCase))
  1016. {
  1017. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  1018. }
  1019. // Special case for 0 length input strings
  1020. if (source.Length == 0 && (startIndex == -1 || startIndex == 0))
  1021. {
  1022. return -1;
  1023. }
  1024. // Make sure we're not out of range
  1025. if (startIndex < 0 || startIndex > source.Length)
  1026. {
  1027. throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
  1028. }
  1029. // Make sure that we allow startIndex == source.Length
  1030. if (startIndex == source.Length)
  1031. {
  1032. startIndex--;
  1033. if (count > 0)
  1034. {
  1035. count--;
  1036. }
  1037. }
  1038. // 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
  1039. if (count < 0 || startIndex - count + 1 < 0)
  1040. {
  1041. throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
  1042. }
  1043. if (options == CompareOptions.OrdinalIgnoreCase)
  1044. {
  1045. return source.LastIndexOf(value.ToString(), startIndex, count, StringComparison.OrdinalIgnoreCase);
  1046. }
  1047. if (GlobalizationMode.Invariant)
  1048. {
  1049. return InvariantLastIndexOf(source, char.ToString(value), startIndex, count, (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
  1050. }
  1051. return LastIndexOfCore(source, value.ToString(), startIndex, count, options);
  1052. }
  1053. public virtual int LastIndexOf(string source, string value, int startIndex, int count, CompareOptions options)
  1054. {
  1055. if (source == null)
  1056. {
  1057. throw new ArgumentNullException(nameof(source));
  1058. }
  1059. if (value == null)
  1060. {
  1061. throw new ArgumentNullException(nameof(value));
  1062. }
  1063. // Validate CompareOptions
  1064. // Ordinal can't be selected with other flags
  1065. if ((options & ValidIndexMaskOffFlags) != 0 &&
  1066. (options != CompareOptions.Ordinal) &&
  1067. (options != CompareOptions.OrdinalIgnoreCase))
  1068. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  1069. // Special case for 0 length input strings
  1070. if (source.Length == 0 && (startIndex == -1 || startIndex == 0))
  1071. {
  1072. return (value.Length == 0) ? 0 : -1;
  1073. }
  1074. // Make sure we're not out of range
  1075. if (startIndex < 0 || startIndex > source.Length)
  1076. {
  1077. throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
  1078. }
  1079. // Make sure that we allow startIndex == source.Length
  1080. if (startIndex == source.Length)
  1081. {
  1082. startIndex--;
  1083. if (count > 0)
  1084. {
  1085. count--;
  1086. }
  1087. // If we are looking for nothing, just return 0
  1088. if (value.Length == 0 && count >= 0 && startIndex - count + 1 >= 0)
  1089. {
  1090. return startIndex;
  1091. }
  1092. }
  1093. // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
  1094. if (count < 0 || startIndex - count + 1 < 0)
  1095. {
  1096. throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
  1097. }
  1098. if (options == CompareOptions.OrdinalIgnoreCase)
  1099. {
  1100. return LastIndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
  1101. }
  1102. if (GlobalizationMode.Invariant)
  1103. return InvariantLastIndexOf(source, value, startIndex, count, (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
  1104. return LastIndexOfCore(source, value, startIndex, count, options);
  1105. }
  1106. internal int LastIndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
  1107. {
  1108. if (GlobalizationMode.Invariant)
  1109. {
  1110. return InvariantLastIndexOf(source, value, startIndex, count, ignoreCase);
  1111. }
  1112. return LastIndexOfOrdinalCore(source, value, startIndex, count, ignoreCase);
  1113. }
  1114. /// <summary>
  1115. /// Gets the SortKey for the given string with the given options.
  1116. /// </summary>
  1117. public virtual SortKey GetSortKey(string source, CompareOptions options)
  1118. {
  1119. if (GlobalizationMode.Invariant)
  1120. {
  1121. return InvariantCreateSortKey(source, options);
  1122. }
  1123. return CreateSortKey(source, options);
  1124. }
  1125. public virtual SortKey GetSortKey(string source)
  1126. {
  1127. if (GlobalizationMode.Invariant)
  1128. {
  1129. return InvariantCreateSortKey(source, CompareOptions.None);
  1130. }
  1131. return CreateSortKey(source, CompareOptions.None);
  1132. }
  1133. public override bool Equals(object? value)
  1134. {
  1135. return value is CompareInfo otherCompareInfo
  1136. && Name == otherCompareInfo.Name;
  1137. }
  1138. public override int GetHashCode() => Name.GetHashCode();
  1139. /// <summary>
  1140. /// This internal method allows a method that allows the equivalent of creating a Sortkey for a
  1141. /// string from CompareInfo, and generate a hashcode value from it. It is not very convenient
  1142. /// to use this method as is and it creates an unnecessary Sortkey object that will be GC'ed.
  1143. ///
  1144. /// The hash code is guaranteed to be the same for string A and B where A.Equals(B) is true and both
  1145. /// the CompareInfo and the CompareOptions are the same. If two different CompareInfo objects
  1146. /// treat the string the same way, this implementation will treat them differently (the same way that
  1147. /// Sortkey does at the moment).
  1148. ///
  1149. /// This method will never be made public itself, but public consumers of it could be created, e.g.:
  1150. ///
  1151. /// string.GetHashCode(CultureInfo)
  1152. /// string.GetHashCode(CompareInfo)
  1153. /// string.GetHashCode(CultureInfo, CompareOptions)
  1154. /// string.GetHashCode(CompareInfo, CompareOptions)
  1155. /// etc.
  1156. ///
  1157. /// (the methods above that take a CultureInfo would use CultureInfo.CompareInfo)
  1158. /// </summary>
  1159. internal int GetHashCodeOfString(string source, CompareOptions options)
  1160. {
  1161. if (source == null)
  1162. {
  1163. throw new ArgumentNullException(nameof(source));
  1164. }
  1165. if ((options & ValidHashCodeOfStringMaskOffFlags) == 0)
  1166. {
  1167. // No unsupported flags are set - continue on with the regular logic
  1168. if (GlobalizationMode.Invariant)
  1169. {
  1170. return ((options & CompareOptions.IgnoreCase) != 0) ? source.GetHashCodeOrdinalIgnoreCase() : source.GetHashCode();
  1171. }
  1172. return GetHashCodeOfStringCore(source, options);
  1173. }
  1174. else if (options == CompareOptions.Ordinal)
  1175. {
  1176. // We allow Ordinal in isolation
  1177. return source.GetHashCode();
  1178. }
  1179. else if (options == CompareOptions.OrdinalIgnoreCase)
  1180. {
  1181. // We allow OrdinalIgnoreCase in isolation
  1182. return source.GetHashCodeOrdinalIgnoreCase();
  1183. }
  1184. else
  1185. {
  1186. // Unsupported combination of flags specified
  1187. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  1188. }
  1189. }
  1190. public virtual int GetHashCode(string source, CompareOptions options)
  1191. {
  1192. // virtual method delegates to non-virtual method
  1193. return GetHashCodeOfString(source, options);
  1194. }
  1195. public int GetHashCode(ReadOnlySpan<char> source, CompareOptions options)
  1196. {
  1197. if ((options & ValidHashCodeOfStringMaskOffFlags) == 0)
  1198. {
  1199. // No unsupported flags are set - continue on with the regular logic
  1200. if (GlobalizationMode.Invariant)
  1201. {
  1202. return ((options & CompareOptions.IgnoreCase) != 0) ? string.GetHashCodeOrdinalIgnoreCase(source) : string.GetHashCode(source);
  1203. }
  1204. return GetHashCodeOfStringCore(source, options);
  1205. }
  1206. else if (options == CompareOptions.Ordinal)
  1207. {
  1208. // We allow Ordinal in isolation
  1209. return string.GetHashCode(source);
  1210. }
  1211. else if (options == CompareOptions.OrdinalIgnoreCase)
  1212. {
  1213. // We allow OrdinalIgnoreCase in isolation
  1214. return string.GetHashCodeOrdinalIgnoreCase(source);
  1215. }
  1216. else
  1217. {
  1218. // Unsupported combination of flags specified
  1219. throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
  1220. }
  1221. }
  1222. public override string ToString() => "CompareInfo - " + Name;
  1223. public SortVersion Version
  1224. {
  1225. get
  1226. {
  1227. if (m_SortVersion == null)
  1228. {
  1229. if (GlobalizationMode.Invariant)
  1230. {
  1231. m_SortVersion = new SortVersion(0, CultureInfo.LOCALE_INVARIANT, new Guid(0, 0, 0, 0, 0, 0, 0,
  1232. (byte) (CultureInfo.LOCALE_INVARIANT >> 24),
  1233. (byte) ((CultureInfo.LOCALE_INVARIANT & 0x00FF0000) >> 16),
  1234. (byte) ((CultureInfo.LOCALE_INVARIANT & 0x0000FF00) >> 8),
  1235. (byte) (CultureInfo.LOCALE_INVARIANT & 0xFF)));
  1236. }
  1237. else
  1238. {
  1239. m_SortVersion = GetSortVersion();
  1240. }
  1241. }
  1242. return m_SortVersion;
  1243. }
  1244. }
  1245. public int LCID => CultureInfo.GetCultureInfo(Name).LCID;
  1246. }
  1247. }