Skip to content

Commit 3263825

Browse files
authored
Merge pull request #3 from linkdotnet/feature/replace
Feature/replace
2 parents 5fdefe6 + 1dc4866 commit 3263825

File tree

5 files changed

+236
-0
lines changed

5 files changed

+236
-0
lines changed

logo.png

-65 Bytes
Loading
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
namespace LinkDotNet.StringBuilder;
2+
3+
internal static class NaiveSearch
4+
{
5+
/// <summary>
6+
/// Finds all occurence of <paramref name="word"/> in <paramref name="text"/>.
7+
/// </summary>
8+
/// <param name="text">The text to look for.</param>
9+
/// <param name="word">The word which should be found in <paramref name="word"/>.</param>
10+
/// <returns>Array of indexes where <paramref name="word"/> was found.</returns>
11+
public static ReadOnlySpan<int> FindAll(ReadOnlySpan<char> text, ReadOnlySpan<char> word)
12+
{
13+
if (text.IsEmpty || word.IsEmpty)
14+
{
15+
return Array.Empty<int>();
16+
}
17+
18+
if (text.Length < word.Length)
19+
{
20+
return Array.Empty<int>();
21+
}
22+
23+
var hits = new TypedSpanList<int>();
24+
25+
for (var i = 0; i < text.Length; i++)
26+
{
27+
for (var j = 0; j < word.Length; j++)
28+
{
29+
if (text[i + j] != word[j])
30+
{
31+
break;
32+
}
33+
34+
if (j == word.Length - 1)
35+
{
36+
hits.Add(i);
37+
}
38+
}
39+
}
40+
41+
return hits.AsSpan;
42+
}
43+
}
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: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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)
50+
=> Replace(oldValue, newValue, 0, Length);
51+
52+
/// <summary>
53+
/// Replaces all instances of one string with another in this builder.
54+
/// </summary>
55+
/// <param name="oldValue">The string to replace.</param>
56+
/// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
57+
/// <param name="startIndex">The index to start in this builder.</param>
58+
/// <param name="count">The number of characters to read in this builder.</param>
59+
/// <remarks>
60+
/// If <paramref name="newValue"/> is <c>empty</c>, instances of <paramref name="oldValue"/>
61+
/// are removed from this builder.
62+
/// </remarks>
63+
public void Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue, int startIndex, int count)
64+
{
65+
var length = startIndex + count;
66+
var slice = buffer[startIndex..length];
67+
68+
// We might want to check whether or not we want to introduce different
69+
// string search algorithms for longer strings.
70+
// I had checked initially with Boyer-Moore but it didn't make that much sense as we
71+
// don't expect very long strings and then the performance is literally the same. So I went with the easier solution.
72+
var hits = NaiveSearch.FindAll(slice, oldValue);
73+
74+
if (hits.IsEmpty)
75+
{
76+
return;
77+
}
78+
79+
var delta = newValue.Length - oldValue.Length;
80+
81+
for (var i = 0; i < hits.Length; i++)
82+
{
83+
var index = startIndex + hits[i] + (delta * i);
84+
Remove(index, oldValue.Length);
85+
Insert(index, newValue);
86+
}
87+
}
88+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
38+
[Fact]
39+
public void ShouldNotAlterIfNotFound()
40+
{
41+
var builder = new ValueStringBuilder();
42+
builder.Append("Hello");
43+
44+
builder.Replace("Test", "Not");
45+
46+
builder.ToString().Should().Be("Hello");
47+
}
48+
49+
[Fact]
50+
public void ShouldReplaceInSpan()
51+
{
52+
var builder = new ValueStringBuilder();
53+
builder.Append("Hello World. How are you doing. Hello world examples are always fun.");
54+
55+
builder.Replace("Hello", "Hallöchen", 0, 10);
56+
57+
builder.ToString().Should().Be("Hallöchen World. How are you doing. Hello world examples are always fun.");
58+
}
59+
}

0 commit comments

Comments
 (0)