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

Support underscores in type to comply with spec v0.3 #19

Merged
merged 2 commits into from
Apr 15, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class TypeAlphabetValidationBenchmarks

private string[] _prefixes = [];

private const string AlphabetStr = "abcdefghijklmnopqrstuvwxyz";
private const string AlphabetStr = "_abcdefghijklmnopqrstuvwxyz";
private readonly SearchValues<char> _searchValues = SearchValues.Create(AlphabetStr);
private const int UnrollValue = 4;

Expand All @@ -33,10 +33,13 @@ public void Setup()
for (var i = 0; i < count; i++)
{
sb.Clear();

for (var j = 0; j < PrefixLength; j++)
{
sb.Append((char)random.Next('a', 'z'));
if (j == PrefixLength / 2)
sb.Append('_');
else
sb.Append((char)random.Next('a', 'z'));
}

_prefixes[i] = sb.ToString();
Expand All @@ -51,7 +54,7 @@ public bool CharCheck()
{
foreach (var c in prefix.AsSpan())
{
isValid &= c is >= 'a' and <= 'z';
isValid &= c is '_' or >= 'a' and <= 'z';
}
}

Expand All @@ -66,7 +69,7 @@ public bool CharCheckAscii()
{
foreach (var c in prefix.AsSpan())
{
isValid &= char.IsAsciiLetterLower(c);
isValid &= char.IsAsciiLetterLower(c) || c == '_';
}
}

Expand All @@ -89,40 +92,25 @@ public bool SearchValuesCheck()
}

[Benchmark]
public bool SearchValuesInRangeCheck()
public bool SearchValuesContainsAny()
{
var isValid = false;
foreach (var prefix in _prefixes)
{
isValid &= !prefix.AsSpan().ContainsAnyExceptInRange('a', 'z');
isValid &= prefix.AsSpan().ContainsAnyExcept(_searchValues);
}

return isValid;
}

[Benchmark]
public bool Simd()
public bool InRangeCheck()
{
var isValid = false;
foreach (var prefix in _prefixes)
{
var lower = new Vector<short>((short)'a');
var higher = new Vector<short>((short)'z');

var shorts = MemoryMarshal.Cast<char, short>(prefix.AsSpan());

for (var j = 0; j < shorts.Length; j += Vector<short>.Count)
{
var span = Vector<short>.Count < shorts.Length - j
? shorts.Slice(j, Vector<short>.Count)
: shorts[^Vector<short>.Count..];

var curVector = new Vector<short>(span);

var isGreater = Vector.GreaterThanOrEqualAll(curVector, lower);
var isLower = Vector.LessThanOrEqualAll(curVector, higher);
isValid &= isGreater && isLower;
}
var span = prefix.AsSpan();
isValid &= !span.ContainsAnyExceptInRange('_', 'z') && !span.Contains('`');
}

return isValid;
Expand Down
17 changes: 9 additions & 8 deletions src/FastIDs.TypeId/TypeId.Core/TypeId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,15 +150,16 @@ public TypeIdDecoded Decode()
/// </remarks>
public static TypeId Parse(string input)
{
var separatorIdx = input.IndexOf('_');
var separatorIdx = input.LastIndexOf('_');
if (separatorIdx == 0)
throw new FormatException("Type separator must be omitted if there is no type present.");
if (separatorIdx > TypeIdConstants.MaxTypeLength)
throw new FormatException($"Type can be at most {TypeIdConstants.MaxTypeLength} characters long.");

var typeSpan = separatorIdx != -1 ? input.AsSpan(0, separatorIdx) : ReadOnlySpan<char>.Empty;
if (!TypeIdParser.ValidateTypeAlphabet(typeSpan))
throw new FormatException("Type must contain only lowercase letters.");
var typeError = TypeIdParser.ValidateType(typeSpan);
if (typeError is not TypeIdParser.TypeError.None)
throw new FormatException(typeError.ToErrorMessage());

var idSpan = input.AsSpan(separatorIdx + 1);
if (idSpan.Length != TypeIdConstants.IdLength)
Expand All @@ -169,7 +170,7 @@ public static TypeId Parse(string input)
if (!Base32.IsValid(idSpan))
throw new FormatException("Id is not a valid Base32 string.");

return new TypeId(input);
return new(input);
}

/// <summary>
Expand All @@ -188,12 +189,12 @@ public static TypeId Parse(string input)
/// </remarks>
public static bool TryParse(string input, out TypeId result)
{
var separatorIdx = input.IndexOf('_');
var separatorIdx = input.LastIndexOf('_');
if (separatorIdx is 0 or > TypeIdConstants.MaxTypeLength)
return Error(out result);

var typeSpan = separatorIdx != -1 ? input.AsSpan(0, separatorIdx) : ReadOnlySpan<char>.Empty;
if (!TypeIdParser.ValidateTypeAlphabet(typeSpan))
if (TypeIdParser.ValidateType(typeSpan) is not TypeIdParser.TypeError.None)
return Error(out result);

var idSpan = input.AsSpan(separatorIdx + 1);
Expand All @@ -203,7 +204,7 @@ public static bool TryParse(string input, out TypeId result)
if (!Base32.IsValid(idSpan))
return Error(out result);

result = new TypeId(input);
result = new(input);
return true;
}

Expand Down
5 changes: 3 additions & 2 deletions src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ public static TypeIdDecoded FromUuidV7(string type, Guid uuidV7)
{
if (type.Length > TypeIdConstants.MaxTypeLength)
throw new FormatException($"Type can be at most {TypeIdConstants.MaxTypeLength} characters long.");
if (!TypeIdParser.ValidateTypeAlphabet(type))
throw new FormatException("Type must contain only lowercase letters.");
var typeError = TypeIdParser.ValidateType(type);
if (typeError is not TypeIdParser.TypeError.None)
throw new FormatException(typeError.ToErrorMessage());

return new TypeIdDecoded(type, uuidV7);
}
Expand Down
40 changes: 34 additions & 6 deletions src/FastIDs.TypeId/TypeId.Core/TypeIdParser.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand All @@ -7,6 +8,7 @@ namespace FastIDs.TypeId;

internal static class TypeIdParser
{
private static readonly SearchValues<char> Alphabet = SearchValues.Create("_abcdefghijklmnopqrstuvwxyz");
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FormatUuidBytes(Span<byte> bytes)
{
Expand All @@ -22,21 +24,47 @@ public static void FormatUuidBytes(Span<byte> bytes)
(bytes[6], bytes[7]) = (bytes[7], bytes[6]);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool ValidateTypeAlphabet(ReadOnlySpan<char> type)
public static TypeError ValidateType(ReadOnlySpan<char> type)
{
if (type.Length == 0)
return TypeError.None;

if (type[0] == '_')
return TypeError.StartsWithUnderscore;
if (type[^1] == '_')
return TypeError.EndsWithUnderscore;

// Vectorized version is faster for strings with length >= 8.
const int vectorizedThreshold = 8;
if (Vector128.IsHardwareAccelerated && type.Length >= vectorizedThreshold)
return !type.ContainsAnyExceptInRange('a', 'z');
return type.ContainsAnyExcept(Alphabet) ? TypeError.InvalidChar : TypeError.None;

// Fallback to scalar version for strings with length < 8 or when hardware intrinsics are not available.
foreach (var c in type)
{
if (!char.IsAsciiLetterLower(c))
return false;
var isValidChar = c is >= 'a' and <= 'z' or '_';
if (!isValidChar)
return TypeError.InvalidChar;
}

return true;
return TypeError.None;
}

public enum TypeError
{
None,
StartsWithUnderscore,
EndsWithUnderscore,
InvalidChar,
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ToErrorMessage(this TypeError error) => error switch
{
TypeError.None => "",
TypeError.StartsWithUnderscore => "Type can't start with an underscore.",
TypeError.EndsWithUnderscore => "Type can't end with an underscore.",
TypeError.InvalidChar => "Type must contain only lowercase letters and underscores.",
_ => "Unknown type error."
};
}
34 changes: 0 additions & 34 deletions src/FastIDs.TypeId/TypeId.Tests/TypeIdTests/FormattingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,38 +68,4 @@ public void Decoded_ToString(string typeIdStr, Guid expectedGuid, string expecte

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

// [TestCaseSource(typeof(TestCases),nameof(TestCases.WithPrefix))]
// public void Encoded_WithPrefix_Suffix(string typeIdStr, Guid expectedGuid, string expectedType)
// {
// var typeId = TypeId.Parse(typeIdStr);
//
// var suffix = typeId.Suffix.ToString();
//
// var expectedSuffix = typeIdStr.Split('_')[1];
// suffix.Should().Be(expectedSuffix);
// }
//
// [TestCaseSource(typeof(TestCases), nameof(TestCases.NoPrefix))]
// public void Encoded_NoPrefix_Suffix(string typeIdStr, Guid expectedGuid, string expectedType)
// {
// var typeId = TypeId.Parse(typeIdStr);
//
// var suffix = typeId.Suffix.ToString();
//
// suffix.Should().Be(typeIdStr);
// }

// [TestCaseSource(nameof(ToStringTestCases))]
// public void ToString_StringReturned(string typeIdString)
// {
// var typeId = TypeId.Parse(typeIdString);
//
// typeId.ToString().Should().Be(typeIdString);
// }
//
// private static TestCaseData[] ToStringTestCases => TestCases.WithPrefix
// .Concat(TestCases.NoPrefix)
// .Select(x => new TestCaseData(x.OriginalArguments[0]))
// .ToArray();
}
30 changes: 18 additions & 12 deletions src/FastIDs.TypeId/TypeId.Tests/TypeIdTests/GenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ public void FromUuidV7_TypeIdCreated(string typeIdStr, Guid uuidV7, string prefi
typeId.ToString().Should().Be(typeIdStr);
}

[TestCase("prefix")]
[TestCase("type")]
[TestCase("")]
[TestCaseSource(nameof(ValidTypes))]
public void New_WithType_TypeIdCreated(string type)
{
var typeId = TypeId.New(type);
Expand All @@ -40,13 +38,21 @@ public void FromUuidV7_IncorrectType_FormatExceptionThrown(string type)
act.Should().Throw<FormatException>();
}

private static TestCaseData[] InvalidTypes => new[]
{
new TestCaseData("PREFIX") { TestName = "Type can't have any uppercase letters" },
new TestCaseData("pre_fix") { TestName = "Type can't have any underscores" },
new TestCaseData("pre.fix") { TestName = "Type can't have any special characters" },
new TestCaseData("pre fix") { TestName = "Type can't have any spaces" },
new TestCaseData("préfix") { TestName = "Type can't have any non-ASCII characters" },
new TestCaseData("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl") { TestName = "Type can't have have more than 63 characters" },
};
private static TestCaseData[] ValidTypes =>
[
new("prefix") { TestName = "Lowercase letters type" },
new("pre_fix") { TestName = "Lowercase letters with underscore type" },
new("") { TestName = "Empty type" },
];

private static TestCaseData[] InvalidTypes =>
[
new("PREFIX") { TestName = "Type can't have any uppercase letters" },
new("pre.fix") { TestName = "Type can't have any special characters" },
new("pre fix") { TestName = "Type can't have any spaces" },
new("préfix") { TestName = "Type can't have any non-ASCII characters" },
new("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl") { TestName = "Type can't have have more than 63 characters" },
new("_prefix") { TestName = "The prefix can't start with an underscore" },
new("prefix_") { TestName = "The prefix can't end with an underscore" },
];
}
56 changes: 0 additions & 56 deletions src/FastIDs.TypeId/TypeId.Tests/TypeIdTests/ParsingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,60 +41,4 @@ public void TryParse_InvalidId_ReturnsFalse(string typeIdStr)
canParse.Should().BeFalse();
typeId.Should().Be((TypeId)default);
}

// [TestCaseSource(typeof(TestCases), nameof(TestCases.NoPrefix))]
// public void Parse_NoPrefixTypeId_Parsed(string typeIdStr, Guid _)
// {
// var typeId = TypeId.Parse(typeIdStr);
//
// typeId.Type.ToString().Should().BeEmpty();
// typeId.Suffix.ToString().Should().Be(typeIdStr);
// typeId.ToString().Should().Be(typeIdStr);
// }
//
// [TestCaseSource(typeof(TestCases), nameof(TestCases.WithPrefix))]
// public void Parse_WithPrefixTypeId_Parsed(string typeIdStr, Guid expectedGuid, string expectedType)
// {
// var typeId = TypeId.Parse(typeIdStr);
//
// typeId.Type.ToString().Should().Be(expectedType);
// typeId..Should().Be(expectedGuid);
// }
//
// [TestCaseSource(typeof(TestCases), nameof(TestCases.InvalidIds))]
// public void Parse_InvalidTypeId_ThrowsFormatException(string typeIdStr)
// {
// Action act = () => TypeId.Parse(typeIdStr);
//
// act.Should().Throw<FormatException>();
// }
//
// [TestCaseSource(typeof(TestCases), nameof(TestCases.NoPrefix))]
// public void TryParse_NoPrefixTypeId_Parsed(string typeIdStr, Guid expectedGuid)
// {
// var canParse = TypeId.TryParse(typeIdStr, out var typeId);
//
// canParse.Should().BeTrue();
// typeId.Type.Should().BeEmpty();
// typeId.Id.Should().Be(expectedGuid);
// }
//
// [TestCaseSource(typeof(TestCases), nameof(TestCases.WithPrefix))]
// public void TryParse_WithPrefixTypeId_Parsed(string typeIdStr, Guid expectedGuid, string expectedType)
// {
// var canParse = TypeId.TryParse(typeIdStr, out var typeId);
//
// canParse.Should().BeTrue();
// typeId.Type.Should().Be(expectedType);
// typeId.Id.Should().Be(expectedGuid);
// }
//
// [TestCaseSource(typeof(TestCases), nameof(TestCases.InvalidIds))]
// public void TryParse_InvalidTypeId_ReturnsFalse(string typeIdStr)
// {
// var canParse = TypeId.TryParse(typeIdStr, out var typeId);
//
// canParse.Should().BeFalse();
// typeId.Should().Be(default(TypeId));
// }
}
Loading
Loading