Skip to content

Commit 1943452

Browse files
committed
Initial work
1 parent 5fdefe6 commit 1943452

File tree

4 files changed

+257
-0
lines changed

4 files changed

+257
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
namespace LinkDotNet.StringBuilder;
2+
3+
/// <summary>
4+
/// Taken from here: https://github.com/linkdotnet/StringOperations and adopted to work with ReadOnlySpan's
5+
/// </summary>
6+
internal static class BoyerMooreSearch
7+
{
8+
private const int AlphabetSize = 256;
9+
10+
/// <summary>
11+
/// Finds all occurence of <paramref name="word"/> in <paramref name="text"/>.
12+
/// </summary>
13+
/// <param name="text">The text to look for.</param>
14+
/// <param name="word">The word which should be found in <paramref name="word"/>.</param>
15+
/// <returns>Array of indexes where <paramref name="word"/> was found.</returns>
16+
public static ReadOnlySpan<int> FindAll(ReadOnlySpan<char> text, ReadOnlySpan<char> word)
17+
{
18+
if (text.IsEmpty || word.IsEmpty)
19+
{
20+
return Array.Empty<int>();
21+
}
22+
23+
if (text.Length < word.Length)
24+
{
25+
return Array.Empty<int>();
26+
}
27+
28+
var badCharacterTable = GetBadCharacterTable(word);
29+
var shift = 0;
30+
var hits = new TypedSpanList<int>();
31+
while (shift <= text.Length - word.Length)
32+
{
33+
var index = word.Length - 1;
34+
35+
index = ReduceIndexWhileMatchAtShift(text, word, index, shift);
36+
37+
if (index < 0)
38+
{
39+
hits.Add(shift);
40+
41+
shift = ShiftPatternToNextCharacterWithLastOccurrenceOfPattern(text, shift, word.Length, badCharacterTable);
42+
}
43+
else
44+
{
45+
shift = ShiftPatternAfterBadCharacter(text, shift, index, badCharacterTable);
46+
}
47+
}
48+
49+
return hits.AsSpan;
50+
}
51+
52+
private static ReadOnlySpan<int> GetBadCharacterTable(ReadOnlySpan<char> text)
53+
{
54+
Span<int> table = new int[AlphabetSize];
55+
table.Fill(-1);
56+
57+
for (var i = 0; i < text.Length; i++)
58+
{
59+
table[text[i]] = i;
60+
}
61+
62+
return table;
63+
}
64+
65+
private static int ReduceIndexWhileMatchAtShift(ReadOnlySpan<char> text, ReadOnlySpan<char> word, int index, int shift)
66+
{
67+
while (index >= 0 && text[shift + index] == word[index])
68+
{
69+
index--;
70+
}
71+
72+
return index;
73+
}
74+
75+
private static int ShiftPatternToNextCharacterWithLastOccurrenceOfPattern(ReadOnlySpan<char> text, int shift, int wordLength, ReadOnlySpan<int> badCharacterTable)
76+
{
77+
return shift + (shift + wordLength < text.Length
78+
? wordLength - badCharacterTable[text[shift + wordLength]]
79+
: 1);
80+
}
81+
82+
private static int ShiftPatternAfterBadCharacter(ReadOnlySpan<char> text, int shift, int index, ReadOnlySpan<int> badCharacterTable)
83+
{
84+
var character = text[shift + index];
85+
return shift + Math.Max(1, index - badCharacterTable[character]);
86+
}
87+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Buffers;
2+
3+
namespace LinkDotNet.StringBuilder;
4+
5+
/// <summary>
6+
/// Represents a List based on the <see cref="Span{T}"/> type.
7+
/// </summary>
8+
/// <typeparam name="T">Any struct.</typeparam>
9+
internal ref struct TypedSpanList<T>
10+
where T : struct
11+
{
12+
private Span<T> buffer;
13+
private int count;
14+
15+
/// <summary>
16+
/// Initializes a new instance of the <see cref="TypedSpanList{T}"/> struct.
17+
/// </summary>
18+
public TypedSpanList()
19+
{
20+
buffer = new T[32];
21+
count = 0;
22+
}
23+
24+
public ReadOnlySpan<T> AsSpan => buffer[..count];
25+
26+
public void Add(T value)
27+
{
28+
if (count >= buffer.Length)
29+
{
30+
Grow();
31+
}
32+
33+
buffer[count] = value;
34+
count++;
35+
}
36+
37+
private void Grow(int capacity = 0)
38+
{
39+
var currentSize = buffer.Length;
40+
var newSize = capacity > 0 ? capacity : currentSize * 2;
41+
var rented = ArrayPool<T>.Shared.Rent(newSize);
42+
buffer.CopyTo(rented);
43+
buffer = rented;
44+
ArrayPool<T>.Shared.Return(rented);
45+
}
46+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
namespace LinkDotNet.StringBuilder;
2+
3+
public ref partial struct ValueStringBuilder
4+
{
5+
/// <summary>
6+
/// Replaces all instances of one character with another in this builder.
7+
/// </summary>
8+
/// <param name="oldValue">The character to replace.</param>
9+
/// <param name="newValue">The character to replace <paramref name="oldValue"/> with.</param>
10+
public void Replace(char oldValue, char newValue) => Replace(oldValue, newValue, 0, Length);
11+
12+
/// <summary>
13+
/// Replaces all instances of one character with another in this builder.
14+
/// </summary>
15+
/// <param name="oldValue">The character to replace.</param>
16+
/// <param name="newValue">The character to replace <paramref name="oldValue"/> with.</param>
17+
/// <param name="startIndex">The index to start in this builder.</param>
18+
/// <param name="count">The number of characters to read in this builder.</param>
19+
public void Replace(char oldValue, char newValue, int startIndex, int count)
20+
{
21+
if (startIndex < 0)
22+
{
23+
throw new ArgumentException("Start index can't be smaller than 0.", nameof(startIndex));
24+
}
25+
26+
if (count > bufferPosition)
27+
{
28+
throw new ArgumentException($"Count: {count} is bigger than the current size {bufferPosition}.", nameof(count));
29+
}
30+
31+
for (var i = startIndex; i < startIndex + count; i++)
32+
{
33+
if (buffer[i] == oldValue)
34+
{
35+
buffer[i] = newValue;
36+
}
37+
}
38+
}
39+
40+
/// <summary>
41+
/// Replaces all instances of one string with another in this builder.
42+
/// </summary>
43+
/// <param name="oldValue">The string to replace.</param>
44+
/// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
45+
/// <remarks>
46+
/// If <paramref name="newValue"/> is <c>empty</c>, instances of <paramref name="oldValue"/>
47+
/// are removed from this builder.
48+
/// </remarks>
49+
public void Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue) => Replace(oldValue, newValue, 0, Length);
50+
51+
/// <summary>
52+
/// Replaces all instances of one string with another in this builder.
53+
/// </summary>
54+
/// <param name="oldValue">The string to replace.</param>
55+
/// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
56+
/// <param name="startIndex">The index to start in this builder.</param>
57+
/// <param name="count">The number of characters to read in this builder.</param>
58+
/// <remarks>
59+
/// If <paramref name="newValue"/> is <c>empty</c>, instances of <paramref name="oldValue"/>
60+
/// are removed from this builder.
61+
/// </remarks>
62+
public void Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue, int startIndex, int count)
63+
{
64+
var length = startIndex + count;
65+
var slice = buffer[startIndex..length];
66+
67+
// We might want to check if for very small strings we go with a naive approach
68+
var hits = BoyerMooreSearch.FindAll(slice, oldValue);
69+
if (hits.IsEmpty)
70+
{
71+
return;
72+
}
73+
74+
var delta = newValue.Length - oldValue.Length;
75+
76+
for (var i = 0; i < hits.Length; i++)
77+
{
78+
var index = startIndex + hits[0] + (delta * i);
79+
Remove(index, oldValue.Length);
80+
var debug = ToString();
81+
Console.WriteLine(debug);
82+
Insert(index, newValue);
83+
debug = ToString();
84+
Console.WriteLine(debug);
85+
}
86+
}
87+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace LinkDotNet.StringBuilder.UnitTests;
2+
3+
public class ValueStringBuilderReplaceTests
4+
{
5+
[Fact]
6+
public void ShouldReplaceAllCharacters()
7+
{
8+
var builder = new ValueStringBuilder();
9+
builder.Append("CCCC");
10+
11+
builder.Replace('C', 'B');
12+
13+
builder.ToString().Should().Be("BBBB");
14+
}
15+
16+
[Fact]
17+
public void ShouldReplaceAllCharactersInGivenSpan()
18+
{
19+
var builder = new ValueStringBuilder();
20+
builder.Append("CCCC");
21+
22+
builder.Replace('C', 'B', 1, 2);
23+
24+
builder.ToString().Should().Be("CBBC");
25+
}
26+
27+
[Fact]
28+
public void ShouldReplaceAllText()
29+
{
30+
var builder = new ValueStringBuilder();
31+
builder.Append("Hello World. How are you doing. Hello world examples are always fun.");
32+
33+
builder.Replace("Hello", "Hallöchen");
34+
35+
builder.ToString().Should().Be("Hallöchen World. How are you doing. Hallöchen world examples are always fun.");
36+
}
37+
}

0 commit comments

Comments
 (0)