using System; using System.Buffers; using System.IO; using System.Runtime.CompilerServices; namespace Cysharp.Text { public partial struct Utf16ValueStringBuilder : IDisposable, IBufferWriter, IResettableBufferWriter { public delegate bool TryFormat(T value, Span destination, out int charsWritten, ReadOnlySpan format); const int ThreadStaticBufferSize = 31111; const int DefaultBufferSize = 32768; // use 32K default buffer. static char newLine1; static char newLine2; static bool crlf; static Utf16ValueStringBuilder() { var newLine = Environment.NewLine.ToCharArray(); if (newLine.Length == 1) { // cr or lf newLine1 = newLine[0]; crlf = false; } else { // crlf(windows) newLine1 = newLine[0]; newLine2 = newLine[1]; crlf = true; } } [ThreadStatic] static char[]? scratchBuffer; [ThreadStatic] internal static bool scratchBufferUsed; char[]? buffer; int index; bool disposeImmediately; /// Length of written buffer. public int Length => index; /// Get the written buffer data. public ReadOnlySpan AsSpan() => buffer.AsSpan(0, index); /// Get the written buffer data. public ReadOnlyMemory AsMemory() => buffer.AsMemory(0, index); /// Get the written buffer data. public ArraySegment AsArraySegment() => new ArraySegment(buffer, 0, index); /// /// Initializes a new instance /// /// /// If true uses thread-static buffer that is faster but must return immediately. /// /// /// This exception is thrown when new StringBuilder(disposeImmediately: true) or ZString.CreateStringBuilder(notNested: true) is nested. /// See the README.md /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Utf16ValueStringBuilder(bool disposeImmediately) { if (disposeImmediately && scratchBufferUsed) { ThrowNestedException(); } char[]? buf; if (disposeImmediately) { buf = scratchBuffer; if (buf == null) { buf = scratchBuffer = new char[ThreadStaticBufferSize]; } scratchBufferUsed = true; } else { buf = ArrayPool.Shared.Rent(DefaultBufferSize); } buffer = buf; index = 0; this.disposeImmediately = disposeImmediately; } /// /// Return the inner buffer to pool. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { if (buffer != null) { if (buffer.Length != ThreadStaticBufferSize) { ArrayPool.Shared.Return(buffer); } buffer = null; index = 0; if (disposeImmediately) { scratchBufferUsed = false; } } } public void Clear() { index = 0; } public void TryGrow(int sizeHint) { if (buffer!.Length < index + sizeHint) { Grow(sizeHint); } } public void Grow(int sizeHint) { var nextSize = buffer!.Length * 2; if (sizeHint != 0) { nextSize = Math.Max(nextSize, index + sizeHint); } var newBuffer = ArrayPool.Shared.Rent(nextSize); buffer.CopyTo(newBuffer, 0); if (buffer.Length != ThreadStaticBufferSize) { ArrayPool.Shared.Return(buffer); } buffer = newBuffer; } /// Appends the default line terminator to the end of this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendLine() { if (crlf) { if (buffer!.Length - index < 2) Grow(2); buffer[index] = newLine1; buffer[index + 1] = newLine2; index += 2; } else { if (buffer!.Length - index < 1) Grow(1); buffer[index] = newLine1; index += 1; } } /// Appends the string representation of a specified value to this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char value) { if (buffer!.Length - index < 1) { Grow(1); } buffer[index++] = value; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char value, int repeatCount) { if (repeatCount < 0) { throw new ArgumentOutOfRangeException(nameof(repeatCount)); } GetSpan(repeatCount).Fill(value); Advance(repeatCount); } /// Appends the string representation of a specified value followed by the default line terminator to the end of this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendLine(char value) { Append(value); AppendLine(); } /// Appends the string representation of a specified value to this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(string value) { Append(value.AsSpan()); } /// Appends the string representation of a specified value followed by the default line terminator to the end of this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendLine(string value) { Append(value); AppendLine(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(string value, int startIndex, int count) { if (value == null) { if (startIndex == 0 && count == 0) { return; } else { throw new ArgumentNullException(nameof(value)); } } Append(value.AsSpan(startIndex, count)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char[] value, int startIndex, int charCount) { if (buffer!.Length - index < charCount) { Grow(charCount); } Array.Copy(value, startIndex, buffer, index, charCount); index += charCount; } /// Appends a contiguous region of arbitrary memory to this instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(ReadOnlySpan value) { if (buffer!.Length - index < value.Length) { Grow(value.Length); } value.CopyTo(buffer.AsSpan(index)); index += value.Length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendLine(ReadOnlySpan value) { Append(value); AppendLine(); } /// Appends the string representation of a specified value to this instance. public void Append(T value) { if (!FormatterCache.TryFormatDelegate(value, buffer.AsSpan(index), out var written, default)) { Grow(written); if (!FormatterCache.TryFormatDelegate(value, buffer.AsSpan(index), out written, default)) { ThrowArgumentException(nameof(value)); } } index += written; } /// Appends the string representation of a specified value followed by the default line terminator to the end of this instance. public void AppendLine(T value) { Append(value); AppendLine(); } static class ExceptionUtil { public static void ThrowArgumentOutOfRangeException(string paramName) { throw new ArgumentOutOfRangeException(paramName); } } /// /// Inserts a string 0 or more times into this builder at the specified position. /// /// The index to insert in this builder. /// The string to insert. /// The number of times to insert the string. public void Insert(int index, string value, int count) { Insert(index, value.AsSpan(), count); } public void Insert(int index, string value) { Insert(index, value.AsSpan(), 1); } public void Insert(int index, ReadOnlySpan value, int count) { if (count < 0) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(count)); } int currentLength = Length; if ((uint)index > (uint)currentLength) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(index)); } if (value.Length == 0 || count == 0) { return; } var newSize = index + value.Length * count; var newBuffer = ArrayPool.Shared.Rent(Math.Max(DefaultBufferSize, newSize)); buffer.AsSpan(0, index).CopyTo(newBuffer); int newBufferIndex = index; for (int i = 0; i < count; i++) { value.CopyTo(newBuffer.AsSpan(newBufferIndex)); newBufferIndex += value.Length; } int remainLnegth = this.index - index; buffer.AsSpan(index, remainLnegth).CopyTo(newBuffer.AsSpan(newBufferIndex)); if (buffer!.Length != ThreadStaticBufferSize) { if (buffer != null) { ArrayPool.Shared.Return(buffer); } } buffer = newBuffer; this.index = newBufferIndex + remainLnegth; } /// /// Replaces all instances of one character with another in this builder. /// /// The character to replace. /// The character to replace with. public void Replace(char oldChar, char newChar) => Replace(oldChar, newChar, 0, Length); /// /// Replaces all instances of one character with another in this builder. /// /// The character to replace. /// The character to replace with. /// The index to start in this builder. /// The number of characters to read in this builder. public void Replace(char oldChar, char newChar, int startIndex, int count) { int currentLength = Length; if ((uint)startIndex > (uint)currentLength) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(startIndex)); } if (count < 0 || startIndex > currentLength - count) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(count)); } int endIndex = startIndex + count; for (int i = startIndex; i < endIndex; i++) { if (buffer![i] == oldChar) { buffer[i] = newChar; } } } /// /// Replaces all instances of one string with another in this builder. /// /// The string to replace. /// The string to replace with. /// /// If is null, instances of /// are removed from this builder. /// public void Replace(string oldValue, string newValue) => Replace(oldValue, newValue, 0, Length); public void Replace(ReadOnlySpan oldValue, ReadOnlySpan newValue) => Replace(oldValue, newValue, 0, Length); /// /// Replaces all instances of one string with another in part of this builder. /// /// The string to replace. /// The string to replace with. /// The index to start in this builder. /// The number of characters to read in this builder. /// /// If is null, instances of /// are removed from this builder. /// public void Replace(string oldValue, string newValue, int startIndex, int count) { if (oldValue == null) { throw new ArgumentNullException(nameof(oldValue)); } Replace(oldValue.AsSpan(), newValue.AsSpan(), startIndex, count); } public void Replace(ReadOnlySpan oldValue, ReadOnlySpan newValue, int startIndex, int count) { int currentLength = Length; if ((uint)startIndex > (uint)currentLength) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(startIndex)); } if (count < 0 || startIndex > currentLength - count) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(count)); } if (oldValue.Length == 0) { throw new ArgumentException("oldValue.Length is 0", nameof(oldValue)); } var readOnlySpan = AsSpan(); int endIndex = startIndex + count; int matchCount = 0; for (int i = startIndex; i < endIndex; i += oldValue.Length) { var span = readOnlySpan.Slice(i, endIndex - i); var pos = span.IndexOf(oldValue, StringComparison.Ordinal); if (pos == -1) { break; } i += pos; matchCount++; } if (matchCount == 0) return; var newBuffer = ArrayPool.Shared.Rent(Math.Max(DefaultBufferSize, Length + (newValue.Length - oldValue.Length) * matchCount)); buffer.AsSpan(0, startIndex).CopyTo(newBuffer); int newBufferIndex = startIndex; for (int i = startIndex; i < endIndex; i += oldValue.Length) { var span = readOnlySpan.Slice(i, endIndex - i); var pos = span.IndexOf(oldValue, StringComparison.Ordinal); if (pos == -1) { var remain = readOnlySpan.Slice(i); remain.CopyTo(newBuffer.AsSpan(newBufferIndex)); newBufferIndex += remain.Length; break; } readOnlySpan.Slice(i, pos).CopyTo(newBuffer.AsSpan(newBufferIndex)); newValue.CopyTo(newBuffer.AsSpan(newBufferIndex + pos)); newBufferIndex += pos + newValue.Length; i += pos; } if (buffer!.Length != ThreadStaticBufferSize) { ArrayPool.Shared.Return(buffer); } buffer = newBuffer; index = newBufferIndex; } /// /// Replaces the contents of a single position within the builder. /// /// The character to use at the position. /// The index to replace. public void ReplaceAt(char newChar, int replaceIndex) { int currentLength = Length; if ((uint)replaceIndex > (uint)currentLength) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(replaceIndex)); } buffer![replaceIndex] = newChar; } /// /// Removes a range of characters from this builder. /// /// /// This method does not reduce the capacity of this builder. /// public void Remove(int startIndex, int length) { if (length < 0) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(length)); } if (startIndex < 0) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(startIndex)); } if (length > Length - startIndex) { ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(length)); } if (Length == length && startIndex == 0) { index = 0; return; } if (length == 0) { return; } int remain = startIndex + length; buffer.AsSpan(remain, Length - remain).CopyTo(buffer.AsSpan(startIndex)); index -= length; } // Output /// Copy inner buffer to the destination span. public bool TryCopyTo(Span destination, out int charsWritten) { if (destination.Length < index) { charsWritten = 0; return false; } charsWritten = index; buffer.AsSpan(0, index).CopyTo(destination); return true; } /// Converts the value of this instance to a System.String. public override string ToString() { if (index == 0) return string.Empty; return new string(buffer, 0, index); } // IBufferWriter /// IBufferWriter.GetMemory. public Memory GetMemory(int sizeHint) { if ((buffer!.Length - index) < sizeHint) { Grow(sizeHint); } return buffer.AsMemory(index); } /// IBufferWriter.GetSpan. public Span GetSpan(int sizeHint) { if ((buffer!.Length - index) < sizeHint) { Grow(sizeHint); } return buffer.AsSpan(index); } /// IBufferWriter.Advance. public void Advance(int count) { index += count; } void IResettableBufferWriter.Reset() { index = 0; } void ThrowArgumentException(string paramName) { throw new ArgumentException("Can't format argument.", paramName); } static void ThrowFormatException() { throw new FormatException("Index (zero based) must be greater than or equal to zero and less than the size of the argument list."); } void AppendFormatInternal(T arg, int width, ReadOnlySpan format, string argName) { if (width <= 0) // leftJustify { width *= -1; if (!FormatterCache.TryFormatDelegate(arg, buffer.AsSpan(index), out var charsWritten, format)) { Grow(charsWritten); if (!FormatterCache.TryFormatDelegate(arg, buffer.AsSpan(index), out charsWritten, format)) { ThrowArgumentException(argName); } } index += charsWritten; int padding = width - charsWritten; if (width > 0 && padding > 0) { Append(' ', padding); // TODO Fill Method is too slow. } } else // rightJustify { if (typeof(T) == typeof(string)) { var s = Unsafe.As(arg); int padding = width - s.Length; if (padding > 0) { Append(' ', padding); // TODO Fill Method is too slow. } Append(s); } else { Span s = stackalloc char[typeof(T).IsValueType ? Unsafe.SizeOf() * 8 : 1024]; if (!FormatterCache.TryFormatDelegate(arg, s, out var charsWritten, format)) { s = stackalloc char[s.Length * 2]; if (!FormatterCache.TryFormatDelegate(arg, s, out charsWritten, format)) { ThrowArgumentException(argName); } } int padding = width - charsWritten; if (padding > 0) { Append(' ', padding); // TODO Fill Method is too slow. } Append(s.Slice(0, charsWritten)); } } } static void ThrowNestedException() { throw new NestedStringBuilderCreationException(nameof(Utf16ValueStringBuilder)); } /// /// Register custom formatter /// public static void RegisterTryFormat(TryFormat formatMethod) { FormatterCache.TryFormatDelegate = formatMethod; } static TryFormat CreateNullableFormatter() where T : struct { return new TryFormat((T? x, Span dest, out int written, ReadOnlySpan format) => { if (x == null) { written = 0; return true; } return FormatterCache.TryFormatDelegate(x.Value, dest, out written, format); }); } /// /// Supports the Nullable type for a given struct type. /// public static void EnableNullableFormat() where T : struct { RegisterTryFormat(CreateNullableFormatter()); } public static class FormatterCache { public static TryFormat TryFormatDelegate; static FormatterCache() { var formatter = (TryFormat?)CreateFormatter(typeof(T)); if (formatter == null) { if (typeof(T).IsEnum) { formatter = new TryFormat(EnumUtil.TryFormatUtf16); } else if (typeof(T) == typeof(string)) { formatter = new TryFormat(TryFormatString); } else { formatter = new TryFormat(TryFormatDefault); } } TryFormatDelegate = formatter; } static bool TryFormatString(T value, Span dest, out int written, ReadOnlySpan format) { var s = value as string; if (s == null) { written = 0; return true; } // also use this length when result is false. written = s.Length; return s.AsSpan().TryCopyTo(dest); } static bool TryFormatDefault(T value, Span dest, out int written, ReadOnlySpan format) { if (value == null) { written = 0; return true; } var s = (value is IFormattable formattable && format.Length != 0) ? formattable.ToString(format.ToString(), null) : value.ToString(); // also use this length when result is false. written = s.Length; return s.AsSpan().TryCopyTo(dest); } } } }