// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; using System.Text; namespace System { public partial class String { // Avoid paying the init cost of all the SearchValues unless they are actually used. internal static class SearchValuesStorage { /// <summary> /// SearchValues would use SpanHelpers.IndexOfAnyValueType for 5 values in this case. /// No need to allocate the SearchValues as a regular Span.IndexOfAny will use the same implementation. /// </summary> public const string NewLineCharsExceptLineFeed = "\r\f\u0085\u2028\u2029"; /// <summary> /// The Unicode Standard, Sec. 5.8, Recommendation R4 and Table 5-2 state that the CR, LF, /// CRLF, NEL, LS, FF, and PS sequences are considered newline functions. That section /// also specifically excludes VT from the list of newline functions, so we do not include /// it in the needle list. /// </summary> public static readonly SearchValues<char> NewLineChars = SearchValues.Create(NewLineCharsExceptLineFeed + "\n"); /// <summary>A <see cref="SearchValues{Char}"/> for all of the Unicode whitespace characters</summary> public static readonly SearchValues<char> WhiteSpaceChars = SearchValues.Create("\t\n\v\f\r\u0020\u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000"); // All ASCII characters except the lowercase letters 'a'-'z'. The first character outside this set is // the first one ToUpperOrdinal must change (a lowercase ASCII letter or a non-ASCII character). public static readonly SearchValues<char> AsciiExceptLowercase = SearchValues.Create("\0\u0001\u0002\u0003\u0004\u0005\u0006\a\b\t\n\v\f\r\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\e\u001C\u001D\u001E\u001F !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`{|}~\u007F"); // All ASCII characters except the uppercase letters 'A'-'Z'. The first character outside this set is // the first one ToLowerOrdinal must change (an uppercase ASCII letter or a non-ASCII character). public static readonly SearchValues<char> AsciiExceptUppercase = SearchValues.Create("\0\u0001\u0002\u0003\u0004\u0005\u0006\a\b\t\n\v\f\r\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\e\u001C\u001D\u001E\u001F !\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007F"); #if DEBUG static SearchValuesStorage() { SearchValues<char> sv = WhiteSpaceChars; for (int i = 0; i <= char.MaxValue; i++) { Debug.Assert(char.IsWhiteSpace((char)i) == sv.Contains((char)i)); } } #endif } internal const int StackallocIntBufferSizeLimit = 128; internal const int StackallocCharBufferSizeLimit = 256; private static void CopyStringContent(string dest, int destPos, string src) { Debug.Assert(dest != null); Debug.Assert(src != null); Debug.Assert(src.Length <= dest.Length - destPos); Buffer.Memmove( destination: ref Unsafe.Add(ref dest._firstChar, destPos), source: ref src._firstChar, elementCount: (uint)src.Length); } public static string Concat(object? arg0) => arg0?.ToString() ?? Empty; public static string Concat(object? arg0, object? arg1) => Concat(arg0?.ToString(), arg1?.ToString()); public static string Concat(object? arg0, object? arg1, object? arg2) => Concat(arg0?.ToString(), arg1?.ToString(), arg2?.ToString()); public static string Concat(params object?[] args) { ArgumentNullException.ThrowIfNull(args); return Concat((ReadOnlySpan<object?>)args); } /// <summary> /// Concatenates the string representations of the elements in a specified span of objects. /// </summary> /// <param name="args">A span of objects that contains the elements to concatenate.</param> /// <returns>The concatenated string representations of the values of the elements in <paramref name="args"/>.</returns> public static string Concat(params ReadOnlySpan<object?> args) { if (args.Length <= 1) { return args.IsEmpty ? Empty : args[0]?.ToString() ?? Empty; } // We need to get an intermediary string array // to fill with each of the args' ToString(), // and then just concat that in one operation. // This way we avoid any intermediary string representations, // or buffer resizing if we use StringBuilder (although the // latter case is partially alleviated due to StringBuilder's // linked-list style implementation) var strings = new string[args.Length]; int totalLength = 0; for (int i = 0; i < args.Length; i++) { object? value = args[i]; string toString = value?.ToString() ?? Empty; // We need to handle both the cases when value or value.ToString() is null strings[i] = toString; totalLength += toString.Length; if (totalLength < 0) // Check for a positive overflow { ThrowHelper.ThrowOutOfMemoryException_StringTooLong(); } } // If all of the ToStrings are null/empty, just return string.Empty if (totalLength == 0) { return Empty; } string result = FastAllocateString(totalLength); int position = 0; // How many characters we've copied so far for (int i = 0; i < strings.Length; i++) { string s = strings[i]; Debug.Assert(s != null); Debug.Assert(position <= totalLength - s.Length, "We didn't allocate enough space for the result string!"); CopyStringContent(result, position, s); position += s.Length; } return result; } public static string Concat<T>(IEnumerable<T> values) => JoinCore(ReadOnlySpan<char>.Empty, values); public static unsafe string Concat(IEnumerable<string?> values) { ArgumentNullException.ThrowIfNull(values); if (values.GetType() == typeof(List<string?>)) // avoid accidentally bypassing a derived type's reimplementation of IEnumerable<T> { return Concat(CollectionsMarshal.AsSpan((List<string?>)values)); } if (values is string?[] valuesArray) { return Concat((ReadOnlySpan<string?>)valuesArray); } using (IEnumerator<string?> en = values.GetEnumerator()) { if (!en.MoveNext()) return Empty; string? firstValue = en.Current; if (!en.MoveNext()) { return firstValue ?? Empty; } var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); result.Append(firstValue); do { result.Append(en.Current); } while (en.MoveNext()); return result.ToString(); } } public static string Concat(string? str0, string? str1) { if (IsNullOrEmpty(str0)) { if (IsNullOrEmpty(str1)) { return Empty; } return str1; } if (IsNullOrEmpty(str1)) { return str0; } int str0Length = str0.Length; int totalLength = str0Length + str1.Length; // Can't overflow to a positive number so just check < 0 if (totalLength < 0) { ThrowHelper.ThrowOutOfMemoryException_StringTooLong(); } string result = FastAllocateString(totalLength); CopyStringContent(result, 0, str0); CopyStringContent(result, str0Length, str1); return result; } public static string Concat(string? str0, string? str1, string? str2) { if (IsNullOrEmpty(str0)) { return Concat(str1, str2); } if (IsNullOrEmpty(str1)) { return Concat(str0, str2); } if (IsNullOrEmpty(str2)) { return Concat(str0, str1); } // It can overflow to a positive number so we accumulate the total length as a long. long totalLength = (long)str0.Length + (long)str1.Length + (long)str2.Length; if (totalLength > int.MaxValue) { ThrowHelper.ThrowOutOfMemoryException_StringTooLong(); } string result = FastAllocateString((int)totalLength); CopyStringContent(result, 0, str0); CopyStringContent(result, str0.Length, str1); CopyStringContent(result, str0.Length + str1.Length, str2); return result; } public static string Concat(string? str0, string? str1, string? str2, string? str3) { if (IsNullOrEmpty(str0)) { return Concat(str1, str2, str3); } if (IsNullOrEmpty(str1)) { return Concat(str0, str2, str3); } if (IsNullOrEmpty(str2)) { return Concat(str0, str1, str3); } if (IsNullOrEmpty(str3)) { return Concat(str0, str1, str2); } // It can overflow to a positive number so we accumulate the total length as a long. long totalLength = (long)str0.Length + (long)str1.Length + (long)str2.Length + (long)str3.Length; if (totalLength > int.MaxValue) { ThrowHelper.ThrowOutOfMemoryException_StringTooLong(); } string result = FastAllocateString((int)totalLength); CopyStringContent(result, 0, str0); CopyStringContent(result, str0.Length, str1); CopyStringContent(result, str0.Length + str1.Length, str2); CopyStringContent(result, str0.Length + str1.Length + str2.Length, str3); return result; } public static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1) { int length = checked(str0.Length + str1.Length); if (length == 0) { return Empty; } string result = FastAllocateString(length); Span<char> resultSpan = new Span<char>(ref result._firstChar, result.Length); str0.CopyTo(resultSpan); str1.CopyTo(resultSpan.Slice(str0.Length)); return result; } public static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1, ReadOnlySpan<char> str2) { int length = checked(str0.Length + str1.Length + str2.Length); if (length == 0) { return Empty; } string result = FastAllocateString(length); Span<char> resultSpan = new Span<char>(ref result._firstChar, result.Length); str0.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str0.Length); str1.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str1.Length); str2.CopyTo(resultSpan); return result; } public static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1, ReadOnlySpan<char> str2, ReadOnlySpan<char> str3) { int length = checked(str0.Length + str1.Length + str2.Length + str3.Length); if (length == 0) { return Empty; } string result = FastAllocateString(length); Span<char> resultSpan = new Span<char>(ref result._firstChar, result.Length); str0.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str0.Length); str1.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str1.Length); str2.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str2.Length); str3.CopyTo(resultSpan); return result; } internal static string Concat(ReadOnlySpan<char> str0, ReadOnlySpan<char> str1, ReadOnlySpan<char> str2, ReadOnlySpan<char> str3, ReadOnlySpan<char> str4) { int length = checked(str0.Length + str1.Length + str2.Length + str3.Length + str4.Length); if (length == 0) { return Empty; } string result = FastAllocateString(length); Span<char> resultSpan = new Span<char>(ref result._firstChar, result.Length); str0.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str0.Length); str1.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str1.Length); str2.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str2.Length); str3.CopyTo(resultSpan); resultSpan = resultSpan.Slice(str3.Length); str4.CopyTo(resultSpan); return result; } public static string Concat(params string?[] values) { ArgumentNullException.ThrowIfNull(values); return Concat((ReadOnlySpan<string?>)values); } /// <summary> /// Concatenates the elements of a specified span of <see cref="string"/>. /// </summary> /// <param name="values">A span of <see cref="string"/> instances.</param> /// <returns>The concatenated elements of <paramref name="values"/>.</returns> public static string Concat(params ReadOnlySpan<string?> values) { if (values.Length <= 1) { return values.IsEmpty ? Empty : values[0] ?? Empty; } // It's possible that the input values array could be changed concurrently on another // thread, such that we can't trust that each read of values[i] will be equivalent. // Worst case, we can make a defensive copy of the array and use that, but we first // optimistically try the allocation and copies assuming that the array isn't changing, // which represents the 99.999% case, in particular since string.Concat is used for // string concatenation by the languages, with the input array being a params array. // Sum the lengths of all input strings long totalLengthLong = 0; for (int i = 0; i < values.Length; i++) { string? value = values[i]; if (value != null) { totalLengthLong += value.Length; } } // If it's too long, fail, or if it's empty, return an empty string. if (totalLengthLong > int.MaxValue) { ThrowHelper.ThrowOutOfMemoryException_StringTooLong(); } int totalLength = (int)totalLengthLong; if (totalLength == 0) { return Empty; } // Allocate a new string and copy each input string into it string result = FastAllocateString(totalLength); int copiedLength = 0; for (int i = 0; i < values.Length; i++) { string? value = values[i]; if (!IsNullOrEmpty(value)) { int valueLen = value.Length; if (valueLen > totalLength - copiedLength) { copiedLength = -1; break; } CopyStringContent(result, copiedLength, value); copiedLength += valueLen; } } // If we copied exactly the right amount, return the new string. Otherwise, // something changed concurrently to mutate the input array: fall back to // doing the concatenation again, but this time with a defensive copy. This // fall back should be extremely rare. return copiedLength == totalLength ? result : Concat((ReadOnlySpan<string?>)values.ToArray()); } public static string Format([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, object? arg0) { return FormatHelper(null, format, new ReadOnlySpan<object?>(in arg0)); } public static string Format([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, object? arg0, object? arg1) { return FormatHelper(null, format, [arg0, arg1]); } public static string Format([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, object? arg0, object? arg1, object? arg2) { return FormatHelper(null, format, [arg0, arg1, arg2]); } public static string Format([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { if (args is null) { // To preserve the original exception behavior, throw an exception about format if both // args and format are null. The actual null check for format is in FormatHelper. ArgumentNullException.Throw(format is null ? nameof(format) : nameof(args)); } return FormatHelper(null, format, (ReadOnlySpan<object?>)args); } /// <summary> /// Replaces the format item in a specified string with the string representation of a corresponding object in a specified span. /// </summary> /// <param name="format">A <see href="https://learn.microsoft.com/dotnet/standard/base-types/composite-formatting">composite format string</see>.</param> /// <param name="args">An object span that contains zero or more objects to format.</param> /// <returns>A copy of <paramref name="format"/> in which the format items have been replaced by the string representation of the corresponding objects in <paramref name="args"/>.</returns> public static string Format([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> args) { return FormatHelper(null, format, args); } public static string Format(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, object? arg0) { return FormatHelper(provider, format, new ReadOnlySpan<object?>(in arg0)); } public static string Format(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, object? arg0, object? arg1) { return FormatHelper(provider, format, [arg0, arg1]); } public static string Format(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, object? arg0, object? arg1, object? arg2) { return FormatHelper(provider, format, [arg0, arg1, arg2]); } public static string Format(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params object?[] args) { if (args is null) { // To preserve the original exception behavior, throw an exception about format if both // args and format are null. The actual null check for format is in FormatHelper. ArgumentNullException.Throw(format is null ? nameof(format) : nameof(args)); } return FormatHelper(provider, format, (ReadOnlySpan<object?>)args); } /// <summary> /// Replaces the format items in a string with the string representations of corresponding objects in a specified span. /// A parameter supplies culture-specific formatting information. /// </summary> /// <param name="provider">An object that supplies culture-specific formatting information.</param> /// <param name="format">A <see href="https://learn.microsoft.com/dotnet/standard/base-types/composite-formatting">composite format string</see>.</param> /// <param name="args">An object span that contains zero or more objects to format.</param> /// <returns>A copy of <paramref name="format"/> in which the format items have been replaced by the string representation of the corresponding objects in <paramref name="args"/>.</returns> public static string Format(IFormatProvider? provider, [StringSyntax(StringSyntaxAttribute.CompositeFormat)] string format, params ReadOnlySpan<object?> args) { return FormatHelper(provider, format, args); } private static unsafe string FormatHelper(IFormatProvider? provider, string format, ReadOnlySpan<object?> args) { ArgumentNullException.ThrowIfNull(format); var sb = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); sb.EnsureCapacity(format.Length + args.Length * 8); sb.AppendFormatHelper(provider, format, args); return sb.ToString(); } /// <summary> /// Replaces the format item or items in a <see cref="CompositeFormat"/> with the string representation of the corresponding objects. /// A parameter supplies culture-specific formatting information. /// </summary> /// <typeparam name="TArg0">The type of the first object to format.</typeparam> /// <param name="provider">An object that supplies culture-specific formatting information.</param> /// <param name="format">A <see cref="CompositeFormat"/>.</param> /// <param name="arg0">The first object to format.</param> /// <returns>The formatted string.</returns> /// <exception cref="ArgumentNullException"><paramref name="format"/> is null.</exception> /// <exception cref="FormatException">The index of a format item is greater than or equal to the number of supplied arguments.</exception> public static string Format<TArg0>(IFormatProvider? provider, CompositeFormat format, TArg0 arg0) { ArgumentNullException.ThrowIfNull(format); format.ValidateNumberOfArgs(1); return Format(provider, format, arg0, 0, 0, default); } /// <summary> /// Replaces the format item or items in a <see cref="CompositeFormat"/> with the string representation of the corresponding objects. /// A parameter supplies culture-specific formatting information. /// </summary> /// <typeparam name="TArg0">The type of the first object to format.</typeparam> /// <typeparam name="TArg1">The type of the second object to format.</typeparam> /// <param name="provider">An object that supplies culture-specific formatting information.</param> /// <param name="format">A <see cref="CompositeFormat"/>.</param> /// <param name="arg0">The first object to format.</param> /// <param name="arg1">The second object to format.</param> /// <returns>The formatted string.</returns> /// <exception cref="ArgumentNullException"><paramref name="format"/> is null.</exception> /// <exception cref="FormatException">The index of a format item is greater than or equal to the number of supplied arguments.</exception> public static string Format<TArg0, TArg1>(IFormatProvider? provider, CompositeFormat format, TArg0 arg0, TArg1 arg1) { ArgumentNullException.ThrowIfNull(format); format.ValidateNumberOfArgs(2); return Format(provider, format, arg0, arg1, 0, default); } /// <summary> /// Replaces the format item or items in a <see cref="CompositeFormat"/> with the string representation of the corresponding objects. /// A parameter supplies culture-specific formatting information. /// </summary> /// <typeparam name="TArg0">The type of the first object to format.</typeparam> /// <typeparam name="TArg1">The type of the second object to format.</typeparam> /// <typeparam name="TArg2">The type of the third object to format.</typeparam> /// <param name="provider">An object that supplies culture-specific formatting information.</param> /// <param name="format">A <see cref="CompositeFormat"/>.</param> /// <param name="arg0">The first object to format.</param> /// <param name="arg1">The second object to format.</param> /// <param name="arg2">The third object to format.</param> /// <returns>The formatted string.</returns> /// <exception cref="ArgumentNullException"><paramref name="format"/> is null.</exception> /// <exception cref="FormatException">The index of a format item is greater than or equal to the number of supplied arguments.</exception> public static string Format<TArg0, TArg1, TArg2>(IFormatProvider? provider, CompositeFormat format, TArg0 arg0, TArg1 arg1, TArg2 arg2) { ArgumentNullException.ThrowIfNull(format); format.ValidateNumberOfArgs(3); return Format(provider, format, arg0, arg1, arg2, default); } /// <summary> /// Replaces the format item or items in a <see cref="CompositeFormat"/> with the string representation of the corresponding objects. /// A parameter supplies culture-specific formatting information. /// </summary> /// <param name="provider">An object that supplies culture-specific formatting information.</param> /// <param name="format">A <see cref="CompositeFormat"/>.</param> /// <param name="args">An array of objects to format.</param> /// <returns>The formatted string.</returns> /// <exception cref="ArgumentNullException"><paramref name="format"/> is null.</exception> /// <exception cref="ArgumentNullException"><paramref name="args"/> is null.</exception> /// <exception cref="FormatException">The index of a format item is greater than or equal to the number of supplied arguments.</exception> public static string Format(IFormatProvider? provider, CompositeFormat format, params object?[] args) { ArgumentNullException.ThrowIfNull(format); ArgumentNullException.ThrowIfNull(args); return Format(provider, format, (ReadOnlySpan<object?>)args); } /// <summary> /// Replaces the format item or items in a <see cref="CompositeFormat"/> with the string representation of the corresponding objects. /// A parameter supplies culture-specific formatting information. /// </summary> /// <param name="provider">An object that supplies culture-specific formatting information.</param> /// <param name="format">A <see cref="CompositeFormat"/>.</param> /// <param name="args">A span of objects to format.</param> /// <returns>The formatted string.</returns> /// <exception cref="ArgumentNullException"><paramref name="format"/> is null.</exception> /// <exception cref="FormatException">The index of a format item is greater than or equal to the number of supplied arguments.</exception> public static string Format(IFormatProvider? provider, CompositeFormat format, params ReadOnlySpan<object?> args) { ArgumentNullException.ThrowIfNull(format); format.ValidateNumberOfArgs(args.Length); return args.Length switch { 0 => format._literalLength == format.Format.Length ? format.Format : Format(provider, format, (object?)null, 0, 0, args), 1 => Format(provider, format, args[0], 0, 0, args), 2 => Format(provider, format, args[0], args[1], 0, args), _ => Format(provider, format, args[0], args[1], args[2], args), }; } private static unsafe string Format<TArg0, TArg1, TArg2>(IFormatProvider? provider, CompositeFormat format, TArg0 arg0, TArg1 arg1, TArg2 arg2, ReadOnlySpan<object?> args) { // If there's no formatting to be done and no brace escaping in the format string, we can just return // the original format string as the result. If there is brace escaping, we need to process the segments // so that the escaped braces are properly unescaped in the result. if (format._formattedCount == 0 && format._literalLength == format.Format.Length) { return format.Format; } // Create the interpolated string handler. var handler = new DefaultInterpolatedStringHandler(format._literalLength, format._formattedCount, provider, stackalloc char[StackallocCharBufferSizeLimit]); // Format each segment. foreach ((string? Literal, int ArgIndex, int Alignment, string? Format) segment in format._segments) { if (segment.Literal is string literal) { handler.AppendLiteral(literal); } else { int index = segment.ArgIndex; switch (index) { case 0: handler.AppendFormatted(arg0, segment.Alignment, segment.Format); break; case 1: handler.AppendFormatted(arg1, segment.Alignment, segment.Format); break; case 2: handler.AppendFormatted(arg2, segment.Alignment, segment.Format); break; default: Debug.Assert(index > 2); handler.AppendFormatted(args[index], segment.Alignment, segment.Format); break; } } } // Complete the operation. return handler.ToStringAndClear(); } public string Insert(int startIndex, string value) { ArgumentNullException.ThrowIfNull(value); ArgumentOutOfRangeException.ThrowIfGreaterThan((uint)startIndex, (uint)Length, nameof(startIndex)); int oldLength = Length; int insertLength = value.Length; if (oldLength == 0) return value; if (insertLength == 0) return this; // In case this computation overflows, newLength will be negative and FastAllocateString throws OutOfMemoryException int newLength = oldLength + insertLength; string result = FastAllocateString(newLength); Buffer.Memmove(ref result._firstChar, ref _firstChar, (nuint)startIndex); Buffer.Memmove(ref Unsafe.Add(ref result._firstChar, startIndex), ref value._firstChar, (nuint)insertLength); Buffer.Memmove(ref Unsafe.Add(ref result._firstChar, startIndex + insertLength), ref Unsafe.Add(ref _firstChar, startIndex), (nuint)(oldLength - startIndex)); return result; } public static string Join(char separator, params string?[] value) { if (value == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); } return JoinCore(new ReadOnlySpan<char>(in separator), new ReadOnlySpan<string?>(value)); } /// <summary> /// Concatenates a span of strings, using the specified separator between each member. /// </summary> /// <param name="separator">The character to use as a separator. <paramref name="separator"/> is included in the returned string only if <paramref name="value"/> has more than one element.</param> /// <param name="value">A span that contains the elements to concatenate.</param> /// <returns> /// A string that consists of the elements of <paramref name="value"/> delimited by the <paramref name="separator"/> string. /// -or- /// <see cref="Empty"/> if <paramref name="value"/> has zero elements. /// </returns> public static string Join(char separator, params ReadOnlySpan<string?> value) { return JoinCore(new ReadOnlySpan<char>(in separator), value); } public static string Join(string? separator, params string?[] value) { if (value == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value); } return JoinCore(separator.AsSpan(), new ReadOnlySpan<string?>(value)); } /// <summary> /// Concatenates a span of strings, using the specified separator between each member. /// </summary> /// <param name="separator">The string to use as a separator. <paramref name="separator"/> is included in the returned string only if <paramref name="value"/> has more than one element.</param> /// <param name="value">A span that contains the elements to concatenate.</param> /// <returns> /// A string that consists of the elements of <paramref name="value"/> delimited by the <paramref name="separator"/> string. /// -or- /// <see cref="Empty"/> if <paramref name="value"/> has zero elements. /// </returns> public static string Join(string? separator, params ReadOnlySpan<string?> value) { return JoinCore(separator.AsSpan(), value); } public static string Join(char separator, string?[] value, int startIndex, int count) => JoinCore(new ReadOnlySpan<char>(in separator), value, startIndex, count); public static string Join(string? separator, string?[] value, int startIndex, int count) => JoinCore(separator.AsSpan(), value, startIndex, count); private static string JoinCore(ReadOnlySpan<char> separator, string?[] value, int startIndex, int count) { ArgumentNullException.ThrowIfNull(value); ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(count); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, value.Length - count); return JoinCore(separator, new ReadOnlySpan<string?>(value, startIndex, count)); } public static unsafe string Join(string? separator, IEnumerable<string?> values) { if (values is List<string?> valuesList) { return JoinCore(separator.AsSpan(), CollectionsMarshal.AsSpan(valuesList)); } if (values is string?[] valuesArray) { return JoinCore(separator.AsSpan(), new ReadOnlySpan<string?>(valuesArray)); } if (values == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.values); } using (IEnumerator<string?> en = values.GetEnumerator()) { if (!en.MoveNext()) { return Empty; } string? firstValue = en.Current; if (!en.MoveNext()) { // Only one value available return firstValue ?? Empty; } // Null separator and values are handled by the StringBuilder var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); result.Append(firstValue); do { result.Append(separator); result.Append(en.Current); } while (en.MoveNext()); return result.ToString(); } } public static string Join(char separator, params object?[] values) { if (values == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.values); } return JoinCore(new ReadOnlySpan<char>(in separator), (ReadOnlySpan<object?>)values); } /// <summary> /// Concatenates the string representations of a span of objects, using the specified separator between each member. /// </summary> /// <param name="separator">The character to use as a separator. <paramref name="separator"/> is included in the returned string only if value has more than one element.</param> /// <param name="values">A span of objects whose string representations will be concatenated.</param> /// <returns> /// A string that consists of the elements of <paramref name="values"/> delimited by the <paramref name="separator"/> character. /// -or- /// <see cref="Empty"/> if <paramref name="values"/> has zero elements. /// </returns> public static string Join(char separator, params ReadOnlySpan<object?> values) => JoinCore(new ReadOnlySpan<char>(in separator), values); public static string Join(string? separator, params object?[] values) { if (values == null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.values); } return JoinCore(separator.AsSpan(), (ReadOnlySpan<object?>)values); } /// <summary> /// Concatenates the string representations of a span of objects, using the specified separator between each member. /// </summary> /// <param name="separator">The string to use as a separator. <paramref name="separator"/> is included in the returned string only if <paramref name="values"/> has more than one element.</param> /// <param name="values">A span of objects whose string representations will be concatenated.</param> /// <returns> /// A string that consists of the elements of <paramref name="values"/> delimited by the <paramref name="separator"/> string. /// -or- /// <see cref="Empty"/> if <paramref name="values"/> has zero elements. /// </returns> public static string Join(string? separator, params ReadOnlySpan<object?> values) => JoinCore(separator.AsSpan(), values); private static unsafe string JoinCore(ReadOnlySpan<char> separator, ReadOnlySpan<object?> values) { if (values.IsEmpty) { return Empty; } string? firstString = values[0]?.ToString(); if (values.Length == 1) { return firstString ?? Empty; } var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); result.Append(firstString); for (int i = 1; i < values.Length; i++) { result.Append(separator); object? value = values[i]; if (value != null) { result.Append(value.ToString()); } } return result.ToString(); } public static string Join<T>(char separator, IEnumerable<T> values) => JoinCore(new ReadOnlySpan<char>(in separator), values); public static string Join<T>(string? separator, IEnumerable<T> values) => JoinCore(separator.AsSpan(), values); private static unsafe string JoinCore<T>(ReadOnlySpan<char> separator, IEnumerable<T> values) { if (values is null) { ThrowHelper.ThrowArgumentNullException(ExceptionArgument.values); } if (typeof(T) == typeof(string)) { if (values.GetType() == typeof(List<string?>)) // avoid accidentally bypassing a derived type's reimplementation of IEnumerable<T> { return JoinCore(separator, CollectionsMarshal.AsSpan((List<string?>)values)); } if (values is string?[] valuesArray) { return JoinCore(separator, new ReadOnlySpan<string?>(valuesArray)); } } using (IEnumerator<T> e = values.GetEnumerator()) { if (!e.MoveNext()) { // If the enumerator is empty, just return an empty string. return Empty; } if (typeof(T) == typeof(char)) { // Special-case T==char, as we can handle that case much more efficiently, // and string.Concat(IEnumerable<char>) can be used as an efficient // enumerable-based equivalent of new string(char[]). IEnumerator<char> en = Unsafe.As<IEnumerator<char>>(e); char c = en.Current; // save the first value if (!en.MoveNext()) { // There was only one char. Return a string from it directly. return CreateFromChar(c); } // Create the builder, add the char we already enumerated, // add the rest, and then get the resulting string. var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); result.Append(c); // first value do { if (!separator.IsEmpty) { result.Append(separator); } c = en.Current; result.Append(c); } while (en.MoveNext()); return result.ToString(); } else if (typeof(T).IsValueType && default(T) is ISpanFormattable) { // Special-case value types that are ISpanFormattable, as we can implement those to avoid // all string allocations for the individual values. We only do this for value types because // a) value types are much more likely to implement ISpanFormattable, and b) we can use // DefaultInterpolatedStringHandler to do all the heavy lifting, and it's more efficient // for value types as the checks it does for interface implementations are all elided. T value = e.Current; // save the first value if (!e.MoveNext()) { // There was only one value. Return a string from it directly. return value!.ToString() ?? Empty; } var result = new DefaultInterpolatedStringHandler(0, 0, CultureInfo.CurrentCulture, stackalloc char[StackallocCharBufferSizeLimit]); result.AppendFormatted(value); // first value do { if (!separator.IsEmpty) { result.AppendFormatted(separator); } result.AppendFormatted(e.Current); } while (e.MoveNext()); return result.ToStringAndClear(); } else { // For all other Ts, fall back to calling ToString on each and appending the resulting // string to a builder. string? firstString = e.Current?.ToString(); // save the first value if (!e.MoveNext()) { return firstString ?? Empty; } var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); result.Append(firstString); do { if (!separator.IsEmpty) { result.Append(separator); } result.Append(e.Current?.ToString()); } while (e.MoveNext()); return result.ToString(); } } } private static string JoinCore(ReadOnlySpan<char> separator, ReadOnlySpan<string?> values) { if (values.Length <= 1) { return values.IsEmpty ? Empty : values[0] ?? Empty; } long totalSeparatorsLength = (long)(values.Length - 1) * separator.Length; if (totalSeparatorsLength > int.MaxValue) { ThrowHelper.ThrowOutOfMemoryException_StringTooLong(); } int totalLength = (int)totalSeparatorsLength; // Calculate the length of the resultant string so we know how much space to allocate. foreach (string? value in values) { if (value != null) { totalLength += value.Length; if (totalLength < 0) // Check for overflow { ThrowHelper.ThrowOutOfMemoryException_StringTooLong(); } } } if (totalLength == 0) { return Empty; } // Copy each of the strings into the result buffer, interleaving with the separator. string result = FastAllocateString(totalLength); int copiedLength = 0; for (int i = 0; i < values.Length; i++) { // It's possible that another thread may have mutated the input array // such that our second read of an index will not be the same string // we got during the first read. // We range check again to avoid buffer overflows if this happens. if (values[i] is string value) { int valueLen = value.Length; if (valueLen > totalLength - copiedLength) { copiedLength = -1; break; } // Fill in the value. CopyStringContent(result, copiedLength, value); copiedLength += valueLen; } if (i < values.Length - 1) { // Fill in the separator. // Special-case length 1 to avoid additional overheads of CopyTo. // This is common due to the char separator overload. ref char dest = ref Unsafe.Add(ref result._firstChar, copiedLength); if (separator.Length == 1) { dest = separator[0]; } else { separator.CopyTo(new Span<char>(ref dest, separator.Length)); } copiedLength += separator.Length; } } // If we copied exactly the right amount, return the new string. Otherwise, // something changed concurrently to mutate the input array: fall back to // doing the concatenation again, but this time with a defensive copy. This // fall back should be extremely rare. return copiedLength == totalLength ? result : JoinCore(separator, values.ToArray().AsSpan()); } public string PadLeft(int totalWidth) => PadLeft(totalWidth, ' '); public string PadLeft(int totalWidth, char paddingChar) { ArgumentOutOfRangeException.ThrowIfNegative(totalWidth); int oldLength = Length; int count = totalWidth - oldLength; if (count <= 0) return this; string result = FastAllocateString(totalWidth); new Span<char>(ref result._firstChar, count).Fill(paddingChar); Buffer.Memmove(ref Unsafe.Add(ref result._firstChar, count), ref _firstChar, (nuint)oldLength); return result; } public string PadRight(int totalWidth) => PadRight(totalWidth, ' '); public string PadRight(int totalWidth, char paddingChar) { ArgumentOutOfRangeException.ThrowIfNegative(totalWidth); int oldLength = Length; int count = totalWidth - oldLength; if (count <= 0) return this; string result = FastAllocateString(totalWidth); Buffer.Memmove(ref result._firstChar, ref _firstChar, (nuint)oldLength); new Span<char>(ref Unsafe.Add(ref result._firstChar, oldLength), count).Fill(paddingChar); return result; } public string Remove(int startIndex, int count) { ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(count); int oldLength = this.Length; ArgumentOutOfRangeException.ThrowIfGreaterThan(count, oldLength - startIndex); if (count == 0) return this; int newLength = oldLength - count; if (newLength == 0) return Empty; string result = FastAllocateString(newLength); Buffer.Memmove(ref result._firstChar, ref _firstChar, (nuint)startIndex); Buffer.Memmove(ref Unsafe.Add(ref result._firstChar, startIndex), ref Unsafe.Add(ref _firstChar, startIndex + count), (nuint)(newLength - startIndex)); return result; } // a remove that just takes a startindex. public string Remove(int startIndex) { if ((uint)startIndex > Length) throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex < 0 ? SR.ArgumentOutOfRange_StartIndex : SR.ArgumentOutOfRange_StartIndexLargerThanLength); return Substring(0, startIndex); } public string Replace(string oldValue, string? newValue, bool ignoreCase, CultureInfo? culture) { return ReplaceCore(oldValue, newValue, culture?.CompareInfo, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None); } public string Replace(string oldValue, string? newValue, StringComparison comparisonType) => comparisonType switch { StringComparison.CurrentCulture or StringComparison.CurrentCultureIgnoreCase => ReplaceCore(oldValue, newValue, CultureInfo.CurrentCulture.CompareInfo, GetCaseCompareOfComparisonCulture(comparisonType)), StringComparison.InvariantCulture or StringComparison.InvariantCultureIgnoreCase => ReplaceCore(oldValue, newValue, CompareInfo.Invariant, GetCaseCompareOfComparisonCulture(comparisonType)), StringComparison.Ordinal => Replace(oldValue, newValue), StringComparison.OrdinalIgnoreCase => ReplaceCore(oldValue, newValue, CompareInfo.Invariant, CompareOptions.OrdinalIgnoreCase), _ => throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType)), }; private string ReplaceCore(string oldValue, string? newValue, CompareInfo? ci, CompareOptions options) { ArgumentException.ThrowIfNullOrEmpty(oldValue); // If they asked to replace oldValue with a null, replace all occurrences // with the empty string. AsSpan() will normalize appropriately. // // If inner ReplaceCore method returns null, it means no substitutions were // performed, so as an optimization we'll return the original string. return ReplaceCore(this, oldValue.AsSpan(), newValue.AsSpan(), ci ?? CultureInfo.CurrentCulture.CompareInfo, options) ?? this; } private static unsafe string? ReplaceCore(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue, CompareInfo compareInfo, CompareOptions options) { Debug.Assert(!oldValue.IsEmpty); Debug.Assert(compareInfo != null); var result = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); result.EnsureCapacity(searchSpace.Length); bool hasDoneAnyReplacements = false; while (true) { int index = compareInfo.IndexOf(searchSpace, oldValue, options, out int matchLength); // There's the possibility that 'oldValue' has zero collation weight (empty string equivalent). // If this is the case, we behave as if there are no more substitutions to be made. if (index < 0 || matchLength == 0) { break; } // append the unmodified portion of search space result.Append(searchSpace.Slice(0, index)); // append the replacement result.Append(newValue); searchSpace = searchSpace.Slice(index + matchLength); hasDoneAnyReplacements = true; } // Didn't find 'oldValue' in the remaining search space, or the match // consisted only of zero collation weight characters. As an optimization, // if we have not yet performed any replacements, we'll save the // allocation. if (!hasDoneAnyReplacements) { result.Dispose(); return null; } // Append what remains of the search space, then allocate the new string. result.Append(searchSpace); return result.ToString(); } // Replaces all instances of oldChar with newChar. // public string Replace(char oldChar, char newChar) { if (oldChar == newChar) return this; int firstIndex = IndexOf(oldChar); if (firstIndex < 0) return this; nuint remainingLength = (uint)(Length - firstIndex); string result = FastAllocateString(Length); int copyLength = firstIndex; // Copy the characters already proven not to match. if (copyLength > 0) { Buffer.Memmove(ref result._firstChar, ref _firstChar, (uint)copyLength); } // Copy the remaining characters, doing the replacement as we go. ref ushort pSrc = ref Unsafe.Add(ref GetRawStringDataAsUInt16(), (uint)copyLength); ref ushort pDst = ref Unsafe.Add(ref result.GetRawStringDataAsUInt16(), (uint)copyLength); // If the string is long enough for vectorization to kick in, we'd like to // process the remaining elements vectorized too. // Thus we adjust the pointers so that at least one full vector from the end can be processed. nuint length = (uint)Length; if (Vector512.IsHardwareAccelerated && length >= (uint)Vector512<ushort>.Count) { nuint adjust = (length - remainingLength) & ((uint)Vector512<ushort>.Count - 1); pSrc = ref Unsafe.Subtract(ref pSrc, adjust); pDst = ref Unsafe.Subtract(ref pDst, adjust); remainingLength += adjust; } else if (Vector256.IsHardwareAccelerated && length >= (uint)Vector256<ushort>.Count) { nuint adjust = (length - remainingLength) & ((uint)Vector256<ushort>.Count - 1); pSrc = ref Unsafe.Subtract(ref pSrc, adjust); pDst = ref Unsafe.Subtract(ref pDst, adjust); remainingLength += adjust; } else if (Vector128.IsHardwareAccelerated && length >= (uint)Vector128<ushort>.Count) { nuint adjust = (length - remainingLength) & ((uint)Vector128<ushort>.Count - 1); pSrc = ref Unsafe.Subtract(ref pSrc, adjust); pDst = ref Unsafe.Subtract(ref pDst, adjust); remainingLength += adjust; } SpanHelpers.ReplaceValueType(ref pSrc, ref pDst, oldChar, newChar, remainingLength); return result; } public unsafe string Replace(string oldValue, string? newValue) { ArgumentException.ThrowIfNullOrEmpty(oldValue); // If newValue is null, treat it as an empty string. Callers use this to remove the oldValue. newValue ??= Empty; // Track the locations of oldValue to be replaced. var replacementIndices = new ValueListBuilder<int>(stackalloc int[StackallocIntBufferSizeLimit]); if (oldValue.Length == 1) { // Special-case oldValues that are a single character. Even though there's an overload that takes // a single character, its newValue is also a single character, so this overload ends up being used // often to remove characters by having an empty newValue. if (newValue.Length == 1) { // With both the oldValue and newValue being a single character, it's cheaper to just use the other overload. return Replace(oldValue[0], newValue[0]); } // Find all occurrences of the oldValue character. char c = oldValue[0]; int i = 0; if (PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(c)) { while (true) { int pos = PackedSpanHelpers.IndexOf(ref Unsafe.Add(ref _firstChar, i), c, Length - i); if (pos < 0) { break; } replacementIndices.Append(i + pos); i += pos + 1; } } else { while (true) { int pos = SpanHelpers.NonPackedIndexOfChar(ref Unsafe.Add(ref _firstChar, i), c, Length - i); if (pos < 0) { break; } replacementIndices.Append(i + pos); i += pos + 1; } } } else { // Find all occurrences of the oldValue string. int i = 0; while (true) { int pos = SpanHelpers.IndexOf(ref Unsafe.Add(ref _firstChar, i), Length - i, ref oldValue._firstChar, oldValue.Length); if (pos < 0) { break; } replacementIndices.Append(i + pos); i += pos + oldValue.Length; } } // If the oldValue wasn't found, just return the original string. if (replacementIndices.Length == 0) { return this; } // Perform the replacement. String allocation and copying is in separate method to make this method faster // for the case where nothing needs replacing. string dst = ReplaceHelper(oldValue.Length, newValue, replacementIndices.AsSpan()); replacementIndices.Dispose(); return dst; } private string ReplaceHelper(int oldValueLength, string newValue, ReadOnlySpan<int> indices) { Debug.Assert(indices.Length > 0); long dstLength = this.Length + ((long)(newValue.Length - oldValueLength)) * indices.Length; if (dstLength > int.MaxValue) ThrowHelper.ThrowOutOfMemoryException_StringTooLong(); string dst = FastAllocateString((int)dstLength); Span<char> dstSpan = new Span<char>(ref dst._firstChar, dst.Length); int thisIdx = 0; int dstIdx = 0; for (int r = 0; r < indices.Length; r++) { int replacementIdx = indices[r]; // Copy over the non-matching portion of the original that precedes this occurrence of oldValue. int count = replacementIdx - thisIdx; if (count != 0) { this.AsSpan(thisIdx, count).CopyTo(dstSpan.Slice(dstIdx)); dstIdx += count; } thisIdx = replacementIdx + oldValueLength; // Copy over newValue to replace the oldValue. newValue.CopyTo(dstSpan.Slice(dstIdx)); dstIdx += newValue.Length; } // Copy over the final non-matching portion at the end of the string. Debug.Assert(this.Length - thisIdx == dstSpan.Length - dstIdx); this.AsSpan(thisIdx).CopyTo(dstSpan.Slice(dstIdx)); return dst; } /// <summary> /// Returns a new string in which all occurrences of a specified Unicode rune in this instance are replaced with another specified Unicode rune using an ordinal comparison. /// </summary> /// <param name="oldRune">The Unicode character to be replaced.</param> /// <param name="newRune">The Unicode character to replace all occurrences of <paramref name="oldRune"/>.</param> /// <returns> /// A string that is equivalent to this instance except that all instances of <paramref name="oldRune"/> are replaced with <paramref name="newRune"/>. /// If <paramref name="oldRune"/> is not found in the current instance, the method returns the current instance unchanged. /// </returns> public unsafe string Replace(Rune oldRune, Rune newRune) { if (oldRune.IsBmp && newRune.IsBmp) { return Replace((char)oldRune.Value, (char)newRune.Value); } ReadOnlySpan<char> oldChars = oldRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); ReadOnlySpan<char> newChars = newRune.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); return ReplaceCore(this, oldChars, newChars, CompareInfo.Invariant, CompareOptions.Ordinal) ?? this; } /// <summary> /// Replaces all newline sequences in the current string with <see cref="Environment.NewLine"/>. /// </summary> /// <returns> /// A string whose contents match the current string, but with all newline sequences replaced /// with <see cref="Environment.NewLine"/>. /// </returns> /// <remarks> /// This method searches for all newline sequences within the string and canonicalizes them to match /// the newline sequence for the current environment. For example, when running on Windows, all /// occurrences of non-Windows newline sequences will be replaced with the sequence CRLF. When /// running on Unix, all occurrences of non-Unix newline sequences will be replaced with /// a single LF character. /// /// It is not recommended that protocol parsers utilize this API. Protocol specifications often /// mandate specific newline sequences. For example, HTTP/1.1 (RFC 8615) mandates that the request /// line, status line, and headers lines end with CRLF. Since this API operates over a wide range /// of newline sequences, a protocol parser utilizing this API could exhibit behaviors unintended /// by the protocol's authors. /// /// This overload is equivalent to calling <see cref="ReplaceLineEndings(string)"/>, passing /// <see cref="Environment.NewLine"/> as the <em>replacementText</em> parameter. /// /// This method is guaranteed O(n) complexity, where <em>n</em> is the length of the input string. /// </remarks> public string ReplaceLineEndings() => ReplaceLineEndings(Environment.NewLineConst); /// <summary> /// Replaces all newline sequences in the current string with <paramref name="replacementText"/>. /// </summary> /// <returns> /// A string whose contents match the current string, but with all newline sequences replaced /// with <paramref name="replacementText"/>. /// </returns> /// <remarks> /// This method searches for all newline sequences within the string and canonicalizes them to the /// newline sequence provided by <paramref name="replacementText"/>. If <paramref name="replacementText"/> /// is <see cref="Empty"/>, all newline sequences within the string will be removed. /// /// It is not recommended that protocol parsers utilize this API. Protocol specifications often /// mandate specific newline sequences. For example, HTTP/1.1 (RFC 8615) mandates that the request /// line, status line, and headers lines end with CRLF. Since this API operates over a wide range /// of newline sequences, a protocol parser utilizing this API could exhibit behaviors unintended /// by the protocol's authors. /// /// The list of recognized newline sequences is CR (U+000D), LF (U+000A), CRLF (U+000D U+000A), /// NEL (U+0085), LS (U+2028), FF (U+000C), and PS (U+2029). This list is given by the Unicode /// Standard, Sec. 5.8, Recommendation R4 and Table 5-2. /// /// This method is guaranteed O(n * r) complexity, where <em>n</em> is the length of the input string, /// and where <em>r</em> is the length of <paramref name="replacementText"/>. /// </remarks> [MethodImpl(MethodImplOptions.AggressiveInlining)] public string ReplaceLineEndings(string replacementText) { return replacementText == "\n" ? ReplaceLineEndingsWithLineFeed() : ReplaceLineEndingsCore(replacementText); } private unsafe string ReplaceLineEndingsCore(string replacementText) { ArgumentNullException.ThrowIfNull(replacementText); // Early-exit: do we need to do anything at all? // If not, return this string as-is. int idxOfFirstNewlineChar = IndexOfNewlineChar(this, replacementText, out int stride); if (idxOfFirstNewlineChar < 0) { return this; } // While writing to the builder, we don't bother memcpying the first // or the last segment into the builder. We'll use the builder only // for the intermediate segments, then we'll sandwich everything together // with one final string.Concat call. ReadOnlySpan<char> firstSegment = this.AsSpan(0, idxOfFirstNewlineChar); ReadOnlySpan<char> remaining = this.AsSpan(idxOfFirstNewlineChar + stride); var builder = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); while (true) { int idx = IndexOfNewlineChar(remaining, replacementText, out stride); if (idx < 0) { break; } // no more newline chars builder.Append(replacementText); builder.Append(remaining.Slice(0, idx)); remaining = remaining.Slice(idx + stride); } string retVal = Concat(firstSegment, builder.AsSpan(), replacementText, remaining); builder.Dispose(); return retVal; } // Scans the input text, returning the index of the first newline char other than the replacement text. // Newline chars are given by the Unicode Standard, Sec. 5.8. private static int IndexOfNewlineChar(ReadOnlySpan<char> text, string replacementText, out int stride) { // !! IMPORTANT !! // // We expect this method may be called with untrusted input, which means we need to // bound the worst-case runtime of this method. We rely on MemoryExtensions.IndexOfAny // having worst-case runtime O(i), where i is the index of the first needle match within // the haystack; or O(n) if no needle is found. This ensures that in the common case // of this method being called within a loop, the worst-case runtime is O(n) rather than // O(n^2), where n is the length of the input text. stride = default; int offset = 0; while (true) { int idx = text.IndexOfAny(SearchValuesStorage.NewLineChars); if ((uint)idx >= (uint)text.Length) { return -1; } offset += idx; stride = 1; // needle found // Did we match CR? If so, and if it's followed by LF, then we need // to consume both chars as a single newline function match. if (text[idx] == '\r') { int nextCharIdx = idx + 1; if ((uint)nextCharIdx < (uint)text.Length && text[nextCharIdx] == '\n') { stride = 2; if (replacementText != "\r\n") { return offset; } } else if (replacementText != "\r") { return offset; } } else if (replacementText.Length != 1 || replacementText[0] != text[idx]) { return offset; } offset += stride; text = text.Slice(idx + stride); } } private unsafe string ReplaceLineEndingsWithLineFeed() { // If we are going to replace the new line with a line feed ('\n'), // we can skip looking for it to avoid breaking out of the vectorized path unnecessarily. int idxOfFirstNewlineChar = this.AsSpan().IndexOfAny(SearchValuesStorage.NewLineCharsExceptLineFeed); if ((uint)idxOfFirstNewlineChar >= (uint)Length) { return this; } int stride = this[idxOfFirstNewlineChar] == '\r' && (uint)(idxOfFirstNewlineChar + 1) < (uint)Length && this[idxOfFirstNewlineChar + 1] == '\n' ? 2 : 1; ReadOnlySpan<char> remaining = this.AsSpan(idxOfFirstNewlineChar + stride); var builder = new ValueStringBuilder(stackalloc char[StackallocCharBufferSizeLimit]); while (true) { int idx = remaining.IndexOfAny(SearchValuesStorage.NewLineCharsExceptLineFeed); if ((uint)idx >= (uint)remaining.Length) break; // no more newline chars stride = remaining[idx] == '\r' && (uint)(idx + 1) < (uint)remaining.Length && remaining[idx + 1] == '\n' ? 2 : 1; builder.Append('\n'); builder.Append(remaining.Slice(0, idx)); remaining = remaining.Slice(idx + stride); } builder.Append('\n'); string retVal = Concat(this.AsSpan(0, idxOfFirstNewlineChar), builder.AsSpan(), remaining); builder.Dispose(); return retVal; } public string[] Split(char separator, StringSplitOptions options = StringSplitOptions.None) { return SplitInternal(new ReadOnlySpan<char>(in separator), int.MaxValue, options); } public string[] Split(char separator, int count, StringSplitOptions options = StringSplitOptions.None) { return SplitInternal(new ReadOnlySpan<char>(in separator), count, options); } /// <summary> /// Splits a string into substrings based on a specified delimiting rune and, optionally, options. /// </summary> /// <param name="separator">A character that delimits the substrings in this string.</param> /// <param name="options">A bitwise combination of the enumeration values that specifies whether to trim substrings and include empty substrings.</param> /// <returns>An array whose elements contain the substrings from this instance that are delimited by <paramref name="separator"/>.</returns> public string[] Split(Rune separator, StringSplitOptions options = StringSplitOptions.None) { return Split(separator, int.MaxValue, options); } /// <summary> /// Splits a string into a maximum number of substrings based on the provided rune separator, optionally omitting empty substrings from the result. /// </summary> /// <param name="separator">A character that delimits the substrings in this string.</param> /// <param name="count">The maximum number of elements expected in the array.</param> /// <param name="options">A bitwise combination of the enumeration values that specifies whether to trim substrings and include empty substrings.</param> /// <returns>An array whose elements contain the substrings from this instance that are delimited by <paramref name="separator"/>.</returns> public unsafe string[] Split(Rune separator, int count, StringSplitOptions options = StringSplitOptions.None) { if (separator.IsBmp) { return Split((char)separator.Value, count, options); } ArgumentOutOfRangeException.ThrowIfNegative(count); CheckStringSplitOptions(options); // Ensure matching the string separator overload. if (count <= 1 || Length == 0) { return CreateSplitArrayOfThisAsSoleValue(options, count); } ReadOnlySpan<char> separatorSpan = separator.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); return Split(separatorSpan, count, options); } // Creates an array of strings by splitting this string at each // occurrence of a separator. The separator is searched for, and if found, // the substring preceding the occurrence is stored as the first element in // the array of strings. We then continue in this manner by searching // the substring that follows the occurrence. On the other hand, if the separator // is not found, the array of strings will contain this instance as its only element. // If the separator is null // whitespace (i.e., Character.IsWhitespace) is used as the separator. // public string[] Split(params char[]? separator) { return SplitInternal(separator, int.MaxValue, StringSplitOptions.None); } /// <summary> /// Splits a string into substrings based on specified delimiting characters. /// </summary> /// <param name="separator">A span of delimiting characters, or an empty span that contains no delimiters.</param> /// <returns>An array whose elements contain the substrings from this instance that are delimited by one or more characters in <paramref name="separator"/>.</returns> public string[] Split(params ReadOnlySpan<char> separator) { return SplitInternal(separator, int.MaxValue, StringSplitOptions.None); } // Creates an array of strings by splitting this string at each // occurrence of a separator. The separator is searched for, and if found, // the substring preceding the occurrence is stored as the first element in // the array of strings. We then continue in this manner by searching // the substring that follows the occurrence. On the other hand, if the separator // is not found, the array of strings will contain this instance as its only element. // If the separator is the empty string (i.e., string.Empty), then // whitespace (i.e., Character.IsWhitespace) is used as the separator. // If there are more than count different strings, the last n-(count-1) // elements are concatenated and added as the last string. // public string[] Split(char[]? separator, int count) { return SplitInternal(separator, count, StringSplitOptions.None); } public string[] Split(char[]? separator, StringSplitOptions options) { return SplitInternal(separator, int.MaxValue, options); } public string[] Split(char[]? separator, int count, StringSplitOptions options) { return SplitInternal(separator, count, options); } private unsafe string[] SplitInternal(ReadOnlySpan<char> separators, int count, StringSplitOptions options) { ArgumentOutOfRangeException.ThrowIfNegative(count); CheckStringSplitOptions(options); ShortCircuit: if (count <= 1 || Length == 0) { // Per the method's documentation, we'll short-circuit the search for separators. // But we still need to post-process the results based on the caller-provided flags. return CreateSplitArrayOfThisAsSoleValue(options, count); } if (separators.IsEmpty && count > Length) { // Caller is already splitting on whitespace; no need for separate trim step if the count is sufficient // to examine the whole input. options &= ~StringSplitOptions.TrimEntries; } var sepListBuilder = new ValueListBuilder<int>(stackalloc int[StackallocIntBufferSizeLimit]); MakeSeparatorListAny(this, separators, ref sepListBuilder); ReadOnlySpan<int> sepList = sepListBuilder.AsSpan(); // Handle the special case of no replaces. if (sepList.Length == 0) { count = 1; goto ShortCircuit; } string[] result = (options != StringSplitOptions.None) ? SplitWithPostProcessing(sepList, default, 1, count, options) : SplitWithoutPostProcessing(sepList, default, 1, count); sepListBuilder.Dispose(); return result; } public string[] Split(string? separator, StringSplitOptions options = StringSplitOptions.None) { return SplitInternal(separator ?? Empty, null, int.MaxValue, options); } public string[] Split(string? separator, int count, StringSplitOptions options = StringSplitOptions.None) { return SplitInternal(separator ?? Empty, null, count, options); } public string[] Split(string[]? separator, StringSplitOptions options) { return SplitInternal(null, separator, int.MaxValue, options); } public string[] Split(string[]? separator, int count, StringSplitOptions options) { return SplitInternal(null, separator, count, options); } private unsafe string[] SplitInternal(string? separator, string?[]? separators, int count, StringSplitOptions options) { ArgumentOutOfRangeException.ThrowIfNegative(count); CheckStringSplitOptions(options); bool singleSeparator = separator != null; if (!singleSeparator && (separators == null || separators.Length == 0)) { // split on whitespace return SplitInternal(default(ReadOnlySpan<char>), count, options); } ShortCircuit: if (count <= 1 || Length == 0) { // Per the method's documentation, we'll short-circuit the search for separators. // But we still need to post-process the results based on the caller-provided flags. return CreateSplitArrayOfThisAsSoleValue(options, count); } if (singleSeparator) { if (separator!.Length == 0) { count = 1; goto ShortCircuit; } else { return SplitInternal(separator, count, options); } } var sepListBuilder = new ValueListBuilder<int>(stackalloc int[StackallocIntBufferSizeLimit]); var lengthListBuilder = new ValueListBuilder<int>(stackalloc int[StackallocIntBufferSizeLimit]); MakeSeparatorListAny(this, separators, ref sepListBuilder, ref lengthListBuilder); ReadOnlySpan<int> sepList = sepListBuilder.AsSpan(); ReadOnlySpan<int> lengthList = lengthListBuilder.AsSpan(); // Handle the special case of no replaces. if (sepList.Length == 0) { return CreateSplitArrayOfThisAsSoleValue(options, count); } string[] result = (options != StringSplitOptions.None) ? SplitWithPostProcessing(sepList, lengthList, 0, count, options) : SplitWithoutPostProcessing(sepList, lengthList, 0, count); sepListBuilder.Dispose(); lengthListBuilder.Dispose(); return result; } private string[] CreateSplitArrayOfThisAsSoleValue(StringSplitOptions options, int count) { Debug.Assert(count >= 0); if (count != 0) { string candidate = this; if ((options & StringSplitOptions.TrimEntries) != 0) { candidate = candidate.Trim(); } if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || candidate.Length != 0) { return [candidate]; } } return []; } private string[] SplitInternal(string separator, int count, StringSplitOptions options) => Split(separator.AsSpan(), count, options); private unsafe string[] Split(ReadOnlySpan<char> separator, int count, StringSplitOptions options) { var sepListBuilder = new ValueListBuilder<int>(stackalloc int[StackallocIntBufferSizeLimit]); MakeSeparatorList(this, separator, ref sepListBuilder); ReadOnlySpan<int> sepList = sepListBuilder.AsSpan(); if (sepList.Length == 0) { // there are no separators so sepListBuilder did not rent an array from pool and there is no need to dispose it return CreateSplitArrayOfThisAsSoleValue(options, count); } string[] result = (options != StringSplitOptions.None) ? SplitWithPostProcessing(sepList, default, separator.Length, count, options) : SplitWithoutPostProcessing(sepList, default, separator.Length, count); sepListBuilder.Dispose(); return result; } // This function will not trim entries or special-case empty entries private string[] SplitWithoutPostProcessing(ReadOnlySpan<int> sepList, ReadOnlySpan<int> lengthList, int defaultLength, int count) { Debug.Assert(count >= 2); int currIndex = 0; int arrIndex = 0; count--; int numActualReplaces = (sepList.Length < count) ? sepList.Length : count; // Allocate space for the new array. // +1 for the string from the end of the last replace to the end of the string. string[] splitStrings = new string[numActualReplaces + 1]; for (int i = 0; i < numActualReplaces && currIndex < Length; i++) { splitStrings[arrIndex++] = Substring(currIndex, sepList[i] - currIndex); currIndex = sepList[i] + (lengthList.IsEmpty ? defaultLength : lengthList[i]); } // Handle the last string at the end of the array if there is one. if (currIndex < Length && numActualReplaces >= 0) { splitStrings[arrIndex] = Substring(currIndex); } else if (arrIndex == numActualReplaces) { // We had a separator character at the end of a string. Rather than just allowing // a null character, we'll replace the last element in the array with an empty string. splitStrings[arrIndex] = Empty; } return splitStrings; } // This function may trim entries or omit empty entries private string[] SplitWithPostProcessing(ReadOnlySpan<int> sepList, ReadOnlySpan<int> lengthList, int defaultLength, int count, StringSplitOptions options) { Debug.Assert(count >= 2); int numReplaces = sepList.Length; // Allocate array to hold items. This array may not be // filled completely in this function, we will create a // new array and copy string references to that new array. int maxItems = (numReplaces < count) ? (numReplaces + 1) : count; string[] splitStrings = new string[maxItems]; int currIndex = 0; int arrIndex = 0; ReadOnlySpan<char> thisEntry; for (int i = 0; i < numReplaces; i++) { thisEntry = this.AsSpan(currIndex, sepList[i] - currIndex); if ((options & StringSplitOptions.TrimEntries) != 0) { thisEntry = thisEntry.Trim(); } if (!thisEntry.IsEmpty || ((options & StringSplitOptions.RemoveEmptyEntries) == 0)) { splitStrings[arrIndex++] = thisEntry.ToString(); } currIndex = sepList[i] + (lengthList.IsEmpty ? defaultLength : lengthList[i]); if (arrIndex == count - 1) { // The next iteration of the loop will provide the final entry into the // results array. If needed, skip over all empty entries before that // point. if ((options & StringSplitOptions.RemoveEmptyEntries) != 0) { while (++i < numReplaces) { thisEntry = this.AsSpan(currIndex, sepList[i] - currIndex); if ((options & StringSplitOptions.TrimEntries) != 0) { thisEntry = thisEntry.Trim(); } if (!thisEntry.IsEmpty) { break; // there's useful data here } currIndex = sepList[i] + (lengthList.IsEmpty ? defaultLength : lengthList[i]); } } break; } } // we must have at least one slot left to fill in the last string. Debug.Assert(arrIndex < maxItems); // Handle the last substring at the end of the array // (could be empty if separator appeared at the end of the input string) thisEntry = this.AsSpan(currIndex); if ((options & StringSplitOptions.TrimEntries) != 0) { thisEntry = thisEntry.Trim(); } if (!thisEntry.IsEmpty || ((options & StringSplitOptions.RemoveEmptyEntries) == 0)) { splitStrings[arrIndex++] = thisEntry.ToString(); } Array.Resize(ref splitStrings, arrIndex); return splitStrings; } /// <summary> /// Uses ValueListBuilder to create list that holds indexes of separators in string. /// </summary> /// <param name="source">The source to parse.</param> /// <param name="separators"><see cref="ReadOnlySpan{T}"/> of separator chars</param> /// <param name="sepListBuilder"><see cref="ValueListBuilder{T}"/> to store indexes</param> internal static void MakeSeparatorListAny(ReadOnlySpan<char> source, ReadOnlySpan<char> separators, ref ValueListBuilder<int> sepListBuilder) { // Special-case no separators to mean any whitespace is a separator. if (separators.Length == 0) { for (int i = 0; i < source.Length; i++) { if (char.IsWhiteSpace(source[i])) { sepListBuilder.Append(i); } } } // Special-case the common cases of 1, 2, and 3 separators, with manual comparisons against each separator. else if (separators.Length <= 3) { char sep0, sep1, sep2; sep0 = separators[0]; sep1 = separators.Length > 1 ? separators[1] : sep0; sep2 = separators.Length > 2 ? separators[2] : sep1; if (Vector128.IsHardwareAccelerated && source.Length >= Vector128<ushort>.Count * 2) { MakeSeparatorListVectorized(source, ref sepListBuilder, sep0, sep1, sep2); return; } for (int i = 0; i < source.Length; i++) { char c = source[i]; if (c == sep0 || c == sep1 || c == sep2) { sepListBuilder.Append(i); } } } // Handle > 3 separators with a probabilistic map, ala IndexOfAny. // This optimizes for chars being unlikely to match a separator. else { var map = new ProbabilisticMap(separators); ref uint charMap = ref Unsafe.As<ProbabilisticMap, uint>(ref map); for (int i = 0; i < source.Length; i++) { if (ProbabilisticMap.Contains(ref charMap, separators, source[i])) { sepListBuilder.Append(i); } } } } private static void MakeSeparatorListVectorized(ReadOnlySpan<char> sourceSpan, ref ValueListBuilder<int> sepListBuilder, char c, char c2, char c3) { // Redundant test so we won't prejit remainder of this method // on platforms where it is not supported if (!Vector128.IsHardwareAccelerated) { throw new PlatformNotSupportedException(); } Debug.Assert(sourceSpan.Length >= Vector128<ushort>.Count*2); int baseIndex = 0; ReadOnlySpan<ushort> sourceSpanUInt16 = MemoryMarshal.Cast<char, ushort>(sourceSpan); ReadOnlySpan<ushort> remaining = sourceSpanUInt16; if (Vector512.IsHardwareAccelerated && (uint)remaining.Length >= (uint)Vector512<ushort>.Count*2) { Vector512<ushort> v1 = Vector512.Create((ushort)c); Vector512<ushort> v2 = Vector512.Create((ushort)c2); Vector512<ushort> v3 = Vector512.Create((ushort)c3); if (Avx512BW.IsSupported && PackedSpanHelpers.CanUsePackedIndexOf(c) && PackedSpanHelpers.CanUsePackedIndexOf(c2) && PackedSpanHelpers.CanUsePackedIndexOf(c3)) { // Process in double chunks & check if either chunk is likely to contain matches at once. // If we get multiple matches in a single chunk, we assume they're likely to be close & // break out of this logic & use the more optimistic loop below. // This is similar logic to SpanHelpers.Packed.cs's IndexOf. Vector512<byte> packedComparand1 = Vector512.Create((byte)c); Vector512<byte> packedComparand2 = Vector512.Create((byte)c2); Vector512<byte> packedComparand3 = Vector512.Create((byte)c3); while ((uint)remaining.Length >= (uint)Vector512<ushort>.Count*2) { Vector512<ushort> vector1 = Vector512.Create(remaining); Vector512<ushort> vector2 = Vector512.Create(remaining.Slice(Vector512<ushort>.Count)); var packed = PackedSpanHelpers.PackSources(vector1.AsInt16(), vector2.AsInt16()); if ((Vector512.Equals(packed, packedComparand1) | Vector512.Equals(packed, packedComparand2) | Vector512.Equals(packed, packedComparand3)) != Vector512<byte>.Zero) { var cmp1 = Vector512.Equals(vector1, v1).AsByte() | Vector512.Equals(vector1, v2).AsByte() | Vector512.Equals(vector1, v3).AsByte(); var cmp2 = Vector512.Equals(vector2, v1).AsByte() | Vector512.Equals(vector2, v2).AsByte() | Vector512.Equals(vector2, v3).AsByte(); // Same logic as below, but for both vectors. ulong mask1 = cmp1.ExtractMostSignificantBits() & 0x5555555555555555; ulong mask2 = cmp2.ExtractMostSignificantBits() & 0x5555555555555555; bool shouldBreak = ulong.PopCount(mask1) + ulong.PopCount(mask2) > 1; while (mask1 != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask1) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos); mask1 = BitOperations.ResetLowestSetBit(mask1); } while (mask2 != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask2) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos + Vector512<ushort>.Count); mask2 = BitOperations.ResetLowestSetBit(mask2); } // Break out of the loop if we had >1 match: if (shouldBreak) { baseIndex += Vector512<ushort>.Count*2; remaining = remaining.Slice(Vector512<ushort>.Count*2); break; } } baseIndex += Vector512<ushort>.Count*2; remaining = remaining.Slice(Vector512<ushort>.Count*2); } } while ((uint)remaining.Length >= (uint)Vector512<ushort>.Count) { Vector512<ushort> vector = Vector512.Create(remaining); Vector512<byte> cmp = Vector512.Equals(vector, v1).AsByte() | Vector512.Equals(vector, v2).AsByte() | Vector512.Equals(vector, v3).AsByte(); if (cmp != Vector512<byte>.Zero) { // Skip every other bit ulong mask = cmp.ExtractMostSignificantBits() & 0x5555555555555555; do { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos); mask = BitOperations.ResetLowestSetBit(mask); } while (mask != 0); } baseIndex += Vector512<ushort>.Count; remaining = remaining.Slice(Vector512<ushort>.Count); } // Handle the last chunk in a vectorized way also. // We do a whole vector's worth again, but just mask out the bits we've already handled. if (remaining.Length > 0) { Vector512<ushort> vector = Vector512.Create(sourceSpanUInt16.Slice(sourceSpanUInt16.Length - Vector512<ushort>.Count)); Vector512<byte> cmp = Vector512.Equals(vector, v1).AsByte() | Vector512.Equals(vector, v2).AsByte() | Vector512.Equals(vector, v3).AsByte(); int finalIndex = sourceSpanUInt16.Length - Vector512<ushort>.Count; ulong mask = cmp.ExtractMostSignificantBits() & 0x5555555555555555 & ~((1UL << (Vector512<byte>.Count - remaining.Length * sizeof(char))) - 1); while (mask != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask) / sizeof(char); sepListBuilder.Append(finalIndex + (int)bitPos); mask = BitOperations.ResetLowestSetBit(mask); } } return; } else if (Vector256.IsHardwareAccelerated && (uint)remaining.Length >= (uint)Vector256<ushort>.Count*2) { Vector256<ushort> v1 = Vector256.Create((ushort)c); Vector256<ushort> v2 = Vector256.Create((ushort)c2); Vector256<ushort> v3 = Vector256.Create((ushort)c3); if (Avx2.IsSupported && PackedSpanHelpers.CanUsePackedIndexOf(c) && PackedSpanHelpers.CanUsePackedIndexOf(c2) && PackedSpanHelpers.CanUsePackedIndexOf(c3)) { // Process in double chunks & check if either chunk is likely to contain matches at once. // If we get multiple matches in a single chunk, we assume they're likely to be close & // break out of this logic & use the more optimistic loop below. // This is similar logic to SpanHelpers.Packed.cs's IndexOf. Vector256<byte> packedComparand1 = Vector256.Create((byte)c); Vector256<byte> packedComparand2 = Vector256.Create((byte)c2); Vector256<byte> packedComparand3 = Vector256.Create((byte)c3); while ((uint)remaining.Length >= (uint)Vector256<ushort>.Count*2) { Vector256<ushort> vector1 = Vector256.Create(remaining); Vector256<ushort> vector2 = Vector256.Create(remaining.Slice(Vector256<ushort>.Count)); var packed = PackedSpanHelpers.PackSources(vector1.AsInt16(), vector2.AsInt16()); if ((Vector256.Equals(packed, packedComparand1) | Vector256.Equals(packed, packedComparand2) | Vector256.Equals(packed, packedComparand3)) != Vector256<byte>.Zero) { var cmp1 = Vector256.Equals(vector1, v1).AsByte() | Vector256.Equals(vector1, v2).AsByte() | Vector256.Equals(vector1, v3).AsByte(); var cmp2 = Vector256.Equals(vector2, v1).AsByte() | Vector256.Equals(vector2, v2).AsByte() | Vector256.Equals(vector2, v3).AsByte(); // Same logic as below, but for both vectors. uint mask1 = cmp1.ExtractMostSignificantBits() & 0x55555555; uint mask2 = cmp2.ExtractMostSignificantBits() & 0x55555555; bool shouldBreak = uint.PopCount(mask1) + uint.PopCount(mask2) > 1; while (mask1 != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask1) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos); mask1 = BitOperations.ResetLowestSetBit(mask1); } while (mask2 != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask2) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos + Vector256<ushort>.Count); mask2 = BitOperations.ResetLowestSetBit(mask2); } // Break out of the loop if we had > 1 match: if (shouldBreak) { baseIndex += Vector256<ushort>.Count*2; remaining = remaining.Slice(Vector256<ushort>.Count*2); break; } } baseIndex += Vector256<ushort>.Count*2; remaining = remaining.Slice(Vector256<ushort>.Count*2); } } while ((uint)remaining.Length >= (uint)Vector256<ushort>.Count) { Vector256<ushort> vector = Vector256.Create(remaining); Vector256<byte> cmp = Vector256.Equals(vector, v1).AsByte() | Vector256.Equals(vector, v2).AsByte() | Vector256.Equals(vector, v3).AsByte(); if (cmp != Vector256<byte>.Zero) { // Skip every other bit uint mask = cmp.ExtractMostSignificantBits() & 0x55555555; do { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos); mask = BitOperations.ResetLowestSetBit(mask); } while (mask != 0); } baseIndex += Vector256<ushort>.Count; remaining = remaining.Slice(Vector256<ushort>.Count); } // Handle the last chunk in a vectorized way also. // We do a whole vector's worth again, but just mask out the bits we've already handled. if (remaining.Length > 0) { Vector256<ushort> vector = Vector256.Create(sourceSpanUInt16.Slice(sourceSpanUInt16.Length - Vector256<ushort>.Count)); Vector256<byte> cmp = Vector256.Equals(vector, v1).AsByte() | Vector256.Equals(vector, v2).AsByte() | Vector256.Equals(vector, v3).AsByte(); int finalIndex = sourceSpanUInt16.Length - Vector256<ushort>.Count; uint mask = cmp.ExtractMostSignificantBits() & 0x55555555 & ~((1u << (Vector256<byte>.Count - remaining.Length * sizeof(char))) - 1); while (mask != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask) / sizeof(char); sepListBuilder.Append(finalIndex + (int)bitPos); mask = BitOperations.ResetLowestSetBit(mask); } } return; } else if (Vector128.IsHardwareAccelerated) { Vector128<ushort> v1 = Vector128.Create((ushort)c); Vector128<ushort> v2 = Vector128.Create((ushort)c2); Vector128<ushort> v3 = Vector128.Create((ushort)c3); if (Sse2.IsSupported && PackedSpanHelpers.CanUsePackedIndexOf(c) && PackedSpanHelpers.CanUsePackedIndexOf(c2) && PackedSpanHelpers.CanUsePackedIndexOf(c3)) { // Process in double chunks & check if either chunk is likely to contain matches at once. // If we get multiple matches in a single chunk, we assume they're likely to be close & // break out of this logic & use the more optimistic loop below. // This is similar logic to SpanHelpers.Packed.cs's IndexOf. Vector128<byte> packedComparand1 = Vector128.Create((byte)c); Vector128<byte> packedComparand2 = Vector128.Create((byte)c2); Vector128<byte> packedComparand3 = Vector128.Create((byte)c3); while ((uint)remaining.Length >= (uint)Vector128<ushort>.Count*2) { Vector128<ushort> vector1 = Vector128.Create(remaining); Vector128<ushort> vector2 = Vector128.Create(remaining.Slice(Vector128<ushort>.Count)); var packed = PackedSpanHelpers.PackSources(vector1.AsInt16(), vector2.AsInt16()); if ((Vector128.Equals(packed, packedComparand1) | Vector128.Equals(packed, packedComparand2) | Vector128.Equals(packed, packedComparand3)) != Vector128<byte>.Zero) { var cmp1 = Vector128.Equals(vector1, v1).AsByte() | Vector128.Equals(vector1, v2).AsByte() | Vector128.Equals(vector1, v3).AsByte(); var cmp2 = Vector128.Equals(vector2, v1).AsByte() | Vector128.Equals(vector2, v2).AsByte() | Vector128.Equals(vector2, v3).AsByte(); // Same logic as below, but for both vectors. uint mask1 = cmp1.ExtractMostSignificantBits() & 0x5555; uint mask2 = cmp2.ExtractMostSignificantBits() & 0x5555; bool shouldBreak = uint.PopCount(mask1) + uint.PopCount(mask2) > 1; while (mask1 != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask1) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos); mask1 = BitOperations.ResetLowestSetBit(mask1); } while (mask2 != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask2) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos + Vector128<ushort>.Count); mask2 = BitOperations.ResetLowestSetBit(mask2); } // Break out of the loop if we had > 1 match: if (shouldBreak) { baseIndex += Vector128<ushort>.Count*2; remaining = remaining.Slice(Vector128<ushort>.Count*2); break; } } baseIndex += Vector128<ushort>.Count*2; remaining = remaining.Slice(Vector128<ushort>.Count*2); } } while ((uint)remaining.Length >= (uint)Vector128<ushort>.Count) { Vector128<ushort> vector = Vector128.Create(remaining); Vector128<byte> cmp = Vector128.Equals(vector, v1).AsByte() | Vector128.Equals(vector, v2).AsByte() | Vector128.Equals(vector, v3).AsByte(); if (cmp != Vector128<byte>.Zero) { // Skip every other bit uint mask = cmp.ExtractMostSignificantBits() & 0x5555; do { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask) / sizeof(char); sepListBuilder.Append(baseIndex + (int)bitPos); mask = BitOperations.ResetLowestSetBit(mask); } while (mask != 0); } baseIndex += Vector128<ushort>.Count; remaining = remaining.Slice(Vector128<ushort>.Count); } // Handle the last chunk in a vectorized way also. // We do a whole vector's worth again, but just mask out the bits we've already handled. if (remaining.Length > 0) { Vector128<ushort> vector = Vector128.Create(sourceSpanUInt16.Slice(sourceSpanUInt16.Length - Vector128<ushort>.Count)); Vector128<byte> cmp = Vector128.Equals(vector, v1).AsByte() | Vector128.Equals(vector, v2).AsByte() | Vector128.Equals(vector, v3).AsByte(); int finalIndex = sourceSpanUInt16.Length - Vector128<ushort>.Count; uint mask = cmp.ExtractMostSignificantBits() & 0x5555 & ~((1u << (Vector128<byte>.Count - remaining.Length * sizeof(char))) - 1); while (mask != 0) { uint bitPos = (uint)BitOperations.TrailingZeroCount(mask) / sizeof(char); sepListBuilder.Append(finalIndex + (int)bitPos); mask = BitOperations.ResetLowestSetBit(mask); } } return; } Debug.Fail("We should not be able to reach this point of MakeSeparatorListVectorized."); } /// <summary> /// Uses ValueListBuilder to create list that holds indexes of separators in string. /// </summary> /// <param name="source">The source to parse.</param> /// <param name="separator">separator string</param> /// <param name="sepListBuilder"><see cref="ValueListBuilder{T}"/> to store indexes</param> internal static void MakeSeparatorList(ReadOnlySpan<char> source, ReadOnlySpan<char> separator, ref ValueListBuilder<int> sepListBuilder) { Debug.Assert(!separator.IsEmpty, "Empty separator"); int i = 0; while (!source.IsEmpty) { int index = source.IndexOf(separator); if (index < 0) { break; } i += index; sepListBuilder.Append(i); i += separator.Length; source = source.Slice(index + separator.Length); } } /// <summary> /// Uses ValueListBuilder to create list that holds indexes of separators in string and list that holds length of separator strings. /// </summary> /// <param name="source">The source to parse.</param> /// <param name="separators">separator strngs</param> /// <param name="sepListBuilder"><see cref="ValueListBuilder{T}"/> for separator indexes</param> /// <param name="lengthListBuilder"><see cref="ValueListBuilder{T}"/> for separator length values</param> internal static void MakeSeparatorListAny(ReadOnlySpan<char> source, ReadOnlySpan<string?> separators, ref ValueListBuilder<int> sepListBuilder, ref ValueListBuilder<int> lengthListBuilder) { Debug.Assert(!separators.IsEmpty, "Zero separators"); for (int i = 0; i < source.Length; i++) { for (int j = 0; j < separators.Length; j++) { string? separator = separators[j]; if (IsNullOrEmpty(separator)) { continue; } int currentSepLength = separator.Length; if (source[i] == separator[0] && currentSepLength <= source.Length - i) { if (currentSepLength == 1 || source.Slice(i, currentSepLength).SequenceEqual(separator)) { sepListBuilder.Append(i); lengthListBuilder.Append(currentSepLength); i += currentSepLength - 1; break; } } } } } internal static void CheckStringSplitOptions(StringSplitOptions options) { const StringSplitOptions AllValidFlags = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries; if ((options & ~AllValidFlags) != 0) { // at least one invalid flag was set ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options); } } // Returns a substring of this string. // public string Substring(int startIndex) { if (startIndex == 0) { return this; } int length = Length - startIndex; if (length == 0) { return Empty; } if ((uint)startIndex > (uint)Length) { ThrowSubstringArgumentOutOfRange(startIndex, length, Length); } return InternalSubString(startIndex, length); } public string Substring(int startIndex, int length) { #if TARGET_64BIT // See comment in Span<T>.Slice for how this works. if ((ulong)(uint)startIndex + (ulong)(uint)length > (ulong)(uint)Length) #else if ((uint)startIndex > (uint)Length || (uint)length > (uint)(Length - startIndex)) #endif { ThrowSubstringArgumentOutOfRange(startIndex, length, Length); } if (length == 0) { return Empty; } if (length == Length) { Debug.Assert(startIndex == 0); return this; } return InternalSubString(startIndex, length); } [DoesNotReturn] private static void ThrowSubstringArgumentOutOfRange(int startIndex, int length, int thisLength) { ArgumentOutOfRangeException.ThrowIfNegative(startIndex); if (startIndex > thisLength) { throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndexLargerThanLength); } ArgumentOutOfRangeException.ThrowIfNegative(length); throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_IndexLength); } private string InternalSubString(int startIndex, int length) { Debug.Assert(startIndex >= 0 && startIndex <= this.Length, "StartIndex is out of range!"); Debug.Assert(length >= 0 && startIndex <= this.Length - length, "length is out of range!"); string result = FastAllocateString(length); Buffer.Memmove( elementCount: (uint)length, destination: ref result._firstChar, source: ref Unsafe.Add(ref _firstChar, (nint)(uint)startIndex /* force zero-extension */)); return result; } // Creates a copy of this string in lower case. The culture is set by culture. public string ToLower() => ToLower(null); // Creates a copy of this string in lower case. The culture is set by culture. public string ToLower(CultureInfo? culture) { CultureInfo cult = culture ?? CultureInfo.CurrentCulture; return cult.TextInfo.ToLower(this); } // Creates a copy of this string in lower case based on invariant culture. public string ToLowerInvariant() { return TextInfo.ToLowerInvariant(this); } public string ToUpper() => ToUpper(null); // Creates a copy of this string in upper case. The culture is set by culture. public string ToUpper(CultureInfo? culture) { CultureInfo cult = culture ?? CultureInfo.CurrentCulture; return cult.TextInfo.ToUpper(this); } // Creates a copy of this string in upper case based on invariant culture. public string ToUpperInvariant() { return TextInfo.ToUpperInvariant(this); } /// <summary> /// Returns a copy of this string converted to uppercase using the casing rules used by /// <see cref="StringComparison.OrdinalIgnoreCase"/> comparisons. /// </summary> /// <returns>The uppercase equivalent of the current string.</returns> /// <remarks> /// The conversion uses a simple, one-to-one mapping that preserves the length of the string. /// Two strings are equal under <see cref="StringComparison.OrdinalIgnoreCase"/> if and only if /// their <see cref="ToUpperOrdinal"/> results are ordinally equal. /// </remarks> public string ToUpperOrdinal() { if (Length == 0) { return Empty; } return ChangeCaseOrdinal(toUpper: true); } /// <summary> /// Returns a copy of this string converted to lowercase using ordinal (simple, one-to-one) casing rules. /// </summary> /// <returns>The lowercase equivalent of the current string.</returns> /// <remarks> /// The conversion uses a simple, one-to-one mapping that preserves the length of the string. /// </remarks> public string ToLowerOrdinal() { if (Length == 0) { return Empty; } return ChangeCaseOrdinal(toUpper: false); } private string ChangeCaseOrdinal(bool toUpper) { Debug.Assert(Length > 0); // Fast path: scan the leading run of ASCII characters that is already in the requested case. // If the entire string is ASCII and needs no change, return the same instance to avoid an // allocation. This mirrors the behavior of TextInfo.ChangeCaseCommon used by ToUpper(Invariant)/ToLower. int consumed = toUpper ? this.AsSpan().IndexOfAnyExcept(SearchValuesStorage.AsciiExceptLowercase) : this.AsSpan().IndexOfAnyExcept(SearchValuesStorage.AsciiExceptUppercase); if (consumed < 0) { // The whole string is ASCII and already in the requested case. return this; } // The leading [0, consumed) chars are ASCII already in the requested case, so copy // them verbatim and run the ordinal casing over the remainder. string result = FastAllocateString(Length); Span<char> resultSpan = new Span<char>(ref result.GetRawStringData(), Length); if (consumed > 0) { this.AsSpan(0, consumed).CopyTo(resultSpan); } if (toUpper) { Globalization.Ordinal.ToUpperOrdinal(this.AsSpan(consumed), resultSpan.Slice(consumed)); } else { Globalization.Ordinal.ToLowerOrdinal(this.AsSpan(consumed), resultSpan.Slice(consumed)); } return result; } // Trims the whitespace from both ends of the string. Whitespace is defined by // char.IsWhiteSpace. // public string Trim() { if (Length == 0 || (!char.IsWhiteSpace(_firstChar) && !char.IsWhiteSpace(this[^1]))) { return this; } return TrimWhiteSpaceHelper(TrimType.Both); } // Removes a set of characters from the beginning and end of this string. public unsafe string Trim(char trimChar) { if (Length == 0 || (_firstChar != trimChar && this[^1] != trimChar)) { return this; } return TrimHelper(&trimChar, 1, TrimType.Both); } /// <summary> /// Removes all leading and trailing instances of a rune from the current string. /// </summary> /// <param name="trimRune">A Unicode rune to remove.</param> /// <returns> /// The string that remains after all instances of the <paramref name="trimRune"/> rune are removed from the start and end of the /// current string. If no runes can be trimmed from the current instance, the method returns the current instance unchanged. /// </returns> public unsafe string Trim(Rune trimRune) { if (trimRune.IsBmp) { return Trim((char)trimRune.Value); } UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)trimRune.Value, out char highSurrogate, out char lowSurrogate); // Trim start int index = 0; while ((uint)(index + 1) < (uint)Length && this[index] == highSurrogate && this[index + 1] == lowSurrogate) { index += 2; } // Trim end int endIndex = Length - 2; while (endIndex > index && this[endIndex] == highSurrogate && this[endIndex + 1] == lowSurrogate) { endIndex -= 2; } return this[index..(endIndex + 2)]; } // Removes a set of characters from the beginning and end of this string. public unsafe string Trim(params char[]? trimChars) { if (trimChars == null || trimChars.Length == 0) { return TrimWhiteSpaceHelper(TrimType.Both); } fixed (char* pTrimChars = &trimChars[0]) { return TrimHelper(pTrimChars, trimChars.Length, TrimType.Both); } } /// <summary> /// Removes all leading and trailing occurrences of a set of characters specified in a span from the current string. /// </summary> /// <param name="trimChars">A span of Unicode characters to remove.</param> /// <returns> /// The string that remains after all occurrences of the characters in the <paramref name="trimChars"/> parameter are removed from the start and end of the current string. /// If <paramref name="trimChars"/> is empty, white-space characters are removed instead. /// If no characters can be trimmed from the current instance, the method returns the current instance unchanged. /// </returns> public unsafe string Trim(params ReadOnlySpan<char> trimChars) { if (trimChars.IsEmpty) { return TrimWhiteSpaceHelper(TrimType.Both); } fixed (char* pTrimChars = &MemoryMarshal.GetReference(trimChars)) { return TrimHelper(pTrimChars, trimChars.Length, TrimType.Both); } } // Removes a set of characters from the beginning of this string. public string TrimStart() => TrimWhiteSpaceHelper(TrimType.Head); // Removes a set of characters from the beginning of this string. public unsafe string TrimStart(char trimChar) => TrimHelper(&trimChar, 1, TrimType.Head); /// <summary> /// Removes all leading instances of a rune from the current string. /// </summary> /// <param name="trimRune">A Unicode rune to remove.</param> /// <returns> /// The string that remains after all instances of the <paramref name="trimRune"/> rune are removed from the start of the /// current string. If no runes can be trimmed from the current instance, the method returns the current instance unchanged. /// </returns> public unsafe string TrimStart(Rune trimRune) { if (trimRune.IsBmp) { return TrimStart((char)trimRune.Value); } UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)trimRune.Value, out char highSurrogate, out char lowSurrogate); int index = 0; while ((uint)(index + 1) < (uint)Length && this[index] == highSurrogate && this[index + 1] == lowSurrogate) { index += 2; } return this[index..]; } // Removes a set of characters from the beginning of this string. public unsafe string TrimStart(params char[]? trimChars) { if (trimChars == null || trimChars.Length == 0) { return TrimWhiteSpaceHelper(TrimType.Head); } fixed (char* pTrimChars = &trimChars[0]) { return TrimHelper(pTrimChars, trimChars.Length, TrimType.Head); } } /// <summary> /// Removes all the leading occurrences of a set of characters specified in a span from the current string. /// </summary> /// <param name="trimChars">A span of Unicode characters to remove.</param> /// <returns> /// The string that remains after all occurrences of characters in the <paramref name="trimChars"/> parameter are removed from the start of the current string. /// If <paramref name="trimChars"/> is empty, white-space characters are removed instead. /// If no characters can be trimmed from the current instance, the method returns the current instance unchanged. /// </returns> public unsafe string TrimStart(params ReadOnlySpan<char> trimChars) { if (trimChars.IsEmpty) { return TrimWhiteSpaceHelper(TrimType.Head); } fixed (char* pTrimChars = &MemoryMarshal.GetReference(trimChars)) { return TrimHelper(pTrimChars, trimChars.Length, TrimType.Head); } } // Removes a set of characters from the end of this string. public string TrimEnd() => TrimWhiteSpaceHelper(TrimType.Tail); // Removes a set of characters from the end of this string. public unsafe string TrimEnd(char trimChar) => TrimHelper(&trimChar, 1, TrimType.Tail); /// <summary> /// Removes all trailing instances of a rune from the current string. /// </summary> /// <param name="trimRune">A Unicode rune to remove.</param> /// <returns> /// The string that remains after all instances of the <paramref name="trimRune"/> rune are removed from the end of the /// current string. If no runes can be trimmed from the current instance, the method returns the current instance unchanged. /// </returns> public unsafe string TrimEnd(Rune trimRune) { if (trimRune.IsBmp) { return TrimEnd((char)trimRune.Value); } UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar((uint)trimRune.Value, out char highSurrogate, out char lowSurrogate); int endIndex = Length - 2; while ((uint)endIndex < (uint)Length && this[endIndex] == highSurrogate && this[endIndex + 1] == lowSurrogate) { endIndex -= 2; } return this[..(endIndex + 2)]; } // Removes a set of characters from the end of this string. public unsafe string TrimEnd(params char[]? trimChars) { if (trimChars == null || trimChars.Length == 0) { return TrimWhiteSpaceHelper(TrimType.Tail); } fixed (char* pTrimChars = &trimChars[0]) { return TrimHelper(pTrimChars, trimChars.Length, TrimType.Tail); } } /// <summary> /// Removes all the trailing occurrences of a set of characters specified in a span from the current string. /// </summary> /// <param name="trimChars">A span of Unicode characters to remove.</param> /// <returns> /// The string that remains after all occurrences of characters in the <paramref name="trimChars"/> parameter are removed from the end of the current string. /// If <paramref name="trimChars"/> is empty, white-space characters are removed instead. /// If no characters can be trimmed from the current instance, the method returns the current instance unchanged. /// </returns> public unsafe string TrimEnd(params ReadOnlySpan<char> trimChars) { if (trimChars.IsEmpty) { return TrimWhiteSpaceHelper(TrimType.Tail); } fixed (char* pTrimChars = &trimChars[0]) { return TrimHelper(pTrimChars, trimChars.Length, TrimType.Tail); } } private string TrimWhiteSpaceHelper(TrimType trimType) { // end will point to the first non-trimmed character on the right. // start will point to the first non-trimmed character on the left. int end = Length - 1; int start = 0; // Trim specified characters. if ((trimType & TrimType.Head) != 0) { for (start = 0; start < Length; start++) { if (!char.IsWhiteSpace(this[start])) { break; } } } if ((trimType & TrimType.Tail) != 0) { for (end = Length - 1; end >= start; end--) { if (!char.IsWhiteSpace(this[end])) { break; } } } return CreateTrimmedString(start, end); } private unsafe string TrimHelper(char* trimChars, int trimCharsLength, TrimType trimType) { Debug.Assert(trimChars != null); Debug.Assert(trimCharsLength > 0); // end will point to the first non-trimmed character on the right. // start will point to the first non-trimmed character on the left. int end = Length - 1; int start = 0; // Trim specified characters. if ((trimType & TrimType.Head) != 0) { for (start = 0; start < Length; start++) { int i = 0; char ch = this[start]; for (i = 0; i < trimCharsLength; i++) { if (trimChars[i] == ch) { break; } } if (i == trimCharsLength) { // The character is not in trimChars, so stop trimming. break; } } } if ((trimType & TrimType.Tail) != 0) { for (end = Length - 1; end >= start; end--) { int i = 0; char ch = this[end]; for (i = 0; i < trimCharsLength; i++) { if (trimChars[i] == ch) { break; } } if (i == trimCharsLength) { // The character is not in trimChars, so stop trimming. break; } } } return CreateTrimmedString(start, end); } private string CreateTrimmedString(int start, int end) { int len = end - start + 1; return len == Length ? this : len == 0 ? Empty : InternalSubString(start, len); } } }