diff --git a/src/FastIDs.TypeId/TypeId.Core/Base32.cs b/src/FastIDs.TypeId/TypeId.Core/Base32.cs index 883783b..f4af985 100644 --- a/src/FastIDs.TypeId/TypeId.Core/Base32.cs +++ b/src/FastIDs.TypeId/TypeId.Core/Base32.cs @@ -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 bytes, Span output) + { + ValidateEncodeParams(bytes, output); + + return EncodeImpl(bytes, output, Base32Constants.Alphabet); + } + + public static int Encode(ReadOnlySpan bytes, Span utf8Output) + { + ValidateEncodeParams(bytes, utf8Output); + + return EncodeImpl(bytes, utf8Output, Base32Constants.Utf8Alphabet); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ValidateEncodeParams(ReadOnlySpan bytes, Span output) where TChar: struct, IBinaryInteger { 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(ReadOnlySpan bytes, Span output, ReadOnlySpan alpha) where TChar: struct, IBinaryInteger + { // 10 byte timestamp output[0] = alpha[(bytes[0] & 224) >> 5]; output[1] = alpha[bytes[0] & 31]; diff --git a/src/FastIDs.TypeId/TypeId.Core/Base32Constants.cs b/src/FastIDs.TypeId/TypeId.Core/Base32Constants.cs index a031784..124eb10 100644 --- a/src/FastIDs.TypeId/TypeId.Core/Base32Constants.cs +++ b/src/FastIDs.TypeId/TypeId.Core/Base32Constants.cs @@ -5,6 +5,8 @@ namespace FastIDs.TypeId; internal static class Base32Constants { public const string Alphabet = "0123456789abcdefghjkmnpqrstvwxyz"; + + public static ReadOnlySpan Utf8Alphabet => "0123456789abcdefghjkmnpqrstvwxyz"u8; public static readonly SearchValues AlphabetValues = SearchValues.Create(Alphabet); diff --git a/src/FastIDs.TypeId/TypeId.Core/TypeId.cs b/src/FastIDs.TypeId/TypeId.Core/TypeId.cs index 257e30d..bd9a0ce 100644 --- a/src/FastIDs.TypeId/TypeId.Core/TypeId.cs +++ b/src/FastIDs.TypeId/TypeId.Core/TypeId.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Text.Unicode; namespace FastIDs.TypeId; @@ -16,7 +17,7 @@ namespace FastIDs.TypeId; /// /// [StructLayout(LayoutKind.Auto)] -public readonly struct TypeId : IEquatable +public readonly struct TypeId : IEquatable, ISpanFormattable, IUtf8SpanFormattable { private readonly string _str; @@ -52,6 +53,45 @@ public bool HasType(ReadOnlySpan type) /// A string representation of the TypeId. public override string ToString() => _str; + /// + /// Returns a string representation of the TypeId. + /// + /// Format string. Can be empty. + /// Format provider. Can be null. + /// Formatted string representation of the TypeId. + /// + /// This method ignores and parameters and outputs the same result as . + /// + public string ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + /// The span in which to write this instance's value formatted as a span of characters. + /// When this method returns, contains the number of characters that were written in . + /// A span containing the characters that represent a standard or custom format string. Can be empty. + /// Format provider. Can be null. + /// true if the formatting was successful; otherwise, false. + /// + /// This method ignores and parameters. + /// + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => + destination.TryWrite($"{_str}", out charsWritten); + + /// + /// Tries to format the value of the current instance into the provided span of bytes in UTF-8 encoding. + /// + /// The span in which to write this instance's value formatted as a span of bytes in UTF-8 encoding. + /// When this method returns, contains the number of bytes that were written in . + /// A span containing the characters that represent a standard or custom format string. Can be empty. + /// Format provider. Can be null. + /// true if the formatting was successful; otherwise, false. + /// + /// This method ignores and parameters. + /// + public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan format, IFormatProvider? provider) => + Utf8.TryWrite(utf8Destination, $"{_str}", out bytesWritten); + /// /// A type component of the TypeId. /// diff --git a/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs b/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs index 044bb8e..630e613 100644 --- a/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs +++ b/src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs @@ -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 +public readonly struct TypeIdDecoded : IEquatable, ISpanFormattable, IUtf8SpanFormattable { /// /// The type part of the TypeId. @@ -49,6 +50,16 @@ public int GetSuffix(Span output) return Base32.Encode(idBytes, output); } + public int GetSuffix(Span utf8Output) + { + Span idBytes = stackalloc byte[Base32Constants.DecodedLength]; + Id.TryWriteBytes(idBytes); + + TypeIdParser.FormatUuidBytes(idBytes); + + return Base32.Encode(idBytes, utf8Output); + } + /// /// Returns the ID generation timestamp. /// @@ -79,6 +90,10 @@ public DateTimeOffset GetTimestamp() /// true if the TypeId has the specified type; otherwise, false. public bool HasType(ReadOnlySpan type) => type.Equals(Type.AsSpan(), StringComparison.Ordinal); + /// + /// Returns a string that represents the TypeId value. + /// + /// Formatted string. public override string ToString() { Span suffixChars = stackalloc char[Base32Constants.EncodedLength]; @@ -89,6 +104,77 @@ public override string ToString() : suffixChars.ToString(); } + /// + /// Returns a string that represents the TypeId value. + /// + /// Format string. Can be empty. + /// Format provider. Can be null. + /// Formatted string. + /// + /// This method ignores and parameters and outputs the same result as . + /// + public string ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + /// Tries to format the value of the current instance into the provided span of characters. + /// + /// The span in which to write this instance's value formatted as a span of characters. + /// When this method returns, contains the number of characters that were written in . + /// A span containing the characters that represent a standard or custom format string. Can be empty. + /// Format provider. Can be null. + /// true if the formatting was successful; otherwise, false. + /// + /// This method ignores and parameters. + /// + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan 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; + } + + /// + /// Tries to format the value of the current instance into the provided span of bytes in UTF-8 encoding. + /// + /// The span in which to write this instance's value formatted as a span of bytes in UTF-8 encoding. + /// When this method returns, contains the number of bytes that were written in . + /// A span containing the characters that represent a standard or custom format string. Can be empty. + /// Format provider. Can be null. + /// true if the formatting was successful; otherwise, false. + /// + /// This method ignores and parameters. + /// + public bool TryFormat(Span utf8Destination, out int bytesWritten, ReadOnlySpan 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); diff --git a/src/FastIDs.TypeId/TypeId.Tests/TypeIdTests/FormattingTests.cs b/src/FastIDs.TypeId/TypeId.Tests/TypeIdTests/FormattingTests.cs index 0eaa85f..0094ae5 100644 --- a/src/FastIDs.TypeId/TypeId.Tests/TypeIdTests/FormattingTests.cs +++ b/src/FastIDs.TypeId/TypeId.Tests/TypeIdTests/FormattingTests.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using System.Text; +using System.Text.Unicode; using FastIDs.TypeId; using FluentAssertions; @@ -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 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 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) @@ -61,6 +93,21 @@ 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 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) { @@ -68,4 +115,34 @@ public void Decoded_ToString(string typeIdStr, Guid expectedGuid, string expecte 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 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 formattedTypeId = stackalloc byte[typeIdStr.Length + 10]; + decoded.TryFormat(formattedTypeId, out var bytesWritten, "", null).Should().BeTrue(); + + Encoding.UTF8.GetString(formattedTypeId[..bytesWritten]).Should().Be(typeIdStr); + } } \ No newline at end of file