Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions Semantics.Strings/SemanticString.cs
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,10 @@ public ref struct SpanSplitEnumerator
private readonly char _separator;
private readonly StringSplitOptions _options;

// StringSplitOptions.TrimEntries, which netstandard2.0/2.1 do not name. The value is the same on
// every target, so a caller on .NET 5+ passing the named member is honoured everywhere.
private const StringSplitOptions TrimEntries = (StringSplitOptions)2;

internal SpanSplitEnumerator(ReadOnlySpan<char> span, char separator, StringSplitOptions options)
{
_remaining = span;
Expand All @@ -1000,30 +1004,36 @@ internal SpanSplitEnumerator(ReadOnlySpan<char> span, char separator, StringSpli
/// <returns>true if there is a next segment; otherwise, false.</returns>
public bool MoveNext()
{
if (_remaining.IsEmpty)
{
return false;
}

int separatorIndex = _remaining.IndexOf(_separator);
if (separatorIndex >= 0)
{
Current = _remaining[..separatorIndex];
_remaining = _remaining[(separatorIndex + 1)..];
}
else
{
Current = _remaining;
_remaining = default;
}

// Handle StringSplitOptions.RemoveEmptyEntries
if (_options == StringSplitOptions.RemoveEmptyEntries && Current.IsEmpty)
// A loop, not recursion: a long run of separators with RemoveEmptyEntries would otherwise
// take one stack frame per empty entry and overflow the stack.
while (!_remaining.IsEmpty)
{
return MoveNext(); // Recursively skip empty entries
int separatorIndex = _remaining.IndexOf(_separator);
if (separatorIndex >= 0)
{
Current = _remaining[..separatorIndex];
_remaining = _remaining[(separatorIndex + 1)..];
}
else
{
Current = _remaining;
_remaining = default;
}

if ((_options & TrimEntries) != 0)
{
Current = Current.Trim();
}

if ((_options & StringSplitOptions.RemoveEmptyEntries) != 0 && Current.IsEmpty)
{
continue;
}

return true;
}

return true;
return false;
}
}
}
48 changes: 48 additions & 0 deletions Semantics.Test/SemanticStringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,54 @@ public void SpanSplitEnumerator_OnlySeparators_HandlesCorrectly()
Assert.IsTrue(parts.All(string.IsNullOrEmpty), "All parts should be empty");
}

private static readonly string[] expectedOnlyX = ["x"];
private static readonly string[] expectedAB = ["a", "b"];
private static readonly string[] expectedTrimmed = ["a", "b", "", "c"];

private static List<string> SplitToList(string value, StringSplitOptions options)
{
MySemanticString semantic = SemanticString<MySemanticString>.Create<MySemanticString>(value);
List<string> parts = [];
foreach (ReadOnlySpan<char> part in semantic.Split(',', options))
{
parts.Add(part.ToString());
}

return parts;
}

[TestMethod]
public void SpanSplitEnumerator_RemoveEmptyEntries_LongRunOfSeparators_DoesNotOverflowTheStack()
{
List<string> parts = SplitToList(new string(',', 1_000_000) + "x", StringSplitOptions.RemoveEmptyEntries);

Assert.AreSequenceEqual(expectedOnlyX, parts);
}

[TestMethod]
public void SpanSplitEnumerator_RemoveEmptyEntriesWithTrimEntries_RemovesWhitespaceEntries()
{
List<string> parts = SplitToList("a, ,b", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

Assert.AreSequenceEqual(expectedAB, parts);
}

[TestMethod]
public void SpanSplitEnumerator_RemoveEmptyEntriesWithTrimEntries_RemovesEmptyEntries()
{
List<string> parts = SplitToList("a,,b", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);

Assert.AreSequenceEqual(expectedAB, parts);
}

[TestMethod]
public void SpanSplitEnumerator_TrimEntries_TrimsEachEntry()
{
List<string> parts = SplitToList(" a , b , ,c", StringSplitOptions.TrimEntries);

Assert.AreSequenceEqual(expectedTrimmed, parts);
}

[TestMethod]
public void ErrorMessages_ContainTypeInformation()
{
Expand Down
Loading