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.
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Globalization;
9 using System.Runtime.CompilerServices;
10 using System.Runtime.InteropServices;
12 using Internal.Runtime.CompilerServices;
16 public partial class String
18 private const int StackallocIntBufferSizeLimit = 128;
20 private static unsafe void FillStringChecked(string dest, int destPos, string src)
22 Debug.Assert(dest != null);
23 Debug.Assert(src != null);
24 if (src.Length > dest.Length - destPos)
26 throw new IndexOutOfRangeException();
29 fixed (char* pDest = &dest._firstChar)
30 fixed (char* pSrc = &src._firstChar)
32 wstrcpy(pDest + destPos, pSrc, src.Length);
36 public static string Concat(object arg0)
42 return arg0.ToString();
45 public static string Concat(object arg0, object arg1)
56 return Concat(arg0.ToString(), arg1.ToString());
59 public static string Concat(object arg0, object arg1, object arg2)
76 return Concat(arg0.ToString(), arg1.ToString(), arg2.ToString());
79 public static string Concat(params object[] args)
83 throw new ArgumentNullException(nameof(args));
88 return args.Length == 0 ?
90 args[0]?.ToString() ?? string.Empty;
93 // We need to get an intermediary string array
94 // to fill with each of the args' ToString(),
95 // and then just concat that in one operation.
97 // This way we avoid any intermediary string representations,
98 // or buffer resizing if we use StringBuilder (although the
99 // latter case is partially alleviated due to StringBuilder's
100 // linked-list style implementation)
102 var strings = new string[args.Length];
106 for (int i = 0; i < args.Length; i++)
108 object value = args[i];
110 string toString = value?.ToString() ?? string.Empty; // We need to handle both the cases when value or value.ToString() is null
111 strings[i] = toString;
113 totalLength += toString.Length;
115 if (totalLength < 0) // Check for a positive overflow
117 throw new OutOfMemoryException();
121 // If all of the ToStrings are null/empty, just return string.Empty
122 if (totalLength == 0)
127 string result = FastAllocateString(totalLength);
128 int position = 0; // How many characters we've copied so far
130 for (int i = 0; i < strings.Length; i++)
132 string s = strings[i];
134 Debug.Assert(s != null);
135 Debug.Assert(position <= totalLength - s.Length, "We didn't allocate enough space for the result string!");
137 FillStringChecked(result, position, s);
138 position += s.Length;
144 public static string Concat<T>(IEnumerable<T> values)
147 throw new ArgumentNullException(nameof(values));
149 if (typeof(T) == typeof(char))
151 // Special-case T==char, as we can handle that case much more efficiently,
152 // and string.Concat(IEnumerable<char>) can be used as an efficient
153 // enumerable-based equivalent of new string(char[]).
154 using (IEnumerator<char> en = Unsafe.As<IEnumerable<char>>(values).GetEnumerator())
158 // There weren't any chars. Return the empty string.
162 char c = en.Current; // save the first char
166 // There was only one char. Return a string from it directly.
167 return CreateFromChar(c);
170 // Create the StringBuilder, add the chars we've already enumerated,
171 // add the rest, and then get the resulting string.
172 StringBuilder result = StringBuilderCache.Acquire();
173 result.Append(c); // first value
179 while (en.MoveNext());
180 return StringBuilderCache.GetStringAndRelease(result);
185 using (IEnumerator<T> en = values.GetEnumerator())
190 // We called MoveNext once, so this will be the first item
191 T currentValue = en.Current;
193 // Call ToString before calling MoveNext again, since
194 // we want to stay consistent with the below loop
195 // Everything should be called in the order
196 // MoveNext-Current-ToString, unless further optimizations
197 // can be made, to avoid breaking changes
198 string firstString = currentValue?.ToString();
200 // If there's only 1 item, simply call ToString on that
203 // We have to handle the case of either currentValue
204 // or its ToString being null
205 return firstString ?? string.Empty;
208 StringBuilder result = StringBuilderCache.Acquire();
210 result.Append(firstString);
214 currentValue = en.Current;
216 if (currentValue != null)
218 result.Append(currentValue.ToString());
221 while (en.MoveNext());
223 return StringBuilderCache.GetStringAndRelease(result);
228 public static string Concat(IEnumerable<string> values)
231 throw new ArgumentNullException(nameof(values));
233 using (IEnumerator<string> en = values.GetEnumerator())
238 string firstValue = en.Current;
242 return firstValue ?? string.Empty;
245 StringBuilder result = StringBuilderCache.Acquire();
246 result.Append(firstValue);
250 result.Append(en.Current);
252 while (en.MoveNext());
254 return StringBuilderCache.GetStringAndRelease(result);
258 public static string Concat(string str0, string str1)
260 if (IsNullOrEmpty(str0))
262 if (IsNullOrEmpty(str1))
269 if (IsNullOrEmpty(str1))
274 int str0Length = str0.Length;
276 string result = FastAllocateString(str0Length + str1.Length);
278 FillStringChecked(result, 0, str0);
279 FillStringChecked(result, str0Length, str1);
284 public static string Concat(string str0, string str1, string str2)
286 if (IsNullOrEmpty(str0))
288 return Concat(str1, str2);
291 if (IsNullOrEmpty(str1))
293 return Concat(str0, str2);
296 if (IsNullOrEmpty(str2))
298 return Concat(str0, str1);
301 int totalLength = str0.Length + str1.Length + str2.Length;
303 string result = FastAllocateString(totalLength);
304 FillStringChecked(result, 0, str0);
305 FillStringChecked(result, str0.Length, str1);
306 FillStringChecked(result, str0.Length + str1.Length, str2);
311 public static string Concat(string str0, string str1, string str2, string str3)
313 if (IsNullOrEmpty(str0))
315 return Concat(str1, str2, str3);
318 if (IsNullOrEmpty(str1))
320 return Concat(str0, str2, str3);
323 if (IsNullOrEmpty(str2))
325 return Concat(str0, str1, str3);
328 if (IsNullOrEmpty(str3))
330 return Concat(str0, str1, str2);
333 int totalLength = str0.Length + str1.Length + str2.Length + str3.Length;
335 string result = FastAllocateString(totalLength);
336 FillStringChecked(result, 0, str0);
337 FillStringChecked(result, str0.Length, str1);
338 FillStringChecked(result, str0.Length + str1.Length, str2);
339 FillStringChecked(result, str0.Length + str1.Length + str2.Length, str3);
344 public static string Concat(params string[] values)
347 throw new ArgumentNullException(nameof(values));
349 if (values.Length <= 1)
351 return values.Length == 0 ?
353 values[0] ?? string.Empty;
356 // It's possible that the input values array could be changed concurrently on another
357 // thread, such that we can't trust that each read of values[i] will be equivalent.
358 // Worst case, we can make a defensive copy of the array and use that, but we first
359 // optimistically try the allocation and copies assuming that the array isn't changing,
360 // which represents the 99.999% case, in particular since string.Concat is used for
361 // string concatenation by the languages, with the input array being a params array.
363 // Sum the lengths of all input strings
364 long totalLengthLong = 0;
365 for (int i = 0; i < values.Length; i++)
367 string value = values[i];
370 totalLengthLong += value.Length;
374 // If it's too long, fail, or if it's empty, return an empty string.
375 if (totalLengthLong > int.MaxValue)
377 throw new OutOfMemoryException();
379 int totalLength = (int)totalLengthLong;
380 if (totalLength == 0)
385 // Allocate a new string and copy each input string into it
386 string result = FastAllocateString(totalLength);
387 int copiedLength = 0;
388 for (int i = 0; i < values.Length; i++)
390 string value = values[i];
391 if (!string.IsNullOrEmpty(value))
393 int valueLen = value.Length;
394 if (valueLen > totalLength - copiedLength)
400 FillStringChecked(result, copiedLength, value);
401 copiedLength += valueLen;
405 // If we copied exactly the right amount, return the new string. Otherwise,
406 // something changed concurrently to mutate the input array: fall back to
407 // doing the concatenation again, but this time with a defensive copy. This
408 // fall back should be extremely rare.
409 return copiedLength == totalLength ? result : Concat((string[])values.Clone());
412 public static string Format(string format, object arg0)
414 return FormatHelper(null, format, new ParamsArray(arg0));
417 public static string Format(string format, object arg0, object arg1)
419 return FormatHelper(null, format, new ParamsArray(arg0, arg1));
422 public static string Format(string format, object arg0, object arg1, object arg2)
424 return FormatHelper(null, format, new ParamsArray(arg0, arg1, arg2));
427 public static string Format(string format, params object[] args)
431 // To preserve the original exception behavior, throw an exception about format if both
432 // args and format are null. The actual null check for format is in FormatHelper.
433 throw new ArgumentNullException((format == null) ? nameof(format) : nameof(args));
436 return FormatHelper(null, format, new ParamsArray(args));
439 public static string Format(IFormatProvider provider, string format, object arg0)
441 return FormatHelper(provider, format, new ParamsArray(arg0));
444 public static string Format(IFormatProvider provider, string format, object arg0, object arg1)
446 return FormatHelper(provider, format, new ParamsArray(arg0, arg1));
449 public static string Format(IFormatProvider provider, string format, object arg0, object arg1, object arg2)
451 return FormatHelper(provider, format, new ParamsArray(arg0, arg1, arg2));
454 public static string Format(IFormatProvider provider, string format, params object[] args)
458 // To preserve the original exception behavior, throw an exception about format if both
459 // args and format are null. The actual null check for format is in FormatHelper.
460 throw new ArgumentNullException((format == null) ? nameof(format) : nameof(args));
463 return FormatHelper(provider, format, new ParamsArray(args));
466 private static string FormatHelper(IFormatProvider provider, string format, ParamsArray args)
469 throw new ArgumentNullException(nameof(format));
471 return StringBuilderCache.GetStringAndRelease(
473 .Acquire(format.Length + args.Length * 8)
474 .AppendFormatHelper(provider, format, args));
477 public string Insert(int startIndex, string value)
480 throw new ArgumentNullException(nameof(value));
481 if (startIndex < 0 || startIndex > this.Length)
482 throw new ArgumentOutOfRangeException(nameof(startIndex));
484 int oldLength = Length;
485 int insertLength = value.Length;
489 if (insertLength == 0)
492 // In case this computation overflows, newLength will be negative and FastAllocateString throws OutOfMemoryException
493 int newLength = oldLength + insertLength;
494 string result = FastAllocateString(newLength);
497 fixed (char* srcThis = &_firstChar)
499 fixed (char* srcInsert = &value._firstChar)
501 fixed (char* dst = &result._firstChar)
503 wstrcpy(dst, srcThis, startIndex);
504 wstrcpy(dst + startIndex, srcInsert, insertLength);
505 wstrcpy(dst + startIndex + insertLength, srcThis + startIndex, oldLength - startIndex);
513 public static string Join(char separator, params string[] value)
517 throw new ArgumentNullException(nameof(value));
520 return Join(separator, value, 0, value.Length);
523 public static unsafe string Join(char separator, params object[] values)
525 // Defer argument validation to the internal function
526 return JoinCore(&separator, 1, values);
529 public static unsafe string Join<T>(char separator, IEnumerable<T> values)
531 // Defer argument validation to the internal function
532 return JoinCore(&separator, 1, values);
535 public static unsafe string Join(char separator, string[] value, int startIndex, int count)
537 // Defer argument validation to the internal function
538 return JoinCore(&separator, 1, value, startIndex, count);
541 // Joins an array of strings together as one string with a separator between each original string.
543 public static string Join(string separator, params string[] value)
547 throw new ArgumentNullException(nameof(value));
549 return Join(separator, value, 0, value.Length);
552 public static unsafe string Join(string separator, params object[] values)
554 separator = separator ?? string.Empty;
555 fixed (char* pSeparator = &separator._firstChar)
557 // Defer argument validation to the internal function
558 return JoinCore(pSeparator, separator.Length, values);
562 public static unsafe string Join<T>(string separator, IEnumerable<T> values)
564 separator = separator ?? string.Empty;
565 fixed (char* pSeparator = &separator._firstChar)
567 // Defer argument validation to the internal function
568 return JoinCore(pSeparator, separator.Length, values);
572 public static string Join(string separator, IEnumerable<string> values)
576 throw new ArgumentNullException(nameof(values));
579 using (IEnumerator<string> en = values.GetEnumerator())
586 string firstValue = en.Current;
590 // Only one value available
591 return firstValue ?? string.Empty;
594 // Null separator and values are handled by the StringBuilder
595 StringBuilder result = StringBuilderCache.Acquire();
596 result.Append(firstValue);
600 result.Append(separator);
601 result.Append(en.Current);
603 while (en.MoveNext());
605 return StringBuilderCache.GetStringAndRelease(result);
609 // Joins an array of strings together as one string with a separator between each original string.
611 public static unsafe string Join(string separator, string[] value, int startIndex, int count)
613 separator = separator ?? string.Empty;
614 fixed (char* pSeparator = &separator._firstChar)
616 // Defer argument validation to the internal function
617 return JoinCore(pSeparator, separator.Length, value, startIndex, count);
621 private static unsafe string JoinCore(char* separator, int separatorLength, object[] values)
625 throw new ArgumentNullException(nameof(values));
628 if (values.Length == 0)
633 string firstString = values[0]?.ToString();
635 if (values.Length == 1)
637 return firstString ?? string.Empty;
640 StringBuilder result = StringBuilderCache.Acquire();
641 result.Append(firstString);
643 for (int i = 1; i < values.Length; i++)
645 result.Append(separator, separatorLength);
646 object value = values[i];
649 result.Append(value.ToString());
653 return StringBuilderCache.GetStringAndRelease(result);
656 private static unsafe string JoinCore<T>(char* separator, int separatorLength, IEnumerable<T> values)
660 throw new ArgumentNullException(nameof(values));
663 using (IEnumerator<T> en = values.GetEnumerator())
670 // We called MoveNext once, so this will be the first item
671 T currentValue = en.Current;
673 // Call ToString before calling MoveNext again, since
674 // we want to stay consistent with the below loop
675 // Everything should be called in the order
676 // MoveNext-Current-ToString, unless further optimizations
677 // can be made, to avoid breaking changes
678 string firstString = currentValue?.ToString();
680 // If there's only 1 item, simply call ToString on that
683 // We have to handle the case of either currentValue
684 // or its ToString being null
685 return firstString ?? string.Empty;
688 StringBuilder result = StringBuilderCache.Acquire();
690 result.Append(firstString);
694 currentValue = en.Current;
696 result.Append(separator, separatorLength);
697 if (currentValue != null)
699 result.Append(currentValue.ToString());
702 while (en.MoveNext());
704 return StringBuilderCache.GetStringAndRelease(result);
708 private static unsafe string JoinCore(char* separator, int separatorLength, string[] value, int startIndex, int count)
710 // If the separator is null, it is converted to an empty string before entering this function.
711 // Even for empty strings, fixed should never return null (it should return a pointer to a null char).
712 Debug.Assert(separator != null);
713 Debug.Assert(separatorLength >= 0);
717 throw new ArgumentNullException(nameof(value));
721 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
725 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NegativeCount);
727 if (startIndex > value.Length - count)
729 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_IndexCountBuffer);
736 value[startIndex] ?? string.Empty;
739 long totalSeparatorsLength = (long)(count - 1) * separatorLength;
740 if (totalSeparatorsLength > int.MaxValue)
742 throw new OutOfMemoryException();
744 int totalLength = (int)totalSeparatorsLength;
746 // Calculate the length of the resultant string so we know how much space to allocate.
747 for (int i = startIndex, end = startIndex + count; i < end; i++)
749 string currentValue = value[i];
750 if (currentValue != null)
752 totalLength += currentValue.Length;
753 if (totalLength < 0) // Check for overflow
755 throw new OutOfMemoryException();
760 // Copy each of the strings into the resultant buffer, interleaving with the separator.
761 string result = FastAllocateString(totalLength);
762 int copiedLength = 0;
764 for (int i = startIndex, end = startIndex + count; i < end; i++)
766 // It's possible that another thread may have mutated the input array
767 // such that our second read of an index will not be the same string
768 // we got during the first read.
770 // We range check again to avoid buffer overflows if this happens.
772 string currentValue = value[i];
773 if (currentValue != null)
775 int valueLen = currentValue.Length;
776 if (valueLen > totalLength - copiedLength)
782 // Fill in the value.
783 FillStringChecked(result, copiedLength, currentValue);
784 copiedLength += valueLen;
789 // Fill in the separator.
790 fixed (char* pResult = &result._firstChar)
792 // If we are called from the char-based overload, we will not
793 // want to call MemoryCopy each time we fill in the separator. So
794 // specialize for 1-length separators.
795 if (separatorLength == 1)
797 pResult[copiedLength] = *separator;
801 wstrcpy(pResult + copiedLength, separator, separatorLength);
804 copiedLength += separatorLength;
808 // If we copied exactly the right amount, return the new string. Otherwise,
809 // something changed concurrently to mutate the input array: fall back to
810 // doing the concatenation again, but this time with a defensive copy. This
811 // fall back should be extremely rare.
812 return copiedLength == totalLength ?
814 JoinCore(separator, separatorLength, (string[])value.Clone(), startIndex, count);
817 public string PadLeft(int totalWidth) => PadLeft(totalWidth, ' ');
819 public string PadLeft(int totalWidth, char paddingChar)
822 throw new ArgumentOutOfRangeException(nameof(totalWidth), SR.ArgumentOutOfRange_NeedNonNegNum);
823 int oldLength = Length;
824 int count = totalWidth - oldLength;
827 string result = FastAllocateString(totalWidth);
830 fixed (char* dst = &result._firstChar)
832 for (int i = 0; i < count; i++)
833 dst[i] = paddingChar;
834 fixed (char* src = &_firstChar)
836 wstrcpy(dst + count, src, oldLength);
843 public string PadRight(int totalWidth) => PadRight(totalWidth, ' ');
845 public string PadRight(int totalWidth, char paddingChar)
848 throw new ArgumentOutOfRangeException(nameof(totalWidth), SR.ArgumentOutOfRange_NeedNonNegNum);
849 int oldLength = Length;
850 int count = totalWidth - oldLength;
853 string result = FastAllocateString(totalWidth);
856 fixed (char* dst = &result._firstChar)
858 fixed (char* src = &_firstChar)
860 wstrcpy(dst, src, oldLength);
862 for (int i = 0; i < count; i++)
863 dst[oldLength + i] = paddingChar;
869 public string Remove(int startIndex, int count)
872 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
874 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NegativeCount);
875 int oldLength = this.Length;
876 if (count > oldLength - startIndex)
877 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_IndexCount);
881 int newLength = oldLength - count;
885 string result = FastAllocateString(newLength);
888 fixed (char* src = &_firstChar)
890 fixed (char* dst = &result._firstChar)
892 wstrcpy(dst, src, startIndex);
893 wstrcpy(dst + startIndex, src + startIndex + count, newLength - startIndex);
900 // a remove that just takes a startindex.
901 public string Remove(int startIndex)
904 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
906 if (startIndex >= Length)
907 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndexLessThanLength);
909 return Substring(0, startIndex);
912 public string Replace(string oldValue, string newValue, bool ignoreCase, CultureInfo culture)
914 return ReplaceCore(oldValue, newValue, culture, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
917 public string Replace(string oldValue, string newValue, StringComparison comparisonType)
919 switch (comparisonType)
921 case StringComparison.CurrentCulture:
922 return ReplaceCore(oldValue, newValue, CultureInfo.CurrentCulture, CompareOptions.None);
924 case StringComparison.CurrentCultureIgnoreCase:
925 return ReplaceCore(oldValue, newValue, CultureInfo.CurrentCulture, CompareOptions.IgnoreCase);
927 case StringComparison.InvariantCulture:
928 return ReplaceCore(oldValue, newValue, CultureInfo.InvariantCulture, CompareOptions.None);
930 case StringComparison.InvariantCultureIgnoreCase:
931 return ReplaceCore(oldValue, newValue, CultureInfo.InvariantCulture, CompareOptions.IgnoreCase);
933 case StringComparison.Ordinal:
934 return Replace(oldValue, newValue);
936 case StringComparison.OrdinalIgnoreCase:
937 return ReplaceCore(oldValue, newValue, CultureInfo.InvariantCulture, CompareOptions.OrdinalIgnoreCase);
940 throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
944 private unsafe string ReplaceCore(string oldValue, string newValue, CultureInfo culture, CompareOptions options)
946 if (oldValue == null)
947 throw new ArgumentNullException(nameof(oldValue));
948 if (oldValue.Length == 0)
949 throw new ArgumentException(SR.Argument_StringZeroLength, nameof(oldValue));
951 // If they asked to replace oldValue with a null, replace all occurrences
952 // with the empty string.
953 if (newValue == null)
954 newValue = string.Empty;
956 CultureInfo referenceCulture = culture ?? CultureInfo.CurrentCulture;
957 StringBuilder result = StringBuilderCache.Acquire();
964 bool hasDoneAnyReplacements = false;
965 CompareInfo ci = referenceCulture.CompareInfo;
969 index = ci.IndexOf(this, oldValue, startIndex, this.Length - startIndex, options, &matchLength);
972 // append the unmodified portion of string
973 result.Append(this, startIndex, index - startIndex);
975 // append the replacement
976 result.Append(newValue);
978 startIndex = index + matchLength;
979 hasDoneAnyReplacements = true;
981 else if (!hasDoneAnyReplacements)
983 // small optimization,
984 // if we have not done any replacements,
985 // we will return the original string
986 StringBuilderCache.Release(result);
991 result.Append(this, startIndex, this.Length - startIndex);
993 } while (index >= 0);
995 return StringBuilderCache.GetStringAndRelease(result);
998 // Replaces all instances of oldChar with newChar.
1000 public string Replace(char oldChar, char newChar)
1002 if (oldChar == newChar)
1007 int remainingLength = Length;
1009 fixed (char* pChars = &_firstChar)
1011 char* pSrc = pChars;
1013 while (remainingLength > 0)
1015 if (*pSrc == oldChar)
1025 if (remainingLength == 0)
1028 string result = FastAllocateString(Length);
1030 fixed (char* pChars = &_firstChar)
1032 fixed (char* pResult = &result._firstChar)
1034 int copyLength = Length - remainingLength;
1036 //Copy the characters already proven not to match.
1039 wstrcpy(pResult, pChars, copyLength);
1042 //Copy the remaining characters, doing the replacement as we go.
1043 char* pSrc = pChars + copyLength;
1044 char* pDst = pResult + copyLength;
1048 char currentChar = *pSrc;
1049 if (currentChar == oldChar)
1050 currentChar = newChar;
1051 *pDst = currentChar;
1056 } while (remainingLength > 0);
1064 public string Replace(string oldValue, string newValue)
1066 if (oldValue == null)
1067 throw new ArgumentNullException(nameof(oldValue));
1068 if (oldValue.Length == 0)
1069 throw new ArgumentException(SR.Argument_StringZeroLength, nameof(oldValue));
1071 // Api behavior: if newValue is null, instances of oldValue are to be removed.
1072 if (newValue == null)
1073 newValue = string.Empty;
1075 Span<int> initialSpan = stackalloc int[StackallocIntBufferSizeLimit];
1076 var replacementIndices = new ValueListBuilder<int>(initialSpan);
1080 fixed (char* pThis = &_firstChar)
1083 int lastPossibleMatchIdx = this.Length - oldValue.Length;
1084 while (matchIdx <= lastPossibleMatchIdx)
1086 char* pMatch = pThis + matchIdx;
1087 for (int probeIdx = 0; probeIdx < oldValue.Length; probeIdx++)
1089 if (pMatch[probeIdx] != oldValue[probeIdx])
1094 // Found a match for the string. Record the location of the match and skip over the "oldValue."
1095 replacementIndices.Append(matchIdx);
1096 matchIdx += oldValue.Length;
1105 if (replacementIndices.Length == 0)
1108 // String allocation and copying is in separate method to make this method faster for the case where
1109 // nothing needs replacing.
1110 string dst = ReplaceHelper(oldValue.Length, newValue, replacementIndices.AsSpan());
1112 replacementIndices.Dispose();
1117 private string ReplaceHelper(int oldValueLength, string newValue, ReadOnlySpan<int> indices)
1119 Debug.Assert(indices.Length > 0);
1121 long dstLength = this.Length + ((long)(newValue.Length - oldValueLength)) * indices.Length;
1122 if (dstLength > int.MaxValue)
1123 throw new OutOfMemoryException();
1124 string dst = FastAllocateString((int)dstLength);
1126 Span<char> dstSpan = new Span<char>(ref dst.GetRawStringData(), dst.Length);
1131 for (int r = 0; r < indices.Length; r++)
1133 int replacementIdx = indices[r];
1135 // Copy over the non-matching portion of the original that precedes this occurrence of oldValue.
1136 int count = replacementIdx - thisIdx;
1139 this.AsSpan().Slice(thisIdx, count).CopyTo(dstSpan.Slice(dstIdx));
1142 thisIdx = replacementIdx + oldValueLength;
1144 // Copy over newValue to replace the oldValue.
1145 newValue.AsSpan().CopyTo(dstSpan.Slice(dstIdx));
1146 dstIdx += newValue.Length;
1149 // Copy over the final non-matching portion at the end of the string.
1150 Debug.Assert(this.Length - thisIdx == dstSpan.Length - dstIdx);
1151 this.AsSpan().Slice(thisIdx).CopyTo(dstSpan.Slice(dstIdx));
1156 public string[] Split(char separator, StringSplitOptions options = StringSplitOptions.None)
1158 return SplitInternal(new ReadOnlySpan<char>(ref separator, 1), int.MaxValue, options);
1161 public string[] Split(char separator, int count, StringSplitOptions options = StringSplitOptions.None)
1163 return SplitInternal(new ReadOnlySpan<char>(ref separator, 1), count, options);
1166 // Creates an array of strings by splitting this string at each
1167 // occurrence of a separator. The separator is searched for, and if found,
1168 // the substring preceding the occurrence is stored as the first element in
1169 // the array of strings. We then continue in this manner by searching
1170 // the substring that follows the occurrence. On the other hand, if the separator
1171 // is not found, the array of strings will contain this instance as its only element.
1172 // If the separator is null
1173 // whitespace (i.e., Character.IsWhitespace) is used as the separator.
1175 public string[] Split(params char[] separator)
1177 return SplitInternal(separator, int.MaxValue, StringSplitOptions.None);
1180 // Creates an array of strings by splitting this string at each
1181 // occurrence of a separator. The separator is searched for, and if found,
1182 // the substring preceding the occurrence is stored as the first element in
1183 // the array of strings. We then continue in this manner by searching
1184 // the substring that follows the occurrence. On the other hand, if the separator
1185 // is not found, the array of strings will contain this instance as its only element.
1186 // If the separator is the empty string (i.e., string.Empty), then
1187 // whitespace (i.e., Character.IsWhitespace) is used as the separator.
1188 // If there are more than count different strings, the last n-(count-1)
1189 // elements are concatenated and added as the last string.
1191 public string[] Split(char[] separator, int count)
1193 return SplitInternal(separator, count, StringSplitOptions.None);
1196 public string[] Split(char[] separator, StringSplitOptions options)
1198 return SplitInternal(separator, int.MaxValue, options);
1201 public string[] Split(char[] separator, int count, StringSplitOptions options)
1203 return SplitInternal(separator, count, options);
1206 private string[] SplitInternal(ReadOnlySpan<char> separators, int count, StringSplitOptions options)
1209 throw new ArgumentOutOfRangeException(nameof(count),
1210 SR.ArgumentOutOfRange_NegativeCount);
1212 if (options < StringSplitOptions.None || options > StringSplitOptions.RemoveEmptyEntries)
1213 throw new ArgumentException(SR.Format(SR.Arg_EnumIllegalVal, options));
1215 bool omitEmptyEntries = (options == StringSplitOptions.RemoveEmptyEntries);
1217 if ((count == 0) || (omitEmptyEntries && Length == 0))
1219 return Array.Empty<string>();
1224 return new string[] { this };
1227 Span<int> initialSpan = stackalloc int[StackallocIntBufferSizeLimit];
1228 var sepListBuilder = new ValueListBuilder<int>(initialSpan);
1230 MakeSeparatorList(separators, ref sepListBuilder);
1231 ReadOnlySpan<int> sepList = sepListBuilder.AsSpan();
1233 // Handle the special case of no replaces.
1234 if (sepList.Length == 0)
1236 return new string[] { this };
1239 string[] result = omitEmptyEntries
1240 ? SplitOmitEmptyEntries(sepList, default, 1, count)
1241 : SplitKeepEmptyEntries(sepList, default, 1, count);
1243 sepListBuilder.Dispose();
1248 public string[] Split(string separator, StringSplitOptions options = StringSplitOptions.None)
1250 return SplitInternal(separator ?? string.Empty, null, int.MaxValue, options);
1253 public string[] Split(string separator, Int32 count, StringSplitOptions options = StringSplitOptions.None)
1255 return SplitInternal(separator ?? string.Empty, null, count, options);
1258 public string[] Split(string[] separator, StringSplitOptions options)
1260 return SplitInternal(null, separator, int.MaxValue, options);
1263 public string[] Split(string[] separator, Int32 count, StringSplitOptions options)
1265 return SplitInternal(null, separator, count, options);
1268 private string[] SplitInternal(string separator, string[] separators, int count, StringSplitOptions options)
1272 throw new ArgumentOutOfRangeException(nameof(count),
1273 SR.ArgumentOutOfRange_NegativeCount);
1276 if (options < StringSplitOptions.None || options > StringSplitOptions.RemoveEmptyEntries)
1278 throw new ArgumentException(SR.Format(SR.Arg_EnumIllegalVal, (int)options));
1281 bool omitEmptyEntries = (options == StringSplitOptions.RemoveEmptyEntries);
1283 bool singleSeparator = separator != null;
1285 if (!singleSeparator && (separators == null || separators.Length == 0))
1287 return SplitInternal((char[])null, count, options);
1290 if ((count == 0) || (omitEmptyEntries && Length == 0))
1292 return Array.Empty<string>();
1295 if (count == 1 || (singleSeparator && separator.Length == 0))
1297 return new string[] { this };
1300 if (singleSeparator)
1302 return SplitInternal(separator, count, options);
1305 Span<int> sepListInitialSpan = stackalloc int[StackallocIntBufferSizeLimit];
1306 var sepListBuilder = new ValueListBuilder<int>(sepListInitialSpan);
1308 Span<int> lengthListInitialSpan = stackalloc int[StackallocIntBufferSizeLimit];
1309 var lengthListBuilder = new ValueListBuilder<int>(lengthListInitialSpan);
1311 MakeSeparatorList(separators, ref sepListBuilder, ref lengthListBuilder);
1312 ReadOnlySpan<int> sepList = sepListBuilder.AsSpan();
1313 ReadOnlySpan<int> lengthList = lengthListBuilder.AsSpan();
1315 // Handle the special case of no replaces.
1316 if (sepList.Length == 0)
1318 return new string[] { this };
1321 string[] result = omitEmptyEntries
1322 ? SplitOmitEmptyEntries(sepList, lengthList, 0, count)
1323 : SplitKeepEmptyEntries(sepList, lengthList, 0, count);
1325 sepListBuilder.Dispose();
1326 lengthListBuilder.Dispose();
1331 private string[] SplitInternal(string separator, int count, StringSplitOptions options)
1333 Span<int> sepListInitialSpan = stackalloc int[StackallocIntBufferSizeLimit];
1334 var sepListBuilder = new ValueListBuilder<int>(sepListInitialSpan);
1336 MakeSeparatorList(separator, ref sepListBuilder);
1337 ReadOnlySpan<int> sepList = sepListBuilder.AsSpan();
1338 if (sepList.Length == 0)
1340 // there are no separators so sepListBuilder did not rent an array from pool and there is no need to dispose it
1341 return new string[] { this };
1344 string[] result = options == StringSplitOptions.RemoveEmptyEntries
1345 ? SplitOmitEmptyEntries(sepList, default, separator.Length, count)
1346 : SplitKeepEmptyEntries(sepList, default, separator.Length, count);
1348 sepListBuilder.Dispose();
1353 private string[] SplitKeepEmptyEntries(ReadOnlySpan<int> sepList, ReadOnlySpan<int> lengthList, int defaultLength, int count)
1355 Debug.Assert(count >= 2);
1361 int numActualReplaces = (sepList.Length < count) ? sepList.Length : count;
1363 //Allocate space for the new array.
1364 //+1 for the string from the end of the last replace to the end of the string.
1365 string[] splitStrings = new string[numActualReplaces + 1];
1367 for (int i = 0; i < numActualReplaces && currIndex < Length; i++)
1369 splitStrings[arrIndex++] = Substring(currIndex, sepList[i] - currIndex);
1370 currIndex = sepList[i] + (lengthList.IsEmpty ? defaultLength : lengthList[i]);
1373 //Handle the last string at the end of the array if there is one.
1374 if (currIndex < Length && numActualReplaces >= 0)
1376 splitStrings[arrIndex] = Substring(currIndex);
1378 else if (arrIndex == numActualReplaces)
1380 //We had a separator character at the end of a string. Rather than just allowing
1381 //a null character, we'll replace the last element in the array with an empty string.
1382 splitStrings[arrIndex] = string.Empty;
1385 return splitStrings;
1389 // This function will not keep the Empty string
1390 private string[] SplitOmitEmptyEntries(ReadOnlySpan<int> sepList, ReadOnlySpan<int> lengthList, int defaultLength, int count)
1392 Debug.Assert(count >= 2);
1394 int numReplaces = sepList.Length;
1396 // Allocate array to hold items. This array may not be
1397 // filled completely in this function, we will create a
1398 // new array and copy string references to that new array.
1399 int maxItems = (numReplaces < count) ? (numReplaces + 1) : count;
1400 string[] splitStrings = new string[maxItems];
1405 for (int i = 0; i < numReplaces && currIndex < Length; i++)
1407 if (sepList[i] - currIndex > 0)
1409 splitStrings[arrIndex++] = Substring(currIndex, sepList[i] - currIndex);
1411 currIndex = sepList[i] + (lengthList.IsEmpty ? defaultLength : lengthList[i]);
1412 if (arrIndex == count - 1)
1414 // If all the remaining entries at the end are empty, skip them
1415 while (i < numReplaces - 1 && currIndex == sepList[++i])
1417 currIndex += (lengthList.IsEmpty ? defaultLength : lengthList[i]);
1423 // we must have at least one slot left to fill in the last string.
1424 Debug.Assert(arrIndex < maxItems);
1426 //Handle the last string at the end of the array if there is one.
1427 if (currIndex < Length)
1429 splitStrings[arrIndex++] = Substring(currIndex);
1432 string[] stringArray = splitStrings;
1433 if (arrIndex != maxItems)
1435 stringArray = new string[arrIndex];
1436 for (int j = 0; j < arrIndex; j++)
1438 stringArray[j] = splitStrings[j];
1445 /// Uses ValueListBuilder to create list that holds indexes of separators in string.
1447 /// <param name="separators"><see cref="ReadOnlySpan{T}"/> of separator chars</param>
1448 /// <param name="sepListBuilder"><see cref="ValueListBuilder{T}"/> to store indexes</param>
1449 /// <returns></returns>
1450 private void MakeSeparatorList(ReadOnlySpan<char> separators, ref ValueListBuilder<int> sepListBuilder)
1452 char sep0, sep1, sep2;
1454 switch (separators.Length)
1456 // Special-case no separators to mean any whitespace is a separator.
1458 for (int i = 0; i < Length; i++)
1460 if (char.IsWhiteSpace(this[i]))
1462 sepListBuilder.Append(i);
1467 // Special-case the common cases of 1, 2, and 3 separators, with manual comparisons against each separator.
1469 sep0 = separators[0];
1470 for (int i = 0; i < Length; i++)
1472 if (this[i] == sep0)
1474 sepListBuilder.Append(i);
1479 sep0 = separators[0];
1480 sep1 = separators[1];
1481 for (int i = 0; i < Length; i++)
1484 if (c == sep0 || c == sep1)
1486 sepListBuilder.Append(i);
1491 sep0 = separators[0];
1492 sep1 = separators[1];
1493 sep2 = separators[2];
1494 for (int i = 0; i < Length; i++)
1497 if (c == sep0 || c == sep1 || c == sep2)
1499 sepListBuilder.Append(i);
1504 // Handle > 3 separators with a probabilistic map, ala IndexOfAny.
1505 // This optimizes for chars being unlikely to match a separator.
1509 ProbabilisticMap map = default;
1510 uint* charMap = (uint*)↦
1511 InitializeProbabilisticMap(charMap, separators);
1513 for (int i = 0; i < Length; i++)
1516 if (IsCharBitSet(charMap, (byte)c) && IsCharBitSet(charMap, (byte)(c >> 8)) &&
1517 separators.Contains(c))
1519 sepListBuilder.Append(i);
1528 /// Uses ValueListBuilder to create list that holds indexes of separators in string.
1530 /// <param name="separator">separator string</param>
1531 /// <param name="sepListBuilder"><see cref="ValueListBuilder{T}"/> to store indexes</param>
1532 /// <returns></returns>
1533 private void MakeSeparatorList(string separator, ref ValueListBuilder<int> sepListBuilder)
1535 Debug.Assert(!IsNullOrEmpty(separator), "!string.IsNullOrEmpty(separator)");
1537 int currentSepLength = separator.Length;
1539 for (int i = 0; i < Length; i++)
1541 if (this[i] == separator[0] && currentSepLength <= Length - i)
1543 if (currentSepLength == 1
1544 || CompareOrdinal(this, i, separator, 0, currentSepLength) == 0)
1546 sepListBuilder.Append(i);
1547 i += currentSepLength - 1;
1554 /// Uses ValueListBuilder to create list that holds indexes of separators in string and list that holds length of separator strings.
1556 /// <param name="separators">separator strngs</param>
1557 /// <param name="sepListBuilder"><see cref="ValueListBuilder{T}"/> for separator indexes</param>
1558 /// <param name="lengthListBuilder"><see cref="ValueListBuilder{T}"/> for separator length values</param>
1559 private void MakeSeparatorList(string[] separators, ref ValueListBuilder<int> sepListBuilder, ref ValueListBuilder<int> lengthListBuilder)
1561 Debug.Assert(separators != null && separators.Length > 0, "separators != null && separators.Length > 0");
1563 int sepCount = separators.Length;
1565 for (int i = 0; i < Length; i++)
1567 for (int j = 0; j < separators.Length; j++)
1569 string separator = separators[j];
1570 if (IsNullOrEmpty(separator))
1574 int currentSepLength = separator.Length;
1575 if (this[i] == separator[0] && currentSepLength <= Length - i)
1577 if (currentSepLength == 1
1578 || CompareOrdinal(this, i, separator, 0, currentSepLength) == 0)
1580 sepListBuilder.Append(i);
1581 lengthListBuilder.Append(currentSepLength);
1582 i += currentSepLength - 1;
1590 // Returns a substring of this string.
1592 public string Substring(int startIndex) => Substring(startIndex, Length - startIndex);
1594 public string Substring(int startIndex, int length)
1598 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
1601 if (startIndex > Length)
1603 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndexLargerThanLength);
1608 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NegativeLength);
1611 if (startIndex > Length - length)
1613 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_IndexLength);
1618 return string.Empty;
1621 if (startIndex == 0 && length == this.Length)
1626 return InternalSubString(startIndex, length);
1629 private unsafe string InternalSubString(int startIndex, int length)
1631 Debug.Assert(startIndex >= 0 && startIndex <= this.Length, "StartIndex is out of range!");
1632 Debug.Assert(length >= 0 && startIndex <= this.Length - length, "length is out of range!");
1634 string result = FastAllocateString(length);
1636 fixed (char* dest = &result._firstChar)
1637 fixed (char* src = &_firstChar)
1639 wstrcpy(dest, src + startIndex, length);
1645 // Creates a copy of this string in lower case. The culture is set by culture.
1646 public string ToLower()
1648 return CultureInfo.CurrentCulture.TextInfo.ToLower(this);
1651 // Creates a copy of this string in lower case. The culture is set by culture.
1652 public string ToLower(CultureInfo culture)
1654 if (culture == null)
1656 throw new ArgumentNullException(nameof(culture));
1658 return culture.TextInfo.ToLower(this);
1661 // Creates a copy of this string in lower case based on invariant culture.
1662 public string ToLowerInvariant()
1664 return CultureInfo.InvariantCulture.TextInfo.ToLower(this);
1667 public string ToUpper()
1669 return CultureInfo.CurrentCulture.TextInfo.ToUpper(this);
1672 // Creates a copy of this string in upper case. The culture is set by culture.
1673 public string ToUpper(CultureInfo culture)
1675 if (culture == null)
1677 throw new ArgumentNullException(nameof(culture));
1679 return culture.TextInfo.ToUpper(this);
1682 //Creates a copy of this string in upper case based on invariant culture.
1683 public string ToUpperInvariant()
1685 return CultureInfo.InvariantCulture.TextInfo.ToUpper(this);
1688 // Trims the whitespace from both ends of the string. Whitespace is defined by
1689 // Char.IsWhiteSpace.
1691 public string Trim() => TrimWhiteSpaceHelper(TrimType.Both);
1693 // Removes a set of characters from the beginning and end of this string.
1694 public unsafe string Trim(char trimChar) => TrimHelper(&trimChar, 1, TrimType.Both);
1696 // Removes a set of characters from the beginning and end of this string.
1697 public unsafe string Trim(params char[] trimChars)
1699 if (trimChars == null || trimChars.Length == 0)
1701 return TrimWhiteSpaceHelper(TrimType.Both);
1703 fixed (char* pTrimChars = &trimChars[0])
1705 return TrimHelper(pTrimChars, trimChars.Length, TrimType.Both);
1709 // Removes a set of characters from the beginning of this string.
1710 public string TrimStart() => TrimWhiteSpaceHelper(TrimType.Head);
1712 // Removes a set of characters from the beginning of this string.
1713 public unsafe string TrimStart(char trimChar) => TrimHelper(&trimChar, 1, TrimType.Head);
1715 // Removes a set of characters from the beginning of this string.
1716 public unsafe string TrimStart(params char[] trimChars)
1718 if (trimChars == null || trimChars.Length == 0)
1720 return TrimWhiteSpaceHelper(TrimType.Head);
1722 fixed (char* pTrimChars = &trimChars[0])
1724 return TrimHelper(pTrimChars, trimChars.Length, TrimType.Head);
1728 // Removes a set of characters from the end of this string.
1729 public string TrimEnd() => TrimWhiteSpaceHelper(TrimType.Tail);
1731 // Removes a set of characters from the end of this string.
1732 public unsafe string TrimEnd(char trimChar) => TrimHelper(&trimChar, 1, TrimType.Tail);
1734 // Removes a set of characters from the end of this string.
1735 public unsafe string TrimEnd(params char[] trimChars)
1737 if (trimChars == null || trimChars.Length == 0)
1739 return TrimWhiteSpaceHelper(TrimType.Tail);
1741 fixed (char* pTrimChars = &trimChars[0])
1743 return TrimHelper(pTrimChars, trimChars.Length, TrimType.Tail);
1747 private string TrimWhiteSpaceHelper(TrimType trimType)
1749 // end will point to the first non-trimmed character on the right.
1750 // start will point to the first non-trimmed character on the left.
1751 int end = Length - 1;
1754 // Trim specified characters.
1755 if (trimType != TrimType.Tail)
1757 for (start = 0; start < Length; start++)
1759 if (!char.IsWhiteSpace(this[start]))
1766 if (trimType != TrimType.Head)
1768 for (end = Length - 1; end >= start; end--)
1770 if (!char.IsWhiteSpace(this[end]))
1777 return CreateTrimmedString(start, end);
1780 private unsafe string TrimHelper(char* trimChars, int trimCharsLength, TrimType trimType)
1782 Debug.Assert(trimChars != null);
1783 Debug.Assert(trimCharsLength > 0);
1785 // end will point to the first non-trimmed character on the right.
1786 // start will point to the first non-trimmed character on the left.
1787 int end = Length - 1;
1790 // Trim specified characters.
1791 if (trimType != TrimType.Tail)
1793 for (start = 0; start < Length; start++)
1796 char ch = this[start];
1797 for (i = 0; i < trimCharsLength; i++)
1799 if (trimChars[i] == ch)
1804 if (i == trimCharsLength)
1806 // The character is not in trimChars, so stop trimming.
1812 if (trimType != TrimType.Head)
1814 for (end = Length - 1; end >= start; end--)
1817 char ch = this[end];
1818 for (i = 0; i < trimCharsLength; i++)
1820 if (trimChars[i] == ch)
1825 if (i == trimCharsLength)
1827 // The character is not in trimChars, so stop trimming.
1833 return CreateTrimmedString(start, end);
1836 private string CreateTrimmedString(int start, int end)
1838 int len = end - start + 1;
1840 len == Length ? this :
1841 len == 0 ? string.Empty :
1842 InternalSubString(start, len);
1845 private enum TrimType