Skip to content

Commit

Permalink
Add SpanWriter and RunLengthEncoder
Browse files Browse the repository at this point in the history
Add SpanWriter and RunLengthEncoder. Tweak SpanReader API.
  • Loading branch information
JeremyKuhne committed Jan 23, 2024
1 parent d355c29 commit bfa71b1
Show file tree
Hide file tree
Showing 6 changed files with 415 additions and 25 deletions.
97 changes: 97 additions & 0 deletions src/thirtytwo/Support/RunLengthEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Jeremy W. Kuhne. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Windows.Support;

/// <summary>
/// Simple run length encoder (RLE) that works on spans.
/// </summary>
/// <remarks>
/// <para>
/// Format used is a byte for the count, followed by a byte for the value.
/// </para>
/// </remarks>
internal static class RunLengthEncoder
{
/// <summary>
/// Get the encoded length, in bytes, of the given data.
/// </summary>
public static int GetEncodedLength(ReadOnlySpan<byte> data)
{
SpanReader<byte> reader = new(data);

int length = 0;
while (reader.TryRead(out byte value))
{
int count = reader.AdvancePast(value) + 1;
while (count > 0)
{
// 1 byte for the count, 1 byte for the value
length += 2;
count -= 0xFF;
}
}

return length;
}

/// <summary>
/// Get the decoded length, in bytes, of the given encoded data.
/// </summary>
public static int GetDecodedLength(ReadOnlySpan<byte> encoded)
{
int length = 0;
for (int i = 0; i < encoded.Length; i += 2)
{
length += encoded[i];
}

return length;
}

/// <summary>
/// Encode the given data into the given <paramref name="encoded"/> span.
/// </summary>
/// <returns>
/// <see langword="false"/> if the <paramref name="encoded"/> span was not large enough to hold the encoded data.
/// </returns>
public static bool TryEncode(ReadOnlySpan<byte> data, Span<byte> encoded, out int written)
{
SpanReader<byte> reader = new(data);
SpanWriter<byte> writer = new(encoded);

while (reader.TryRead(out byte value))
{
int count = reader.AdvancePast(value) + 1;
while (count > 0)
{
if (!writer.TryWrite((byte)Math.Min(count, 0xFF)) || !writer.TryWrite(value))
{
written = writer.Position;
return false;
}

count -= 0xFF;
}
}

written = writer.Position;
return true;
}

public static bool TryDecode(ReadOnlySpan<byte> encoded, Span<byte> data)
{
SpanReader<byte> reader = new(encoded);
SpanWriter<byte> writer = new(data);

while (reader.TryRead(out byte count))
{
if (!reader.TryRead(out byte value) || !writer.TryWrite(count, value))
{
return false;
}
}

return true;
}
}
51 changes: 50 additions & 1 deletion src/thirtytwo/Support/SpanReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ public unsafe ref struct SpanReader<T>(ReadOnlySpan<T> span) where T : unmanaged
{
private ReadOnlySpan<T> _unread = span;
public ReadOnlySpan<T> Span { get; } = span;
public readonly ReadOnlySpan<T> UnreadSpan => _unread;

public int Position
{
readonly get => Span.Length - _unread.Length;
set => _unread = Span[value..];
}

public readonly int Length => Span.Length;

/// <summary>
/// Try to read everything up to the given <paramref name="delimiter"/>. Advances the reader past the
Expand Down Expand Up @@ -54,6 +61,7 @@ public bool TryReadTo(T delimiter, bool advancePastDelimiter, out ReadOnlySpan<T
index++;
}

// Advance unread
UncheckedSlice(ref _unread, index, _unread.Length - index);
}
}
Expand Down Expand Up @@ -176,6 +184,47 @@ public bool TryRead<TValue>(int count, out ReadOnlySpan<TValue> value) where TVa
return success;
}

/// <summary>
/// Advance the reader if the given <paramref name="next"/> values are next.
/// </summary>
/// <param name="next">The span to compare the next items to.</param>
/// <returns><see langword="true"/> if the values were found and the reader advanced.</returns>
public bool TryAdvancePast(ReadOnlySpan<T> next)
{
bool success = false;
if (_unread.StartsWith(next))
{
UnsafeAdvance(next.Length);
success = true;
}

return success;
}

/// <summary>
/// Advance the reader past consecutive instances of the given <paramref name="value"/>.
/// </summary>
/// <returns>How many positions the reader has been advanced</returns>
public int AdvancePast(T value)
{
int count = 0;

int index = _unread.IndexOfAnyExcept(value);
if (index == -1)
{
// Everything left is the value
count = _unread.Length;
_unread = default;
}
else if (index != 0)
{
count = index;
UnsafeAdvance(index);
}

return count;
}

/// <summary>
/// Advance the reader by the given <paramref name="count"/>.
/// </summary>
Expand Down
112 changes: 112 additions & 0 deletions src/thirtytwo/Support/SpanWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) Jeremy W. Kuhne. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Windows.Support;

/// <summary>
/// Fast stack based <see cref="Span{T}"/> writer.
/// </summary>
internal unsafe ref struct SpanWriter<T>(Span<T> span) where T : unmanaged, IEquatable<T>
{
private Span<T> _unwritten = span;
public Span<T> Span { get; } = span;

public int Position
{
readonly get => Span.Length - _unwritten.Length;
set => _unwritten = Span[value..];
}

public readonly int Length => Span.Length;

/// <summary>
/// Try to write the given value.
/// </summary>
public bool TryWrite(T value)
{
bool success = false;

if (!_unwritten.IsEmpty)
{
success = true;
_unwritten[0] = value;
UnsafeAdvance(1);
}

return success;
}

/// <summary>
/// Try to write the given value.
/// </summary>
public bool TryWrite(ReadOnlySpan<T> values)
{
bool success = false;

if (_unwritten.Length >= values.Length)
{
success = true;
values.CopyTo(_unwritten);
UnsafeAdvance(values.Length);
}

return success;
}

/// <summary>
/// Try to write the given value <paramref name="count"/> times.
/// </summary>
public bool TryWrite(int count, T value)
{
bool success = false;

if (_unwritten.Length >= count)
{
success = true;
_unwritten[..count].Fill(value);
UnsafeAdvance(count);
}

return success;
}

/// <summary>
/// Advance the writer by the given <paramref name="count"/>.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Advance(int count) => _unwritten = _unwritten[count..];

/// <summary>
/// Rewind the writer by the given <paramref name="count"/>.
/// </summary>
public void Rewind(int count) => _unwritten = Span[(Span.Length - _unwritten.Length - count)..];

/// <summary>
/// Reset the reader to the beginning of the span.
/// </summary>
public void Reset() => _unwritten = Span;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UnsafeAdvance(int count)
{
Debug.Assert((uint)count <= (uint)_unwritten.Length);
UncheckedSlice(ref _unwritten, count, _unwritten.Length - count);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UncheckedSliceTo(ref Span<T> span, int length)
{
Debug.Assert((uint)length <= (uint)span.Length);
span = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(span), length);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UncheckedSlice(ref Span<T> span, int start, int length)
{
Debug.Assert((uint)start <= (uint)span.Length && (uint)length <= (uint)(span.Length - start));
span = MemoryMarshal.CreateSpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)start), length);
}
}
44 changes: 44 additions & 0 deletions src/thirtytwo_tests/Support/RunLengthEncoderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Jeremy W. Kuhne. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Windows.Support;

public class RunLengthEncoderTests
{
[Fact]
public void RunLengthEncoder_TryEncode()
{
ReadOnlySpan<byte> data = [1, 1, 1, 2, 2, 3, 3, 3, 3];
Span<byte> encoded = new byte[RunLengthEncoder.GetEncodedLength(data)];
RunLengthEncoder.TryEncode(data, encoded, out int written).Should().BeTrue();
written.Should().Be(6);
encoded.ToArray().Should().BeEquivalentTo([3, 1, 2, 2, 4, 3]);
}

[Theory]
[InlineData(0, 0)]
[InlineData(1, 2)]
[InlineData(255, 2)]
[InlineData(256, 4)]
public void RunLengthEncoder_GetEncodedLength(int count, int expectedLength)
{
Span<byte> data = new byte[count];
Span<byte> encoded = new byte[RunLengthEncoder.GetEncodedLength(data)];
RunLengthEncoder.TryEncode(data, encoded, out int written).Should().BeTrue();
written.Should().Be(expectedLength);
encoded.Length.Should().Be(expectedLength);
}

[Fact]
public void RunLengthEncoder_RoundTrip()
{
ReadOnlySpan<byte> data = [1, 1, 1, 2, 2, 3, 3, 3, 3];
Span<byte> encoded = new byte[RunLengthEncoder.GetEncodedLength(data)];
RunLengthEncoder.TryEncode(data, encoded, out int written).Should().BeTrue();
written.Should().Be(6);

Span<byte> decoded = new byte[RunLengthEncoder.GetDecodedLength(encoded)];
RunLengthEncoder.TryDecode(encoded, decoded).Should().BeTrue();
decoded.ToArray().Should().BeEquivalentTo(data.ToArray());
}
}
Loading

0 comments on commit bfa71b1

Please sign in to comment.