Skip to content

Commit

Permalink
Implement Formattable interfaces (#23)
Browse files Browse the repository at this point in the history
* Implement formattable interfaces in TypeIdDecoded

* Implement formattable interfaces in TypeId
  • Loading branch information
firenero authored May 19, 2024
1 parent 41477b7 commit d26055a
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 5 deletions.
25 changes: 22 additions & 3 deletions src/FastIDs.TypeId/TypeId.Core/Base32.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
using System.Text;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;

namespace FastIDs.TypeId;

internal static class Base32
{
public static int Encode(ReadOnlySpan<byte> bytes, Span<char> output)
{
ValidateEncodeParams(bytes, output);

return EncodeImpl(bytes, output, Base32Constants.Alphabet);
}

public static int Encode(ReadOnlySpan<byte> bytes, Span<byte> utf8Output)
{
ValidateEncodeParams(bytes, utf8Output);

return EncodeImpl(bytes, utf8Output, Base32Constants.Utf8Alphabet);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ValidateEncodeParams<TChar>(ReadOnlySpan<byte> bytes, Span<TChar> output) where TChar: struct, IBinaryInteger<TChar>
{
if (bytes.Length != Base32Constants.DecodedLength)
throw new FormatException($"Input must be {Base32Constants.DecodedLength} bytes long.");
if (output.Length < Base32Constants.EncodedLength)
throw new FormatException($"Output must be at least {Base32Constants.EncodedLength} chars long.");

const string alpha = Base32Constants.Alphabet;
}

private static int EncodeImpl<TChar>(ReadOnlySpan<byte> bytes, Span<TChar> output, ReadOnlySpan<TChar> alpha) where TChar: struct, IBinaryInteger<TChar>
{
// 10 byte timestamp
output[0] = alpha[(bytes[0] & 224) >> 5];
output[1] = alpha[bytes[0] & 31];
Expand Down
2 changes: 2 additions & 0 deletions src/FastIDs.TypeId/TypeId.Core/Base32Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace FastIDs.TypeId;
internal static class Base32Constants
{
public const string Alphabet = "0123456789abcdefghjkmnpqrstvwxyz";

public static ReadOnlySpan<byte> Utf8Alphabet => "0123456789abcdefghjkmnpqrstvwxyz"u8;

public static readonly SearchValues<char> AlphabetValues = SearchValues.Create(Alphabet);

Expand Down
42 changes: 41 additions & 1 deletion src/FastIDs.TypeId/TypeId.Core/TypeId.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Unicode;

namespace FastIDs.TypeId;

Expand All @@ -16,7 +17,7 @@ namespace FastIDs.TypeId;
/// </code>
/// </remarks>
[StructLayout(LayoutKind.Auto)]
public readonly struct TypeId : IEquatable<TypeId>
public readonly struct TypeId : IEquatable<TypeId>, ISpanFormattable, IUtf8SpanFormattable
{
private readonly string _str;

Expand Down Expand Up @@ -52,6 +53,45 @@ public bool HasType(ReadOnlySpan<char> type)
/// <returns>A string representation of the TypeId.</returns>
public override string ToString() => _str;

/// <summary>
/// Returns a string representation of the TypeId.
/// </summary>
/// <param name="format">Format string. Can be empty.</param>
/// <param name="formatProvider">Format provider. Can be null.</param>
/// <returns>Formatted string representation of the TypeId.</returns>
/// <remarks>
/// This method ignores <paramref name="format"/> and <paramref name="formatProvider"/> parameters and outputs the same result as <see cref="ToString()"/>.
/// </remarks>
public string ToString(string? format, IFormatProvider? formatProvider) => ToString();

/// <summary>
/// Tries to format the value of the current instance into the provided span of characters.
/// </summary>
/// <param name="destination">The span in which to write this instance's value formatted as a span of characters.</param>
/// <param name="charsWritten">When this method returns, contains the number of characters that were written in <paramref name="destination"/>.</param>
/// <param name="format">A span containing the characters that represent a standard or custom format string. Can be empty.</param>
/// <param name="provider">Format provider. Can be null.</param>
/// <returns><c>true</c> if the formatting was successful; otherwise, <c>false</c>.</returns>
/// <remarks>
/// This method ignores <paramref name="format"/> and <paramref name="provider"/> parameters.
/// </remarks>
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider) =>
destination.TryWrite($"{_str}", out charsWritten);

/// <summary>
/// Tries to format the value of the current instance into the provided span of bytes in UTF-8 encoding.
/// </summary>
/// <param name="utf8Destination">The span in which to write this instance's value formatted as a span of bytes in UTF-8 encoding.</param>
/// <param name="bytesWritten">When this method returns, contains the number of bytes that were written in <paramref name="utf8Destination"/>.</param>
/// <param name="format">A span containing the characters that represent a standard or custom format string. Can be empty.</param>
/// <param name="provider">Format provider. Can be null.</param>
/// <returns><c>true</c> if the formatting was successful; otherwise, <c>false</c>.</returns>
/// <remarks>
/// This method ignores <paramref name="format"/> and <paramref name="provider"/> parameters.
/// </remarks>
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format, IFormatProvider? provider) =>
Utf8.TryWrite(utf8Destination, $"{_str}", out bytesWritten);

/// <summary>
/// A type component of the TypeId.
/// </summary>
Expand Down
88 changes: 87 additions & 1 deletion src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
using System.Runtime.InteropServices;
using System.Text.Unicode;
using UUIDNext;
using UUIDNext.Generator;

namespace FastIDs.TypeId;

[StructLayout(LayoutKind.Auto)]
public readonly struct TypeIdDecoded : IEquatable<TypeIdDecoded>
public readonly struct TypeIdDecoded : IEquatable<TypeIdDecoded>, ISpanFormattable, IUtf8SpanFormattable
{
/// <summary>
/// The type part of the TypeId.
Expand Down Expand Up @@ -49,6 +50,16 @@ public int GetSuffix(Span<char> output)
return Base32.Encode(idBytes, output);
}

public int GetSuffix(Span<byte> utf8Output)
{
Span<byte> idBytes = stackalloc byte[Base32Constants.DecodedLength];
Id.TryWriteBytes(idBytes);

TypeIdParser.FormatUuidBytes(idBytes);

return Base32.Encode(idBytes, utf8Output);
}

/// <summary>
/// Returns the ID generation timestamp.
/// </summary>
Expand Down Expand Up @@ -79,6 +90,10 @@ public DateTimeOffset GetTimestamp()
/// <returns><c>true</c> if the TypeId has the specified type; otherwise, <c>false</c>.</returns>
public bool HasType(ReadOnlySpan<char> type) => type.Equals(Type.AsSpan(), StringComparison.Ordinal);

/// <summary>
/// Returns a string that represents the TypeId value.
/// </summary>
/// <returns>Formatted string.</returns>
public override string ToString()
{
Span<char> suffixChars = stackalloc char[Base32Constants.EncodedLength];
Expand All @@ -89,6 +104,77 @@ public override string ToString()
: suffixChars.ToString();
}

/// <summary>
/// Returns a string that represents the TypeId value.
/// </summary>
/// <param name="format">Format string. Can be empty.</param>
/// <param name="formatProvider">Format provider. Can be null.</param>
/// <returns>Formatted string.</returns>
/// <remarks>
/// This method ignores <paramref name="format"/> and <paramref name="formatProvider"/> parameters and outputs the same result as <see cref="ToString()"/>.
/// </remarks>
public string ToString(string? format, IFormatProvider? formatProvider) => ToString();

/// <summary>
/// Tries to format the value of the current instance into the provided span of characters.
/// </summary>
/// <param name="destination">The span in which to write this instance's value formatted as a span of characters.</param>
/// <param name="charsWritten">When this method returns, contains the number of characters that were written in <paramref name="destination"/>.</param>
/// <param name="format">A span containing the characters that represent a standard or custom format string. Can be empty.</param>
/// <param name="provider">Format provider. Can be null.</param>
/// <returns><c>true</c> if the formatting was successful; otherwise, <c>false</c>.</returns>
/// <remarks>
/// This method ignores <paramref name="format"/> and <paramref name="provider"/> parameters.
/// </remarks>
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
{
charsWritten = 0;
if (Type.Length != 0)
{
if (!destination.TryWrite($"{Type}_", out charsWritten))
return false;
}

var suffixSpan = destination[charsWritten..];
if (suffixSpan.Length < Base32Constants.EncodedLength)
return false;

var suffixCharsWritten = GetSuffix(suffixSpan);
charsWritten += suffixCharsWritten;

return true;
}

/// <summary>
/// Tries to format the value of the current instance into the provided span of bytes in UTF-8 encoding.
/// </summary>
/// <param name="utf8Destination">The span in which to write this instance's value formatted as a span of bytes in UTF-8 encoding.</param>
/// <param name="bytesWritten">When this method returns, contains the number of bytes that were written in <paramref name="utf8Destination"/>.</param>
/// <param name="format">A span containing the characters that represent a standard or custom format string. Can be empty.</param>
/// <param name="provider">Format provider. Can be null.</param>
/// <returns><c>true</c> if the formatting was successful; otherwise, <c>false</c>.</returns>
/// <remarks>
/// This method ignores <paramref name="format"/> and <paramref name="provider"/> parameters.
/// </remarks>
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
{
bytesWritten = 0;
if (Type.Length != 0)
{
if (!Utf8.TryWrite(utf8Destination, $"{Type}_", out bytesWritten))
return false;
}

var suffixSpan = utf8Destination[bytesWritten..];
if (suffixSpan.Length < Base32Constants.EncodedLength)
return false;

var suffixBytesWritten = GetSuffix(suffixSpan);
bytesWritten += suffixBytesWritten;

return true;
}

public bool Equals(TypeIdDecoded other) => Type == other.Type && Id.Equals(other.Id);

public override bool Equals(object? obj) => obj is TypeIdDecoded other && Equals(other);
Expand Down
77 changes: 77 additions & 0 deletions src/FastIDs.TypeId/TypeId.Tests/TypeIdTests/FormattingTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Linq;
using System.Text;
using System.Text.Unicode;
using FastIDs.TypeId;
using FluentAssertions;

Expand Down Expand Up @@ -36,6 +38,36 @@ public void Encoded_ToString(string typeIdStr, Guid expectedGuid, string expecte

typeId.ToString().Should().Be(typeIdStr);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Encoded_ToStringFormat(string typeIdStr, Guid expectedGuid, string expectedType)
{
var typeId = TypeId.Parse(typeIdStr);

typeId.ToString("", null).Should().Be(typeIdStr);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Encoded_TryFormat(string typeIdStr, Guid expectedGuid, string expectedType)
{
var typeId = TypeId.Parse(typeIdStr);

Span<char> formattedTypeId = stackalloc char[typeIdStr.Length + 10];
typeId.TryFormat(formattedTypeId, out var charsWritten, "", null).Should().BeTrue();

formattedTypeId[..charsWritten].ToString().Should().Be(typeIdStr);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Encoded_TryFormatUtf8(string typeIdStr, Guid expectedGuid, string expectedType)
{
var typeId = TypeId.Parse(typeIdStr);

Span<byte> formattedTypeId = stackalloc byte[typeIdStr.Length + 10];
typeId.TryFormat(formattedTypeId, out var bytesWritten, "", null).Should().BeTrue();

Encoding.UTF8.GetString(formattedTypeId[..bytesWritten]).Should().Be(typeIdStr);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Decoded_GetSuffix(string typeIdStr, Guid expectedGuid, string expectedType)
Expand All @@ -61,11 +93,56 @@ public void Decoded_GetSuffixSpan(string typeIdStr, Guid expectedGuid, string ex
suffix[..charsWritten].ToString().Should().Be(expectedSuffix);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Decoded_GetSuffixUtf8Span(string typeIdStr, Guid expectedGuid, string expectedType)
{
var decoded = TypeId.FromUuidV7(expectedType, expectedGuid);

var expectedSuffix = typeIdStr.Split('_')[^1];

Span<byte> utf8Suffix = stackalloc byte[expectedSuffix.Length + 10];
var bytesWritten = decoded.GetSuffix(utf8Suffix);

bytesWritten.Should().Be(expectedSuffix.Length);
var suffix = Encoding.UTF8.GetString(utf8Suffix[..bytesWritten]);
suffix.Should().Be(expectedSuffix);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Decoded_ToString(string typeIdStr, Guid expectedGuid, string expectedType)
{
var decoded = TypeId.FromUuidV7(expectedType, expectedGuid);

decoded.ToString().Should().Be(typeIdStr);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Decoded_ToStringFormat(string typeIdStr, Guid expectedGuid, string expectedType)
{
var decoded = TypeId.FromUuidV7(expectedType, expectedGuid);

decoded.ToString("", null).Should().Be(typeIdStr);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Decoded_TryFormat(string typeIdStr, Guid expectedGuid, string expectedType)
{
var decoded = TypeId.FromUuidV7(expectedType, expectedGuid);

Span<char> formattedTypeId = stackalloc char[typeIdStr.Length + 10];
decoded.TryFormat(formattedTypeId, out var charsWritten, "", null).Should().BeTrue();

formattedTypeId[..charsWritten].ToString().Should().Be(typeIdStr);
}

[TestCaseSource(typeof(TestCases), nameof(TestCases.ValidIds))]
public void Decoded_TryFormatUtf8(string typeIdStr, Guid expectedGuid, string expectedType)
{
var decoded = TypeId.FromUuidV7(expectedType, expectedGuid);

Span<byte> formattedTypeId = stackalloc byte[typeIdStr.Length + 10];
decoded.TryFormat(formattedTypeId, out var bytesWritten, "", null).Should().BeTrue();

Encoding.UTF8.GetString(formattedTypeId[..bytesWritten]).Should().Be(typeIdStr);
}
}

0 comments on commit d26055a

Please sign in to comment.