Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save SunGuangdong/c42ea073352b0856f29ff1a68bbad6fe to your computer and use it in GitHub Desktop.
Save SunGuangdong/c42ea073352b0856f29ff1a68bbad6fe to your computer and use it in GitHub Desktop.
Non-Alloc String Splitter for C# / .NET
/** 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