Skip to content

Commit 2e6eab6

Browse files
committed
.
1 parent a5558ae commit 2e6eab6

File tree

3 files changed

+337
-244
lines changed

3 files changed

+337
-244
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/// <summary>
2+
/// Helper for matching patterns in StringBuilder that may span across chunk boundaries.
3+
/// </summary>
4+
static class CrossChunkMatcher
5+
{
6+
/// <summary>
7+
/// Iterates through StringBuilder chunks, invoking callbacks for potential matches
8+
/// both within chunks and spanning chunk boundaries.
9+
/// </summary>
10+
/// <param name="builder">The StringBuilder to search</param>
11+
/// <param name="carryoverSize">Size of carryover buffer (typically maxPatternLength - 1)</param>
12+
/// <param name="context">User context passed to callbacks</param>
13+
/// <param name="onCrossChunk">Called for each potential cross-chunk match position</param>
14+
/// <param name="onWithinChunk">Called for each position within a chunk</param>
15+
public static void ProcessChunks<TContext>(
16+
StringBuilder builder,
17+
int carryoverSize,
18+
TContext context,
19+
CrossChunkHandler<TContext> onCrossChunk,
20+
WithinChunkHandler<TContext> onWithinChunk)
21+
{
22+
Span<char> carryoverBuffer = stackalloc char[carryoverSize];
23+
var carryoverLength = 0;
24+
var previousChunkAbsoluteEnd = 0;
25+
var absolutePosition = 0;
26+
27+
foreach (var chunk in builder.GetChunks())
28+
{
29+
var chunkSpan = chunk.Span;
30+
31+
// Check for matches spanning from previous chunk to current chunk
32+
if (carryoverLength > 0)
33+
{
34+
for (var carryoverIndex = 0; carryoverIndex < carryoverLength; carryoverIndex++)
35+
{
36+
var remainingInCarryover = carryoverLength - carryoverIndex;
37+
var startPosition = previousChunkAbsoluteEnd - carryoverLength + carryoverIndex;
38+
39+
onCrossChunk(
40+
builder,
41+
carryoverBuffer,
42+
carryoverIndex,
43+
remainingInCarryover,
44+
chunkSpan,
45+
startPosition,
46+
context);
47+
}
48+
}
49+
50+
// Process matches entirely within this chunk
51+
var chunkIndex = 0;
52+
while (chunkIndex < chunk.Length)
53+
{
54+
var absoluteIndex = absolutePosition + chunkIndex;
55+
var skipAhead = onWithinChunk(chunk, chunkSpan, chunkIndex, absoluteIndex, context);
56+
chunkIndex += skipAhead > 0 ? skipAhead : 1;
57+
}
58+
59+
// Save last N chars for next iteration
60+
carryoverLength = Math.Min(carryoverSize, chunk.Length);
61+
chunkSpan.Slice(chunk.Length - carryoverLength, carryoverLength).CopyTo(carryoverBuffer);
62+
63+
previousChunkAbsoluteEnd = absolutePosition + chunk.Length;
64+
absolutePosition += chunk.Length;
65+
}
66+
}
67+
68+
/// <summary>
69+
/// Applies matches to a StringBuilder in descending position order.
70+
/// </summary>
71+
public static void ApplyMatches<TMatch>(
72+
StringBuilder builder,
73+
List<TMatch> matches,
74+
Func<TMatch, int> getIndex,
75+
Func<TMatch, int> getLength,
76+
Func<TMatch, string> getValue)
77+
{
78+
foreach (var match in matches.OrderByDescending(getIndex))
79+
{
80+
builder.Overwrite(getValue(match), getIndex(match), getLength(match));
81+
}
82+
}
83+
84+
/// <summary>
85+
/// Callback for processing potential cross-chunk matches.
86+
/// </summary>
87+
/// <param name="builder">The StringBuilder being processed</param>
88+
/// <param name="carryoverBuffer">Buffer containing end of previous chunk</param>
89+
/// <param name="carryoverIndex">Starting index within carryover buffer</param>
90+
/// <param name="remainingInCarryover">Characters remaining from carryoverIndex to end</param>
91+
/// <param name="currentChunkSpan">Span of the current chunk</param>
92+
/// <param name="absoluteStartPosition">Absolute position in StringBuilder where potential match starts</param>
93+
/// <param name="context">User context</param>
94+
public delegate void CrossChunkHandler<TContext>(
95+
StringBuilder builder,
96+
Span<char> carryoverBuffer,
97+
int carryoverIndex,
98+
int remainingInCarryover,
99+
CharSpan currentChunkSpan,
100+
int absoluteStartPosition,
101+
TContext context);
102+
103+
/// <summary>
104+
/// Callback for processing positions within a chunk.
105+
/// </summary>
106+
/// <param name="chunk">The current chunk memory</param>
107+
/// <param name="chunkSpan">Span of the current chunk</param>
108+
/// <param name="chunkIndex">Current index within the chunk</param>
109+
/// <param name="absoluteIndex">Absolute position in StringBuilder</param>
110+
/// <param name="context">User context</param>
111+
/// <returns>Number of positions to skip ahead (0 or 1 for normal iteration, more to skip past a match)</returns>
112+
public delegate int WithinChunkHandler<TContext>(
113+
ReadOnlyMemory<char> chunk,
114+
CharSpan chunkSpan,
115+
int chunkIndex,
116+
int absoluteIndex,
117+
TContext context);
118+
}

0 commit comments

Comments
 (0)