1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Globalization;
6 using System.Runtime.InteropServices;
10 public partial class String
12 public bool Contains(string value)
14 return (IndexOf(value, StringComparison.Ordinal) >= 0);
17 public bool Contains(string value, StringComparison comparisonType)
19 return (IndexOf(value, comparisonType) >= 0);
22 public bool Contains(char value)
24 return IndexOf(value) != -1;
27 public bool Contains(char value, StringComparison comparisonType)
29 return IndexOf(value, comparisonType) != -1;
32 // Returns the index of the first occurrence of a specified character in the current instance.
33 // The search starts at startIndex and runs thorough the next count characters.
35 public int IndexOf(char value)
37 return IndexOf(value, 0, this.Length);
40 public int IndexOf(char value, int startIndex)
42 return IndexOf(value, startIndex, this.Length - startIndex);
45 public int IndexOf(char value, StringComparison comparisonType)
47 switch (comparisonType)
49 case StringComparison.CurrentCulture:
50 return CultureInfo.CurrentCulture.CompareInfo.IndexOf(this, value, CompareOptions.None);
52 case StringComparison.CurrentCultureIgnoreCase:
53 return CultureInfo.CurrentCulture.CompareInfo.IndexOf(this, value, CompareOptions.IgnoreCase);
55 case StringComparison.InvariantCulture:
56 return CultureInfo.InvariantCulture.CompareInfo.IndexOf(this, value, CompareOptions.None);
58 case StringComparison.InvariantCultureIgnoreCase:
59 return CultureInfo.InvariantCulture.CompareInfo.IndexOf(this, value, CompareOptions.IgnoreCase);
61 case StringComparison.Ordinal:
62 return CultureInfo.InvariantCulture.CompareInfo.IndexOf(this, value, CompareOptions.Ordinal);
64 case StringComparison.OrdinalIgnoreCase:
65 return CultureInfo.InvariantCulture.CompareInfo.IndexOf(this, value, CompareOptions.OrdinalIgnoreCase);
68 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
72 public unsafe int IndexOf(char value, int startIndex, int count)
74 if (startIndex < 0 || startIndex > Length)
75 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
77 if (count < 0 || count > Length - startIndex)
78 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
80 fixed (char* pChars = &_firstChar)
82 char* pCh = pChars + startIndex;
86 if (*pCh == value) goto ReturnIndex;
87 if (*(pCh + 1) == value) goto ReturnIndex1;
88 if (*(pCh + 2) == value) goto ReturnIndex2;
89 if (*(pCh + 3) == value) goto ReturnIndex3;
110 return (int)(pCh - pChars);
114 // Returns the index of the first occurrence of any specified character in the current instance.
115 // The search starts at startIndex and runs to startIndex + count - 1.
117 public int IndexOfAny(char[] anyOf)
119 return IndexOfAny(anyOf, 0, this.Length);
122 public int IndexOfAny(char[] anyOf, int startIndex)
124 return IndexOfAny(anyOf, startIndex, this.Length - startIndex);
127 public int IndexOfAny(char[] anyOf, int startIndex, int count)
130 throw new ArgumentNullException(nameof(anyOf));
132 if ((uint)startIndex > (uint)Length)
133 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
135 if ((uint)count > (uint)(Length - startIndex))
136 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
138 if (anyOf.Length == 2)
140 // Very common optimization for directory separators (/, \), quotes (", '), brackets, etc
141 return IndexOfAny(anyOf[0], anyOf[1], startIndex, count);
143 else if (anyOf.Length == 3)
145 return IndexOfAny(anyOf[0], anyOf[1], anyOf[2], startIndex, count);
147 else if (anyOf.Length > 3)
149 return IndexOfCharArray(anyOf, startIndex, count);
151 else if (anyOf.Length == 1)
153 return IndexOf(anyOf[0], startIndex, count);
155 else // anyOf.Length == 0
161 private unsafe int IndexOfAny(char value1, char value2, int startIndex, int count)
163 fixed (char* pChars = &_firstChar)
165 char* pCh = pChars + startIndex;
171 if (c == value1 || c == value2)
172 return (int)(pCh - pChars);
174 // Possibly reads outside of count and can include null terminator
175 // Handled in the return logic
178 if (c == value1 || c == value2)
179 return (count == 1 ? -1 : (int)(pCh - pChars) + 1);
189 private unsafe int IndexOfAny(char value1, char value2, char value3, int startIndex, int count)
191 fixed (char* pChars = &_firstChar)
193 char* pCh = pChars + startIndex;
199 if (c == value1 || c == value2 || c == value3)
200 return (int)(pCh - pChars);
210 private unsafe int IndexOfCharArray(char[] anyOf, int startIndex, int count)
212 // use probabilistic map, see InitializeProbabilisticMap
213 ProbabilisticMap map = default(ProbabilisticMap);
214 uint* charMap = (uint*)↦
216 InitializeProbabilisticMap(charMap, anyOf);
218 fixed (char* pChars = &_firstChar)
220 char* pCh = pChars + startIndex;
226 if (IsCharBitSet(charMap, (byte)thisChar) &&
227 IsCharBitSet(charMap, (byte)(thisChar >> 8)) &&
228 ArrayContains((char)thisChar, anyOf))
230 return (int)(pCh - pChars);
241 private const int PROBABILISTICMAP_BLOCK_INDEX_MASK = 0x7;
242 private const int PROBABILISTICMAP_BLOCK_INDEX_SHIFT = 0x3;
243 private const int PROBABILISTICMAP_SIZE = 0x8;
245 // A probabilistic map is an optimization that is used in IndexOfAny/
246 // LastIndexOfAny methods. The idea is to create a bit map of the characters we
247 // are searching for and use this map as a "cheap" check to decide if the
248 // current character in the string exists in the array of input characters.
249 // There are 256 bits in the map, with each character mapped to 2 bits. Every
250 // character is divided into 2 bytes, and then every byte is mapped to 1 bit.
251 // The character map is an array of 8 integers acting as map blocks. The 3 lsb
252 // in each byte in the character is used to index into this map to get the
253 // right block, the value of the remaining 5 msb are used as the bit position
254 // inside this block.
255 private static unsafe void InitializeProbabilisticMap(uint* charMap, ReadOnlySpan<char> anyOf)
257 bool hasAscii = false;
258 uint* charMapLocal = charMap; // https://github.com/dotnet/coreclr/issues/14264
260 for (int i = 0; i < anyOf.Length; ++i)
265 SetCharBit(charMapLocal, (byte)c);
276 SetCharBit(charMapLocal, (byte)c);
282 // Common to search for ASCII symbols. Just set the high value once.
283 charMapLocal[0] |= 1u;
287 private static bool ArrayContains(char searchChar, char[] anyOf)
289 for (int i = 0; i < anyOf.Length; i++)
291 if (anyOf[i] == searchChar)
298 private unsafe static bool IsCharBitSet(uint* charMap, byte value)
300 return (charMap[value & PROBABILISTICMAP_BLOCK_INDEX_MASK] & (1u << (value >> PROBABILISTICMAP_BLOCK_INDEX_SHIFT))) != 0;
303 private unsafe static void SetCharBit(uint* charMap, byte value)
305 charMap[value & PROBABILISTICMAP_BLOCK_INDEX_MASK] |= 1u << (value >> PROBABILISTICMAP_BLOCK_INDEX_SHIFT);
308 public int IndexOf(String value)
310 return IndexOf(value, StringComparison.CurrentCulture);
313 public int IndexOf(String value, int startIndex)
315 return IndexOf(value, startIndex, StringComparison.CurrentCulture);
318 public int IndexOf(String value, int startIndex, int count)
320 if (startIndex < 0 || startIndex > this.Length)
322 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
325 if (count < 0 || count > this.Length - startIndex)
327 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
330 return IndexOf(value, startIndex, count, StringComparison.CurrentCulture);
333 public int IndexOf(String value, StringComparison comparisonType)
335 return IndexOf(value, 0, this.Length, comparisonType);
338 public int IndexOf(String value, int startIndex, StringComparison comparisonType)
340 return IndexOf(value, startIndex, this.Length - startIndex, comparisonType);
343 public int IndexOf(String value, int startIndex, int count, StringComparison comparisonType)
347 throw new ArgumentNullException(nameof(value));
349 if (startIndex < 0 || startIndex > this.Length)
350 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
352 if (count < 0 || startIndex > this.Length - count)
353 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
355 switch (comparisonType)
357 case StringComparison.CurrentCulture:
358 return CultureInfo.CurrentCulture.CompareInfo.IndexOf(this, value, startIndex, count, CompareOptions.None);
360 case StringComparison.CurrentCultureIgnoreCase:
361 return CultureInfo.CurrentCulture.CompareInfo.IndexOf(this, value, startIndex, count, CompareOptions.IgnoreCase);
363 case StringComparison.InvariantCulture:
364 return CultureInfo.InvariantCulture.CompareInfo.IndexOf(this, value, startIndex, count, CompareOptions.None);
366 case StringComparison.InvariantCultureIgnoreCase:
367 return CultureInfo.InvariantCulture.CompareInfo.IndexOf(this, value, startIndex, count, CompareOptions.IgnoreCase);
369 case StringComparison.Ordinal:
370 return CultureInfo.InvariantCulture.CompareInfo.IndexOf(this, value, startIndex, count, CompareOptions.Ordinal);
372 case StringComparison.OrdinalIgnoreCase:
373 return TextInfo.IndexOfStringOrdinalIgnoreCase(this, value, startIndex, count);
376 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
380 // Returns the index of the last occurrence of a specified character in the current instance.
381 // The search starts at startIndex and runs backwards to startIndex - count + 1.
382 // The character at position startIndex is included in the search. startIndex is the larger
383 // index within the string.
385 public int LastIndexOf(char value)
387 return LastIndexOf(value, this.Length - 1, this.Length);
390 public int LastIndexOf(char value, int startIndex)
392 return LastIndexOf(value, startIndex, startIndex + 1);
395 public unsafe int LastIndexOf(char value, int startIndex, int count)
400 if (startIndex < 0 || startIndex >= Length)
401 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
403 if (count < 0 || count - 1 > startIndex)
404 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
406 fixed (char* pChars = &_firstChar)
408 char* pCh = pChars + startIndex;
410 //We search [startIndex..EndIndex]
413 if (*pCh == value) goto ReturnIndex;
414 if (*(pCh - 1) == value) goto ReturnIndex1;
415 if (*(pCh - 2) == value) goto ReturnIndex2;
416 if (*(pCh - 3) == value) goto ReturnIndex3;
437 return (int)(pCh - pChars);
441 // Returns the index of the last occurrence of any specified character in the current instance.
442 // The search starts at startIndex and runs backwards to startIndex - count + 1.
443 // The character at position startIndex is included in the search. startIndex is the larger
444 // index within the string.
446 public int LastIndexOfAny(char[] anyOf)
448 return LastIndexOfAny(anyOf, this.Length - 1, this.Length);
451 public int LastIndexOfAny(char[] anyOf, int startIndex)
453 return LastIndexOfAny(anyOf, startIndex, startIndex + 1);
456 public unsafe int LastIndexOfAny(char[] anyOf, int startIndex, int count)
459 throw new ArgumentNullException(nameof(anyOf));
464 if ((uint)startIndex >= (uint)Length)
466 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
469 if ((count < 0) || ((count - 1) > startIndex))
471 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
474 if (anyOf.Length > 1)
476 return LastIndexOfCharArray(anyOf, startIndex, count);
478 else if (anyOf.Length == 1)
480 return LastIndexOf(anyOf[0], startIndex, count);
482 else // anyOf.Length == 0
488 private unsafe int LastIndexOfCharArray(char[] anyOf, int startIndex, int count)
490 // use probabilistic map, see InitializeProbabilisticMap
491 ProbabilisticMap map = default(ProbabilisticMap);
492 uint* charMap = (uint*)↦
494 InitializeProbabilisticMap(charMap, anyOf);
496 fixed (char* pChars = &_firstChar)
498 char* pCh = pChars + startIndex;
504 if (IsCharBitSet(charMap, (byte)thisChar) &&
505 IsCharBitSet(charMap, (byte)(thisChar >> 8)) &&
506 ArrayContains((char)thisChar, anyOf))
508 return (int)(pCh - pChars);
519 // Returns the index of the last occurrence of any character in value in the current instance.
520 // The search starts at startIndex and runs backwards to startIndex - count + 1.
521 // The character at position startIndex is included in the search. startIndex is the larger
522 // index within the string.
524 public int LastIndexOf(String value)
526 return LastIndexOf(value, this.Length - 1, this.Length, StringComparison.CurrentCulture);
529 public int LastIndexOf(String value, int startIndex)
531 return LastIndexOf(value, startIndex, startIndex + 1, StringComparison.CurrentCulture);
534 public int LastIndexOf(String value, int startIndex, int count)
538 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
541 return LastIndexOf(value, startIndex, count, StringComparison.CurrentCulture);
544 public int LastIndexOf(String value, StringComparison comparisonType)
546 return LastIndexOf(value, this.Length - 1, this.Length, comparisonType);
549 public int LastIndexOf(String value, int startIndex, StringComparison comparisonType)
551 return LastIndexOf(value, startIndex, startIndex + 1, comparisonType);
554 public int LastIndexOf(String value, int startIndex, int count, StringComparison comparisonType)
557 throw new ArgumentNullException(nameof(value));
559 // Special case for 0 length input strings
560 if (this.Length == 0 && (startIndex == -1 || startIndex == 0))
561 return (value.Length == 0) ? 0 : -1;
563 // Now after handling empty strings, make sure we're not out of range
564 if (startIndex < 0 || startIndex > this.Length)
565 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
567 // Make sure that we allow startIndex == this.Length
568 if (startIndex == this.Length)
575 // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
576 if (count < 0 || startIndex - count + 1 < 0)
577 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
579 // If we are looking for nothing, just return startIndex
580 if (value.Length == 0)
583 switch (comparisonType)
585 case StringComparison.CurrentCulture:
586 return CultureInfo.CurrentCulture.CompareInfo.LastIndexOf(this, value, startIndex, count, CompareOptions.None);
588 case StringComparison.CurrentCultureIgnoreCase:
589 return CultureInfo.CurrentCulture.CompareInfo.LastIndexOf(this, value, startIndex, count, CompareOptions.IgnoreCase);
591 case StringComparison.InvariantCulture:
592 return CultureInfo.InvariantCulture.CompareInfo.LastIndexOf(this, value, startIndex, count, CompareOptions.None);
594 case StringComparison.InvariantCultureIgnoreCase:
595 return CultureInfo.InvariantCulture.CompareInfo.LastIndexOf(this, value, startIndex, count, CompareOptions.IgnoreCase);
597 case StringComparison.Ordinal:
598 return CultureInfo.InvariantCulture.CompareInfo.LastIndexOf(this, value, startIndex, count, CompareOptions.Ordinal);
600 case StringComparison.OrdinalIgnoreCase:
601 return TextInfo.LastIndexOfStringOrdinalIgnoreCase(this, value, startIndex, count);
604 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
608 [StructLayout(LayoutKind.Explicit, Size = PROBABILISTICMAP_SIZE * sizeof(uint))]
609 private struct ProbabilisticMap { }