Skip to content

Commit 3922af2

Browse files
authored
Merge pull request #339 from DataObjects-NET/after-pr-322-changes
Apply corrections to #322 changes
2 parents 4309951 + 8343bcf commit 3922af2

File tree

2 files changed

+332
-306
lines changed

2 files changed

+332
-306
lines changed
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
// Copy/pasted from https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
5+
// + re-structured to fit project rules
6+
// + formatting rules applied
7+
8+
using System;
9+
using System.Buffers;
10+
using System.Diagnostics;
11+
using System.Runtime.CompilerServices;
12+
using System.Runtime.InteropServices;
13+
14+
#nullable enable
15+
16+
namespace Xtensive.Core
17+
{
18+
internal ref struct ValueStringBuilder
19+
{
20+
private char[]? arrayToReturnToPool;
21+
private Span<char> chars;
22+
private int position;
23+
24+
public ref char this[int index] => ref chars[index];
25+
26+
public int Length
27+
{
28+
readonly get => position;
29+
set => position = value;
30+
}
31+
32+
public int Capacity => chars.Length;
33+
34+
35+
/// <summary>
36+
/// Returns the underlying storage of the builder.
37+
/// </summary>
38+
public Span<char> RawChars => chars;
39+
40+
/// <summary>
41+
/// Ensures that capacity is greater or equal to <paramref name="capacity"/>.
42+
/// Grows inner collection if required.
43+
/// </summary>
44+
/// <param name="capacity">Required capacity</param>
45+
public void EnsureCapacity(int capacity)
46+
{
47+
// If the caller has a bug and calls this with negative capacity, make sure to call Grow to throw an exception.
48+
if ((uint) capacity > (uint) chars.Length) {
49+
Grow(capacity - position);
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Get a pinnable reference to the builder.
55+
/// Does not ensure there is a null char after <see cref="Length"/>
56+
/// This overload is pattern matched in the C# 7.3+ compiler so you can omit
57+
/// the explicit method call, and write eg "fixed (char* c = builder)"
58+
/// </summary>
59+
public ref char GetPinnableReference() => ref MemoryMarshal.GetReference(chars);
60+
61+
/// <summary>
62+
/// Get a pinnable reference to the builder.
63+
/// </summary>
64+
/// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param>
65+
public ref char GetPinnableReference(bool terminate)
66+
{
67+
if (terminate) {
68+
EnsureCapacity(Length + 1);
69+
chars[Length] = '\0';
70+
}
71+
72+
return ref MemoryMarshal.GetReference(chars);
73+
}
74+
75+
public void Insert(int index, char value, int count)
76+
{
77+
if (position > chars.Length - count) {
78+
Grow(count);
79+
}
80+
81+
var remaining = position - index;
82+
chars.Slice(index, remaining).CopyTo(chars.Slice(index + count));
83+
chars.Slice(index, count).Fill(value);
84+
position += count;
85+
}
86+
87+
/// <summary>
88+
/// Inserts non-nul string by given index.
89+
/// </summary>
90+
/// <param name="index">Index to insert.</param>
91+
/// <param name="s">Non-null string.</param>
92+
public void Insert(int index, string? s)
93+
{
94+
if (s == null) {
95+
return;
96+
}
97+
98+
var count = s.Length;
99+
if (position > (chars.Length - count)) {
100+
Grow(count);
101+
}
102+
103+
var remaining = position - index;
104+
chars.Slice(index, remaining).CopyTo(chars.Slice(index + count));
105+
#if NET6_0_OR_GREATE
106+
s.CopyTo(chars.Slice(index));
107+
#else
108+
s.AsSpan().CopyTo(chars.Slice(index));
109+
#endif
110+
position += count;
111+
}
112+
113+
/// <summary>
114+
/// Appends char.
115+
/// </summary>
116+
/// <param name="c">Char to append.</param>
117+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
118+
public void Append(char c)
119+
{
120+
var origPos = position;
121+
var origChars = chars;
122+
if ((uint) origPos < (uint) origChars.Length) {
123+
origChars[origPos] = c;
124+
position = origPos + 1;
125+
}
126+
else {
127+
GrowAndAppend(c);
128+
}
129+
}
130+
131+
/// <summary>
132+
/// Appends non-null string.
133+
/// </summary>
134+
/// <param name="s">String to append.</param>
135+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
136+
public void Append(string? s)
137+
{
138+
if (s == null) {
139+
return;
140+
}
141+
142+
var origPos = position;
143+
if (s.Length == 1 && (uint) origPos < (uint) chars.Length) {
144+
// very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc.
145+
chars[origPos] = s[0];
146+
position = origPos + 1;
147+
}
148+
else {
149+
if (origPos > chars.Length - s.Length) {
150+
Grow(s.Length);
151+
}
152+
153+
#if NET6_0_OR_GREATER
154+
s.CopyTo(chars.Slice(origPos));
155+
#else
156+
s.AsSpan().CopyTo(chars.Slice(origPos));
157+
#endif
158+
position += s.Length;
159+
}
160+
}
161+
162+
public void Append(char c, int count)
163+
{
164+
if (position > chars.Length - count) {
165+
Grow(count);
166+
}
167+
168+
var dst = chars.Slice(position, count);
169+
for (var i = 0; i < dst.Length; i++) {
170+
dst[i] = c;
171+
}
172+
173+
position += count;
174+
}
175+
176+
public void Append(ReadOnlySpan<char> value)
177+
{
178+
var pos = position;
179+
if (pos > chars.Length - value.Length) {
180+
Grow(value.Length);
181+
}
182+
183+
value.CopyTo(chars.Slice(position));
184+
position += value.Length;
185+
}
186+
187+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
188+
public Span<char> AppendSpan(int length)
189+
{
190+
var origPos = position;
191+
if (origPos > chars.Length - length) {
192+
Grow(length);
193+
}
194+
195+
position = origPos + length;
196+
return chars.Slice(origPos, length);
197+
}
198+
199+
[MethodImpl(MethodImplOptions.NoInlining)]
200+
private void GrowAndAppend(char c)
201+
{
202+
Grow(1);
203+
Append(c);
204+
}
205+
206+
/// <summary>
207+
/// Resize the internal buffer either by doubling current buffer size or
208+
/// by adding <paramref name="additionalCapacityBeyondPos"/> to
209+
/// <see cref="position"/> whichever is greater.
210+
/// </summary>
211+
/// <param name="additionalCapacityBeyondPos">
212+
/// Number of chars requested beyond current position.
213+
/// </param>
214+
[MethodImpl(MethodImplOptions.NoInlining)]
215+
private void Grow(int additionalCapacityBeyondPos)
216+
{
217+
const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
218+
219+
ArgumentValidator.EnsureArgumentIsGreaterThan(additionalCapacityBeyondPos, 0, nameof(additionalCapacityBeyondPos));
220+
221+
// Increase to at least the required size (_pos + additionalCapacityBeyondPos), but try
222+
// to double the size if possible, bounding the doubling to not go beyond the max array length.
223+
var newCapacity = (int) Math.Max(
224+
(uint) (position + additionalCapacityBeyondPos),
225+
Math.Min((uint) chars.Length * 2, ArrayMaxLength));
226+
227+
// Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
228+
// This could also go negative if the actual required length wraps around.
229+
var poolArray = ArrayPool<char>.Shared.Rent(newCapacity);
230+
231+
chars.Slice(0, position).CopyTo(poolArray);
232+
233+
var toReturn = arrayToReturnToPool;
234+
chars = arrayToReturnToPool = poolArray;
235+
if (toReturn != null) {
236+
ArrayPool<char>.Shared.Return(toReturn);
237+
}
238+
}
239+
240+
241+
/// <summary>
242+
/// Returns a span around the contents of the builder.
243+
/// </summary>
244+
/// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param>
245+
public ReadOnlySpan<char> AsSpan(bool terminate)
246+
{
247+
if (terminate) {
248+
EnsureCapacity(Length + 1);
249+
chars[Length] = '\0';
250+
}
251+
252+
return chars.Slice(0, position);
253+
}
254+
255+
/// <summary>
256+
/// Returns a span around the contents of the builder.
257+
/// </summary>
258+
/// <returns>Read-only span wrapper of the content.</returns>
259+
public ReadOnlySpan<char> AsSpan() => chars.Slice(0, position);
260+
261+
/// <summary>
262+
/// Returns a span around the contents of the builder.
263+
/// </summary>
264+
/// <param name="start">Start position.</param>
265+
/// <returns>Read-only span wrapper of the content with reqested portion of data.</returns>
266+
public ReadOnlySpan<char> AsSpan(int start) => chars.Slice(start, position - start);
267+
268+
/// <summary>
269+
/// Returns a span around the contents of the builder.
270+
/// </summary>
271+
/// <param name="start">Start position.</param>
272+
/// <param name="length">Length.</param>
273+
/// <returns>Read-only span wrapper of the content with reqested portion of data.</returns>
274+
public ReadOnlySpan<char> AsSpan(int start, int length) => chars.Slice(start, length);
275+
276+
/// <summary>
277+
/// Tries to copy rawchars to given <paramref name="destination"/>.
278+
/// If try is failed it disposes resources.
279+
/// </summary>
280+
/// <param name="destination">The span to copy to.</param>
281+
/// <param name="charsWritten">Count of copied chars.</param>
282+
/// <returns><see langword="true"/> if copy was sucessful, othrewise <see langword ="false"/></returns>
283+
public bool TryCopyTo(Span<char> destination, out int charsWritten)
284+
{
285+
if (chars.Slice(0, position).TryCopyTo(destination)) {
286+
charsWritten = position;
287+
Dispose();
288+
return true;
289+
}
290+
else {
291+
charsWritten = 0;
292+
Dispose();
293+
return false;
294+
}
295+
}
296+
297+
/// <summary>
298+
/// Transforms result to string and disposes resources.
299+
/// </summary>
300+
/// <returns>result string</returns>
301+
public override string ToString()
302+
{
303+
var s = chars.Slice(0, position).ToString();
304+
Dispose();
305+
return s;
306+
}
307+
308+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
309+
public void Dispose()
310+
{
311+
var toReturn = arrayToReturnToPool;
312+
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
313+
if (toReturn != null) {
314+
ArrayPool<char>.Shared.Return(toReturn);
315+
}
316+
}
317+
318+
public ValueStringBuilder(Span<char> initialBuffer)
319+
{
320+
arrayToReturnToPool = null;
321+
chars = initialBuffer;
322+
position = 0;
323+
}
324+
325+
public ValueStringBuilder(int initialCapacity)
326+
{
327+
arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
328+
chars = arrayToReturnToPool;
329+
position = 0;
330+
}
331+
}
332+
}

0 commit comments

Comments
 (0)