com.alicizax.unity.cysharp..../Utf16ValueStringBuilder.cs
陈思海 4fbea560b5 init
2025-01-09 13:57:51 +08:00

771 lines
25 KiB
C#

using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
namespace Cysharp.Text
{
public partial struct Utf16ValueStringBuilder : IDisposable, IBufferWriter<char>, IResettableBufferWriter<char>
{
public delegate bool TryFormat<T>(T value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> 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;
/// <summary>Length of written buffer.</summary>
public int Length => index;
/// <summary>Get the written buffer data.</summary>
public ReadOnlySpan<char> AsSpan() => buffer.AsSpan(0, index);
/// <summary>Get the written buffer data.</summary>
public ReadOnlyMemory<char> AsMemory() => buffer.AsMemory(0, index);
/// <summary>Get the written buffer data.</summary>
public ArraySegment<char> AsArraySegment() => new ArraySegment<char>(buffer, 0, index);
/// <summary>
/// Initializes a new instance
/// </summary>
/// <param name="disposeImmediately">
/// If true uses thread-static buffer that is faster but must return immediately.
/// </param>
/// <exception cref="InvalidOperationException">
/// This exception is thrown when <c>new StringBuilder(disposeImmediately: true)</c> or <c>ZString.CreateStringBuilder(notNested: true)</c> is nested.
/// See the README.md
/// </exception>
[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<char>.Shared.Rent(DefaultBufferSize);
}
buffer = buf;
index = 0;
this.disposeImmediately = disposeImmediately;
}
/// <summary>
/// Return the inner buffer to pool.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
if (buffer != null)
{
if (buffer.Length != ThreadStaticBufferSize)
{
ArrayPool<char>.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<char>.Shared.Rent(nextSize);
buffer.CopyTo(newBuffer, 0);
if (buffer.Length != ThreadStaticBufferSize)
{
ArrayPool<char>.Shared.Return(buffer);
}
buffer = newBuffer;
}
/// <summary>Appends the default line terminator to the end of this instance.</summary>
[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;
}
}
/// <summary>Appends the string representation of a specified value to this instance.</summary>
[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);
}
/// <summary>Appends the string representation of a specified value followed by the default line terminator to the end of this instance.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendLine(char value)
{
Append(value);
AppendLine();
}
/// <summary>Appends the string representation of a specified value to this instance.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(string value)
{
Append(value.AsSpan());
}
/// <summary>Appends the string representation of a specified value followed by the default line terminator to the end of this instance.</summary>
[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;
}
/// <summary>Appends a contiguous region of arbitrary memory to this instance.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(ReadOnlySpan<char> 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<char> value)
{
Append(value);
AppendLine();
}
/// <summary>Appends the string representation of a specified value to this instance.</summary>
public void Append<T>(T value)
{
if (!FormatterCache<T>.TryFormatDelegate(value, buffer.AsSpan(index), out var written, default))
{
Grow(written);
if (!FormatterCache<T>.TryFormatDelegate(value, buffer.AsSpan(index), out written, default))
{
ThrowArgumentException(nameof(value));
}
}
index += written;
}
/// <summary>Appends the string representation of a specified value followed by the default line terminator to the end of this instance.</summary>
public void AppendLine<T>(T value)
{
Append(value);
AppendLine();
}
static class ExceptionUtil
{
public static void ThrowArgumentOutOfRangeException(string paramName)
{
throw new ArgumentOutOfRangeException(paramName);
}
}
/// <summary>
/// Inserts a string 0 or more times into this builder at the specified position.
/// </summary>
/// <param name="index">The index to insert in this builder.</param>
/// <param name="value">The string to insert.</param>
/// <param name="count">The number of times to insert the string.</param>
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<char> 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<char>.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<char>.Shared.Return(buffer);
}
}
buffer = newBuffer;
this.index = newBufferIndex + remainLnegth;
}
/// <summary>
/// Replaces all instances of one character with another in this builder.
/// </summary>
/// <param name="oldChar">The character to replace.</param>
/// <param name="newChar">The character to replace <paramref name="oldChar"/> with.</param>
public void Replace(char oldChar, char newChar) => Replace(oldChar, newChar, 0, Length);
/// <summary>
/// Replaces all instances of one character with another in this builder.
/// </summary>
/// <param name="oldChar">The character to replace.</param>
/// <param name="newChar">The character to replace <paramref name="oldChar"/> with.</param>
/// <param name="startIndex">The index to start in this builder.</param>
/// <param name="count">The number of characters to read in this builder.</param>
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;
}
}
}
/// <summary>
/// Replaces all instances of one string with another in this builder.
/// </summary>
/// <param name="oldValue">The string to replace.</param>
/// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
/// <remarks>
/// If <paramref name="newValue"/> is <c>null</c>, instances of <paramref name="oldValue"/>
/// are removed from this builder.
/// </remarks>
public void Replace(string oldValue, string newValue) => Replace(oldValue, newValue, 0, Length);
public void Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue) => Replace(oldValue, newValue, 0, Length);
/// <summary>
/// Replaces all instances of one string with another in part of this builder.
/// </summary>
/// <param name="oldValue">The string to replace.</param>
/// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
/// <param name="startIndex">The index to start in this builder.</param>
/// <param name="count">The number of characters to read in this builder.</param>
/// <remarks>
/// If <paramref name="newValue"/> is <c>null</c>, instances of <paramref name="oldValue"/>
/// are removed from this builder.
/// </remarks>
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<char> oldValue, ReadOnlySpan<char> 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<char>.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<char>.Shared.Return(buffer);
}
buffer = newBuffer;
index = newBufferIndex;
}
/// <summary>
/// Replaces the contents of a single position within the builder.
/// </summary>
/// <param name="newChar">The character to use at the position.</param>
/// <param name="replaceIndex">The index to replace.</param>
public void ReplaceAt(char newChar, int replaceIndex)
{
int currentLength = Length;
if ((uint)replaceIndex > (uint)currentLength)
{
ExceptionUtil.ThrowArgumentOutOfRangeException(nameof(replaceIndex));
}
buffer![replaceIndex] = newChar;
}
/// <summary>
/// Removes a range of characters from this builder.
/// </summary>
/// <remarks>
/// This method does not reduce the capacity of this builder.
/// </remarks>
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
/// <summary>Copy inner buffer to the destination span.</summary>
public bool TryCopyTo(Span<char> destination, out int charsWritten)
{
if (destination.Length < index)
{
charsWritten = 0;
return false;
}
charsWritten = index;
buffer.AsSpan(0, index).CopyTo(destination);
return true;
}
/// <summary>Converts the value of this instance to a System.String.</summary>
public override string ToString()
{
if (index == 0)
return string.Empty;
return new string(buffer, 0, index);
}
// IBufferWriter
/// <summary>IBufferWriter.GetMemory.</summary>
public Memory<char> GetMemory(int sizeHint)
{
if ((buffer!.Length - index) < sizeHint)
{
Grow(sizeHint);
}
return buffer.AsMemory(index);
}
/// <summary>IBufferWriter.GetSpan.</summary>
public Span<char> GetSpan(int sizeHint)
{
if ((buffer!.Length - index) < sizeHint)
{
Grow(sizeHint);
}
return buffer.AsSpan(index);
}
/// <summary>IBufferWriter.Advance.</summary>
public void Advance(int count)
{
index += count;
}
void IResettableBufferWriter<char>.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>(T arg, int width, ReadOnlySpan<char> format, string argName)
{
if (width <= 0) // leftJustify
{
width *= -1;
if (!FormatterCache<T>.TryFormatDelegate(arg, buffer.AsSpan(index), out var charsWritten, format))
{
Grow(charsWritten);
if (!FormatterCache<T>.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<string>(arg);
int padding = width - s.Length;
if (padding > 0)
{
Append(' ', padding); // TODO Fill Method is too slow.
}
Append(s);
}
else
{
Span<char> s = stackalloc char[typeof(T).IsValueType ? Unsafe.SizeOf<T>() * 8 : 1024];
if (!FormatterCache<T>.TryFormatDelegate(arg, s, out var charsWritten, format))
{
s = stackalloc char[s.Length * 2];
if (!FormatterCache<T>.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));
}
/// <summary>
/// Register custom formatter
/// </summary>
public static void RegisterTryFormat<T>(TryFormat<T> formatMethod)
{
FormatterCache<T>.TryFormatDelegate = formatMethod;
}
static TryFormat<T?> CreateNullableFormatter<T>() where T : struct
{
return new TryFormat<T?>((T? x, Span<char> dest, out int written, ReadOnlySpan<char> format) =>
{
if (x == null)
{
written = 0;
return true;
}
return FormatterCache<T>.TryFormatDelegate(x.Value, dest, out written, format);
});
}
/// <summary>
/// Supports the Nullable type for a given struct type.
/// </summary>
public static void EnableNullableFormat<T>() where T : struct
{
RegisterTryFormat<T?>(CreateNullableFormatter<T>());
}
public static class FormatterCache<T>
{
public static TryFormat<T> TryFormatDelegate;
static FormatterCache()
{
var formatter = (TryFormat<T>?)CreateFormatter(typeof(T));
if (formatter == null)
{
if (typeof(T).IsEnum)
{
formatter = new TryFormat<T>(EnumUtil<T>.TryFormatUtf16);
}
else if (typeof(T) == typeof(string))
{
formatter = new TryFormat<T>(TryFormatString);
}
else
{
formatter = new TryFormat<T>(TryFormatDefault);
}
}
TryFormatDelegate = formatter;
}
static bool TryFormatString(T value, Span<char> dest, out int written, ReadOnlySpan<char> 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<char> dest, out int written, ReadOnlySpan<char> 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);
}
}
}
}