-
-
Save SunGuangdong/c42ea073352b0856f29ff1a68bbad6fe to your computer and use it in GitHub Desktop.
Non-Alloc String Splitter for C# / .NET
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Non-Alloc String Splitter for C# / .NET | |
** (c) 2024 https://github.com/sator-imaging | |
** Licensed under the MIT License | |
How to Use | |
========== | |
```cs | |
var result = new NonAllocStringSplitter("ABC DEF", ' '); | |
_ = result.Count; // 2 | |
_ = result.Value0; // ABC | |
_ = result.Value1; // DEF | |
// iterate | |
for (int i = 0; i < result.Count; i++) | |
{ | |
var span = result[i]; | |
} | |
// out of range values are null | |
result.Value2; | |
... | |
result.Value9; | |
// split by sequence or by any char | |
result = new NonAllocStringSplitter("ABC DEF", " C"); // no ' C' found, 1 item | |
result = new NonAllocStringSplitter("ABC DEF", " C".AsSpan()); // split by ' ' or 'C', 2 items | |
``` | |
*/ | |
using System; | |
using System.Runtime.CompilerServices; | |
#nullable enable | |
namespace SatorImaging.UnityFundamentals | |
{ | |
/// <summary> | |
/// Non-allocation string splitter. | |
/// </summary> | |
/// <remarks> | |
/// NOTE: max 10 items are allowed to be splitted otherwise throws exception. | |
/// </remarks> | |
public readonly ref struct NonAllocStringSplitter //: IEnumerator | |
{ | |
readonly public int Count; | |
readonly public ReadOnlySpan<char> Value0; | |
readonly public ReadOnlySpan<char> Value1; | |
readonly public ReadOnlySpan<char> Value2; | |
readonly public ReadOnlySpan<char> Value3; | |
readonly public ReadOnlySpan<char> Value4; | |
readonly public ReadOnlySpan<char> Value5; | |
readonly public ReadOnlySpan<char> Value6; | |
readonly public ReadOnlySpan<char> Value7; | |
readonly public ReadOnlySpan<char> Value8; | |
readonly public ReadOnlySpan<char> Value9; | |
NonAllocStringSplitter(ReadOnlySpan<char> initValue) | |
{ | |
this.Count = 0; | |
this.Value0 = initValue; | |
this.Value1 = initValue; | |
this.Value2 = initValue; | |
this.Value3 = initValue; | |
this.Value4 = initValue; | |
this.Value5 = initValue; | |
this.Value6 = initValue; | |
this.Value7 = initValue; | |
this.Value8 = initValue; | |
this.Value9 = initValue; | |
} | |
public NonAllocStringSplitter(ReadOnlySpan<char> text, char splitter) : this(default) | |
{ | |
var span = text.TrimEnd(splitter); | |
int start = 0; | |
int pos; | |
int length; | |
while (true) | |
{ | |
length = span.Slice(start).IndexOf(splitter); | |
#region //////// COPY & PASTE //////// | |
if (length < 0) | |
{ | |
length = span.Length - start; | |
} | |
pos = length + start; | |
if (pos == start) | |
goto NEXT; | |
switch (this.Count) | |
{ | |
case 0: | |
this.Value0 = span.Slice(start, length); | |
break; | |
case 1: | |
this.Value1 = span.Slice(start, length); | |
break; | |
case 2: | |
this.Value2 = span.Slice(start, length); | |
break; | |
case 3: | |
this.Value3 = span.Slice(start, length); | |
break; | |
case 4: | |
this.Value4 = span.Slice(start, length); | |
break; | |
case 5: | |
this.Value5 = span.Slice(start, length); | |
break; | |
case 6: | |
this.Value6 = span.Slice(start, length); | |
break; | |
case 7: | |
this.Value7 = span.Slice(start, length); | |
break; | |
case 8: | |
this.Value8 = span.Slice(start, length); | |
break; | |
case 9: | |
this.Value9 = span.Slice(start, length); | |
break; | |
default: | |
throw new NotSupportedException("item count exceeded: max 10 items"); | |
} | |
this.Count++; | |
NEXT: | |
start = pos + 1; | |
if (start >= span.Length) | |
break; | |
} | |
#endregion | |
} | |
public NonAllocStringSplitter(ReadOnlySpan<char> text, ReadOnlySpan<char> splitAny) : this(default) | |
{ | |
var span = text.TrimEnd(splitAny); | |
int start = 0; | |
int pos; | |
int length; | |
while (true) | |
{ | |
length = span.Slice(start).IndexOfAny(splitAny); | |
#region //////// COPY & PASTE //////// | |
if (length < 0) | |
{ | |
length = span.Length - start; | |
} | |
pos = length + start; | |
if (pos == start) | |
goto NEXT; | |
switch (this.Count) | |
{ | |
case 0: | |
this.Value0 = span.Slice(start, length); | |
break; | |
case 1: | |
this.Value1 = span.Slice(start, length); | |
break; | |
case 2: | |
this.Value2 = span.Slice(start, length); | |
break; | |
case 3: | |
this.Value3 = span.Slice(start, length); | |
break; | |
case 4: | |
this.Value4 = span.Slice(start, length); | |
break; | |
case 5: | |
this.Value5 = span.Slice(start, length); | |
break; | |
case 6: | |
this.Value6 = span.Slice(start, length); | |
break; | |
case 7: | |
this.Value7 = span.Slice(start, length); | |
break; | |
case 8: | |
this.Value8 = span.Slice(start, length); | |
break; | |
case 9: | |
this.Value9 = span.Slice(start, length); | |
break; | |
default: | |
throw new NotSupportedException("item count exceeded: max 10 items"); | |
} | |
this.Count++; | |
NEXT: | |
start = pos + 1; | |
if (start >= span.Length) | |
break; | |
} | |
#endregion | |
} | |
public NonAllocStringSplitter(ReadOnlySpan<char> text, string splitBySequence) : this(default) | |
{ | |
var span = text.TrimEnd(splitBySequence); | |
int start = 0; | |
int pos; | |
int length; | |
while (true) | |
{ | |
length = span.Slice(start).IndexOf(splitBySequence); | |
#region //////// COPY & PASTE //////// | |
if (length < 0) | |
{ | |
length = span.Length - start; | |
} | |
pos = length + start; | |
if (pos == start) | |
goto NEXT; | |
switch (this.Count) | |
{ | |
case 0: | |
this.Value0 = span.Slice(start, length); | |
break; | |
case 1: | |
this.Value1 = span.Slice(start, length); | |
break; | |
case 2: | |
this.Value2 = span.Slice(start, length); | |
break; | |
case 3: | |
this.Value3 = span.Slice(start, length); | |
break; | |
case 4: | |
this.Value4 = span.Slice(start, length); | |
break; | |
case 5: | |
this.Value5 = span.Slice(start, length); | |
break; | |
case 6: | |
this.Value6 = span.Slice(start, length); | |
break; | |
case 7: | |
this.Value7 = span.Slice(start, length); | |
break; | |
case 8: | |
this.Value8 = span.Slice(start, length); | |
break; | |
case 9: | |
this.Value9 = span.Slice(start, length); | |
break; | |
default: | |
throw new NotSupportedException("item count exceeded: max 10 items"); | |
} | |
this.Count++; | |
NEXT: | |
start = pos + 1; | |
if (start >= span.Length) | |
break; | |
} | |
#endregion | |
} | |
/// <summary>Use `.Value0~9` instead to achieve a really little bit performance gain.</summary> | |
readonly public ReadOnlySpan<char> this[int index] | |
{ | |
get | |
{ | |
if (unchecked((uint)index >= (uint)Count)) | |
throw new IndexOutOfRangeException(); | |
var result = index switch | |
{ | |
0 => Value0, | |
1 => Value1, | |
2 => Value2, | |
3 => Value3, | |
4 => Value4, | |
5 => Value5, | |
6 => Value6, | |
7 => Value7, | |
8 => Value8, | |
9 => Value9, | |
_ => throw new IndexOutOfRangeException(), | |
}; | |
return result; | |
} | |
} | |
} | |
/// <summary> | |
/// Extension methods for <see cref="NonAllocStringSplitter"/> | |
/// </summary> | |
public static class NonAllocStringSplitterExtensions | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static NonAllocStringSplitter SplitNonAlloc(this ReadOnlySpan<char> text, char splitter) => new(text, splitter); | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static NonAllocStringSplitter SplitNonAlloc(this ReadOnlySpan<char> text, ReadOnlySpan<char> splitAny) => new(text, splitAny); | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public static NonAllocStringSplitter SplitNonAlloc(this ReadOnlySpan<char> text, string splitBySequence) => new(text, splitBySequence); | |
#if UNITY_EDITOR | |
public static class DEBUG | |
{ | |
const string MENU_ROOT = "DEBUG/" + nameof(NonAllocStringSplitter) + "/"; | |
[UnityEditor.MenuItem(MENU_ROOT + nameof(Basic_Tests), priority = int.MaxValue / 2)] | |
public static void Basic_Tests() | |
{ | |
var splitter = ' '; | |
var input = "ABC DEF"; | |
var result = new NonAllocStringSplitter(input, splitter); | |
UnityEngine.Debug.Log($"({input}) {result.Count}: {result.Value0.ToString()} / {result.Value1.ToString()}"); | |
input = ",,,,,,,///no-split///,,,,,,,,"; | |
result = new NonAllocStringSplitter(input, new char[] { ' ', '/', ',' }); | |
UnityEngine.Debug.Log($"({input}) {result.Count}: {result.Value0.ToString()} / {result.Value1.ToString()}"); | |
input = "split by sequence"; | |
var splitSeq = " yb "; | |
result = new NonAllocStringSplitter(input, splitSeq); | |
UnityEngine.Debug.Log($"Expect Length = 1: ({input}) {result.Count}: {result.Value0.ToString()} / {result.Value1.ToString()}"); | |
result = new NonAllocStringSplitter(input, splitSeq.AsSpan()); | |
UnityEngine.Debug.Log($"Expect Length = 2: ({input}) {result.Count}: {result.Value0.ToString()} / {result.Value1.ToString()}"); | |
input = "310 311 312 313 314 315 316 317 318 319"; // OK: 10 items | |
result = new NonAllocStringSplitter(input, splitter); | |
for (int i = 0; i < result.Count; i++) | |
{ | |
UnityEngine.Debug.Log($"Iterate #{i}: {result[i].ToString()}"); | |
} | |
input = "310 311 312 313 314 315 316 317 318 319 320"; // NG: 11 items...!! | |
try | |
{ | |
_ = new NonAllocStringSplitter(input, splitter); | |
} | |
catch (Exception exc) | |
{ | |
UnityEngine.Debug.LogWarning("EXPECTED EXCEPTION: " + exc.Message); | |
} | |
} | |
} | |
#endif | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment