Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/replace #3

Merged
merged 2 commits into from
Apr 8, 2022
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
Binary file modified logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions src/LinkDotNet.StringBuilder/NaiveSearch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace LinkDotNet.StringBuilder;

internal static class NaiveSearch
{
/// <summary>
/// Finds all occurence of <paramref name="word"/> in <paramref name="text"/>.
/// </summary>
/// <param name="text">The text to look for.</param>
/// <param name="word">The word which should be found in <paramref name="word"/>.</param>
/// <returns>Array of indexes where <paramref name="word"/> was found.</returns>
public static ReadOnlySpan<int> FindAll(ReadOnlySpan<char> text, ReadOnlySpan<char> word)
{
if (text.IsEmpty || word.IsEmpty)
{
return Array.Empty<int>();
}

if (text.Length < word.Length)
{
return Array.Empty<int>();
}

var hits = new TypedSpanList<int>();

for (var i = 0; i < text.Length; i++)
{
for (var j = 0; j < word.Length; j++)
{
if (text[i + j] != word[j])
{
break;
}

if (j == word.Length - 1)
{
hits.Add(i);
}
}
}

return hits.AsSpan;
}
}
46 changes: 46 additions & 0 deletions src/LinkDotNet.StringBuilder/TypedSpanList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Buffers;

namespace LinkDotNet.StringBuilder;

/// <summary>
/// Represents a List based on the <see cref="Span{T}"/> type.
/// </summary>
/// <typeparam name="T">Any struct.</typeparam>
internal ref struct TypedSpanList<T>
where T : struct
{
private Span<T> buffer;
private int count;

/// <summary>
/// Initializes a new instance of the <see cref="TypedSpanList{T}"/> struct.
/// </summary>
public TypedSpanList()
{
buffer = new T[32];
count = 0;
}

public ReadOnlySpan<T> AsSpan => buffer[..count];

public void Add(T value)
{
if (count >= buffer.Length)
{
Grow();
}

buffer[count] = value;
count++;
}

private void Grow(int capacity = 0)
{
var currentSize = buffer.Length;
var newSize = capacity > 0 ? capacity : currentSize * 2;
var rented = ArrayPool<T>.Shared.Rent(newSize);
buffer.CopyTo(rented);
buffer = rented;
ArrayPool<T>.Shared.Return(rented);
}
}
88 changes: 88 additions & 0 deletions src/LinkDotNet.StringBuilder/ValueStringBuilder.Replace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace LinkDotNet.StringBuilder;

public ref partial struct ValueStringBuilder
{
/// <summary>
/// Replaces all instances of one character with another in this builder.
/// </summary>
/// <param name="oldValue">The character to replace.</param>
/// <param name="newValue">The character to replace <paramref name="oldValue"/> with.</param>
public void Replace(char oldValue, char newValue) => Replace(oldValue, newValue, 0, Length);

/// <summary>
/// Replaces all instances of one character with another in this builder.
/// </summary>
/// <param name="oldValue">The character to replace.</param>
/// <param name="newValue">The character to replace <paramref name="oldValue"/> with.</param>
/// <param name="startIndex">The index to start in this builder.</param>
/// <param name="count">The number of characters to read in this builder.</param>
public void Replace(char oldValue, char newValue, int startIndex, int count)
{
if (startIndex < 0)
{
throw new ArgumentException("Start index can't be smaller than 0.", nameof(startIndex));
}

if (count > bufferPosition)
{
throw new ArgumentException($"Count: {count} is bigger than the current size {bufferPosition}.", nameof(count));
}

for (var i = startIndex; i < startIndex + count; i++)
{
if (buffer[i] == oldValue)
{
buffer[i] = newValue;
}
}
}

/// <summary>
/// Replaces all instances of one string with another in this builder.
/// </summary>
/// <param name="oldValue">The string to replace.</param>
/// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
/// <remarks>
/// If <paramref name="newValue"/> is <c>empty</c>, instances of <paramref name="oldValue"/>
/// are removed from this builder.
/// </remarks>
public void Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue)
=> Replace(oldValue, newValue, 0, Length);

/// <summary>
/// Replaces all instances of one string with another in this builder.
/// </summary>
/// <param name="oldValue">The string to replace.</param>
/// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
/// <param name="startIndex">The index to start in this builder.</param>
/// <param name="count">The number of characters to read in this builder.</param>
/// <remarks>
/// If <paramref name="newValue"/> is <c>empty</c>, instances of <paramref name="oldValue"/>
/// are removed from this builder.
/// </remarks>
public void Replace(ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue, int startIndex, int count)
{
var length = startIndex + count;
var slice = buffer[startIndex..length];

// We might want to check whether or not we want to introduce different
// string search algorithms for longer strings.
// I had checked initially with Boyer-Moore but it didn't make that much sense as we
// don't expect very long strings and then the performance is literally the same. So I went with the easier solution.
var hits = NaiveSearch.FindAll(slice, oldValue);

if (hits.IsEmpty)
{
return;
}

var delta = newValue.Length - oldValue.Length;

for (var i = 0; i < hits.Length; i++)
{
var index = startIndex + hits[i] + (delta * i);
Remove(index, oldValue.Length);
Insert(index, newValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
namespace LinkDotNet.StringBuilder.UnitTests;

public class ValueStringBuilderReplaceTests
{
[Fact]
public void ShouldReplaceAllCharacters()
{
var builder = new ValueStringBuilder();
builder.Append("CCCC");

builder.Replace('C', 'B');

builder.ToString().Should().Be("BBBB");
}

[Fact]
public void ShouldReplaceAllCharactersInGivenSpan()
{
var builder = new ValueStringBuilder();
builder.Append("CCCC");

builder.Replace('C', 'B', 1, 2);

builder.ToString().Should().Be("CBBC");
}

[Fact]
public void ShouldReplaceAllText()
{
var builder = new ValueStringBuilder();
builder.Append("Hello World. How are you doing. Hello world examples are always fun.");

builder.Replace("Hello", "Hallöchen");

builder.ToString().Should().Be("Hallöchen World. How are you doing. Hallöchen world examples are always fun.");
}

[Fact]
public void ShouldNotAlterIfNotFound()
{
var builder = new ValueStringBuilder();
builder.Append("Hello");

builder.Replace("Test", "Not");

builder.ToString().Should().Be("Hello");
}

[Fact]
public void ShouldReplaceInSpan()
{
var builder = new ValueStringBuilder();
builder.Append("Hello World. How are you doing. Hello world examples are always fun.");

builder.Replace("Hello", "Hallöchen", 0, 10);

builder.ToString().Should().Be("Hallöchen World. How are you doing. Hello world examples are always fun.");
}
}