base32 refactoring
.NET Test / .NET tests (push) Failing after 1m54s

This commit is contained in:
2026-07-10 21:53:37 +04:00
parent d11c74e5d6
commit 566c813e8d
18 changed files with 375 additions and 88 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Setup .NET - name: Setup .NET
uses: https://github.com/actions/setup-dotnet@v4 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 10.x dotnet-version: 10.x
+2 -1
View File
@@ -6,6 +6,7 @@ on:
tags-ignore: tags-ignore:
- '**' - '**'
paths-ignore: paths-ignore:
- 'LICENSE'
- 'README.md' - 'README.md'
- '.gitea/workflows/publish-*.yaml' - '.gitea/workflows/publish-*.yaml'
pull_request: pull_request:
@@ -31,7 +32,7 @@ jobs:
10.0.x 10.0.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore --disable-parallel
- name: Build .NET 10.0 - name: Build .NET 10.0
run: dotnet build --no-restore --framework net10.0 --configuration Release ./Core.Tests/Core.Tests.csproj run: dotnet build --no-restore --framework net10.0 --configuration Release ./Core.Tests/Core.Tests.csproj
+4 -2
View File
@@ -1,5 +1,7 @@
{ {
"dotnet.defaultSolution": "JustDotNet.Core.sln", "dotnet.defaultSolution": "JustDotNet.Core.slnx",
"omnisharp.enableEditorConfigSupport": true,
"dotnetAcquisitionExtension.enableTelemetry": false, "dotnetAcquisitionExtension.enableTelemetry": false,
"dotnet.testWindow.useTestingPlatformProtocol": true "dotnet.testWindow.useTestingPlatformProtocol": true,
"dotnet.formatting.organizeImportsOnFormat": true
} }
+88 -11
View File
@@ -3,11 +3,31 @@ namespace Just.Core.Tests.Base32Conversions;
public class Decode public class Decode
{ {
[Theory] [Theory]
[InlineData(15243)] [InlineData(15243, Base32EncodeOptions.None)]
[InlineData(812010)] [InlineData(15243, Base32EncodeOptions.LowerCase)]
[InlineData(97331334)] [InlineData(15243, Base32EncodeOptions.NoPadding)]
[InlineData(20354)] [InlineData(15243, Base32EncodeOptions.LowerCaseNoPadding)]
public void WhenEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed) [InlineData(812010, Base32EncodeOptions.None)]
[InlineData(812010, Base32EncodeOptions.LowerCase)]
[InlineData(812010, Base32EncodeOptions.NoPadding)]
[InlineData(812010, Base32EncodeOptions.LowerCaseNoPadding)]
[InlineData(97331334, Base32EncodeOptions.None)]
[InlineData(97331334, Base32EncodeOptions.LowerCase)]
[InlineData(97331334, Base32EncodeOptions.NoPadding)]
[InlineData(97331334, Base32EncodeOptions.LowerCaseNoPadding)]
[InlineData(20354, Base32EncodeOptions.None)]
[InlineData(20354, Base32EncodeOptions.LowerCase)]
[InlineData(20354, Base32EncodeOptions.NoPadding)]
[InlineData(20354, Base32EncodeOptions.LowerCaseNoPadding)]
[InlineData(33409, Base32EncodeOptions.None)]
[InlineData(33409, Base32EncodeOptions.LowerCase)]
[InlineData(33409, Base32EncodeOptions.NoPadding)]
[InlineData(33409, Base32EncodeOptions.LowerCaseNoPadding)]
[InlineData(59113, Base32EncodeOptions.None)]
[InlineData(59113, Base32EncodeOptions.LowerCase)]
[InlineData(59113, Base32EncodeOptions.NoPadding)]
[InlineData(59113, Base32EncodeOptions.LowerCaseNoPadding)]
public void WhenEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed, Base32EncodeOptions options)
{ {
var rng = new Random(seed); var rng = new Random(seed);
@@ -16,7 +36,7 @@ public class Decode
var testBytes = new byte[i]; var testBytes = new byte[i];
rng.NextBytes(testBytes); rng.NextBytes(testBytes);
var resultString = Base32.Encode(testBytes); var resultString = Base32.Encode(testBytes, options);
var resultBytes = Base32.Decode(resultString); var resultBytes = Base32.Decode(resultString);
resultBytes.ShouldBeEquivalentTo(testBytes); resultBytes.ShouldBeEquivalentTo(testBytes);
@@ -25,10 +45,18 @@ public class Decode
[Theory] [Theory]
[InlineData("FG4M3ZQM3TVWDMBUP5L7N7V3JS7KBM2E", new byte[] { 0x29, 0xb8, 0xcd, 0xe6, 0x0c, 0xdc, 0xeb, 0x61, 0xb0, 0x34, 0x7f, 0x57, 0xf6, 0xfe, 0xbb, 0x4c, 0xbe, 0xa0, 0xb3, 0x44, })] [InlineData("FG4M3ZQM3TVWDMBUP5L7N7V3JS7KBM2E", new byte[] { 0x29, 0xb8, 0xcd, 0xe6, 0x0c, 0xdc, 0xeb, 0x61, 0xb0, 0x34, 0x7f, 0x57, 0xf6, 0xfe, 0xbb, 0x4c, 0xbe, 0xa0, 0xb3, 0x44, })]
[InlineData("fg4m3zqm3tvwdmbup5l7n7v3js7kbm2e", new byte[] { 0x29, 0xb8, 0xcd, 0xe6, 0x0c, 0xdc, 0xeb, 0x61, 0xb0, 0x34, 0x7f, 0x57, 0xf6, 0xfe, 0xbb, 0x4c, 0xbe, 0xa0, 0xb3, 0x44, })]
[InlineData("WXYEOQUZULMCY6ZQTDOLTRUZZMKQ====", new byte[] { 0xb5, 0xf0, 0x47, 0x42, 0x99, 0xa2, 0xd8, 0x2c, 0x7b, 0x30, 0x98, 0xdc, 0xb9, 0xc6, 0x99, 0xcb, 0x15, })] [InlineData("WXYEOQUZULMCY6ZQTDOLTRUZZMKQ====", new byte[] { 0xb5, 0xf0, 0x47, 0x42, 0x99, 0xa2, 0xd8, 0x2c, 0x7b, 0x30, 0x98, 0xdc, 0xb9, 0xc6, 0x99, 0xcb, 0x15, })]
[InlineData("wxyeoquzulmcy6zqtdoltruzzmkq====", new byte[] { 0xb5, 0xf0, 0x47, 0x42, 0x99, 0xa2, 0xd8, 0x2c, 0x7b, 0x30, 0x98, 0xdc, 0xb9, 0xc6, 0x99, 0xcb, 0x15, })]
[InlineData("2IO2HTALCXZWCBD2", new byte[] { 0xd2, 0x1d, 0xa3, 0xcc, 0x0b, 0x15, 0xf3, 0x61, 0x04, 0x7a, })] [InlineData("2IO2HTALCXZWCBD2", new byte[] { 0xd2, 0x1d, 0xa3, 0xcc, 0x0b, 0x15, 0xf3, 0x61, 0x04, 0x7a, })]
[InlineData("ZFXJMF5N", new byte[] { 0b11001001, 0b01101110, 0b10010110, 0b00010111, 0b10101101, })] [InlineData("ZFXJMF5N", new byte[] { 0b11001001, 0b01101110, 0b10010110, 0b00010111, 0b10101101, })]
[InlineData("zfxjmf5n", new byte[] { 0b11001001, 0b01101110, 0b10010110, 0b00010111, 0b10101101, })]
[InlineData("CPIKTMY=", new byte[] { 0b00010011, 0b11010000, 0b10101001, 0b10110011, })] [InlineData("CPIKTMY=", new byte[] { 0b00010011, 0b11010000, 0b10101001, 0b10110011, })]
[InlineData("EFCDEAA=", new byte[] { 0x21, 0x44, 0x32, 0x00, })]
[InlineData("EFCDEAI=", new byte[] { 0x21, 0x44, 0x32, 0x01, })]
[InlineData("EFCDEAQ=", new byte[] { 0x21, 0x44, 0x32, 0x02, })]
[InlineData("EFCDEAY=", new byte[] { 0x21, 0x44, 0x32, 0x03, })]
[InlineData("EFCDEBA=", new byte[] { 0x21, 0x44, 0x32, 0x04, })]
[InlineData("JVNJA===", new byte[] { 0b01001101, 0b01011010, 0b10010000, })] [InlineData("JVNJA===", new byte[] { 0b01001101, 0b01011010, 0b10010000, })]
[InlineData("74OQ====", new byte[] { 0b11111111, 0b00011101, })] [InlineData("74OQ====", new byte[] { 0b11111111, 0b00011101, })]
public void WhenCalledWithValidString_ShouldReturnValidByteArray(string str, byte[] expected) public void WhenCalledWithValidString_ShouldReturnValidByteArray(string str, byte[] expected)
@@ -59,14 +87,41 @@ public class Decode
[Theory] [Theory]
[InlineData(" ")] [InlineData(" ")]
[InlineData("hg2515i3215")] [InlineData("hg2515i3215q")]
[InlineData("hg712)21")] [InlineData("hg712)21")]
[InlineData("hg712f 21")] [InlineData("hg712f 211")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")] [InlineData("AEBAGB^F")]
public void WhenCalledWithNotValidString_ShouldThrowFormatException(string testString) public void WhenCalledWithNotValidString_ShouldThrowFormatException(string testString)
{ {
Action action = () => Base32.Decode(testString); Action action = () => _ = Base32.Decode(testString);
action.ShouldThrow<FormatException>(); action.ShouldThrow<FormatException>()
.WithMessage("Provided string contains invalid characters.");
}
[Theory]
[InlineData("A")]
[InlineData("AAA")]
[InlineData("AAAAAA")]
[InlineData("AEBAGBAFA")]
[InlineData("AEBAGBAFAAA")]
[InlineData("AEBAGBAFAAAAAA")]
public void WhenCalledWithInvalidStringLength_ShouldThrowFormatException(string testString)
{
Action action = () => _ = Base32.Decode(testString);
action.ShouldThrow<FormatException>()
.WithMessage("Invalid Base32 string length.");
}
[Theory]
[InlineData("ABCDEFG")]
[InlineData("AAAAAAB")]
[InlineData("EFCDF5J")]
[InlineData("EFCDF5P")]
public void WhenCalledWithInvalidLastByteEncoding_ShouldThrowFormatException(string testString)
{
Action action = () => _ = Base32.Decode(testString);
action.ShouldThrow<FormatException>()
.WithMessage("Invalid Base32 string. Inconsistent tail bits.");
} }
[Theory] [Theory]
@@ -76,4 +131,26 @@ public class Decode
{ {
Base32.Decode(testString).ShouldBeEmpty(); Base32.Decode(testString).ShouldBeEmpty();
} }
[Theory]
[InlineData("====")]
[InlineData("============")]
public void WhenCalledWithOnlyPadding_ShouldReturnEmptyArray(string testString)
{
Base32.Decode(testString).ShouldBeEmpty();
}
[Theory]
[InlineData("AEBAGBAF", 1)]
[InlineData("AEBAGBAF", 2)]
[InlineData("AEBAGBAF", 3)]
[InlineData("AEBAGBAFAY", 5)]
[InlineData("AEBAGBAFAYDQ", 6)]
public void SpanTooSmall_ThrowsArgumentException(string input, int outputLength)
{
byte[] output = new byte[outputLength];
Action action = () => _ = Base32.Decode(input, output);
action.ShouldThrow<ArgumentException>()
.WithMessage("Decoded input can not fit in output span. (Parameter 'output')");
}
} }
+41
View File
@@ -4,6 +4,7 @@ public class Encode
{ {
[Theory] [Theory]
[InlineData("TQ======")] [InlineData("TQ======")]
[InlineData("CE======")]
[InlineData("3X3A====")] [InlineData("3X3A====")]
[InlineData("426G6===")] [InlineData("426G6===")]
[InlineData("C3V3Y===")] [InlineData("C3V3Y===")]
@@ -30,8 +31,10 @@ public class Encode
{ {
var resultBytes = Base32.Decode(testString); var resultBytes = Base32.Decode(testString);
var resultString = Base32.Encode(resultBytes); var resultString = Base32.Encode(resultBytes);
var resultStringLowerCase = Base32.Encode(resultBytes, Base32EncodeOptions.LowerCase);
resultString.ShouldBe(testString); resultString.ShouldBe(testString);
resultStringLowerCase.ShouldBe(testString.ToLowerInvariant());
} }
[Theory] [Theory]
@@ -47,7 +50,29 @@ public class Encode
public void WhenCalledWithNotEmptyByteArray_ShouldReturnValidString(string expected, byte[] testArray) public void WhenCalledWithNotEmptyByteArray_ShouldReturnValidString(string expected, byte[] testArray)
{ {
var str = Base32.Encode(testArray); var str = Base32.Encode(testArray);
var strLowerCase = Base32.Encode(testArray, Base32EncodeOptions.LowerCase);
str.ShouldBe(expected); str.ShouldBe(expected);
strLowerCase.ShouldBe(expected.ToLowerInvariant());
}
[Theory]
[InlineData("FG4M3ZQM3TVWDMBUP5L7N7V3JS7KBM2E", new byte[] { 0x29, 0xb8, 0xcd, 0xe6, 0x0c, 0xdc, 0xeb, 0x61, 0xb0, 0x34, 0x7f, 0x57, 0xf6, 0xfe, 0xbb, 0x4c, 0xbe, 0xa0, 0xb3, 0x44, })]
[InlineData("WXYEOQUZULMCY6ZQTDOLTRUZZMKQ", new byte[] { 0xb5, 0xf0, 0x47, 0x42, 0x99, 0xa2, 0xd8, 0x2c, 0x7b, 0x30, 0x98, 0xdc, 0xb9, 0xc6, 0x99, 0xcb, 0x15, })]
[InlineData("2IO2HTALCXZWCBD2AAAAAAAAAAAA", new byte[] { 0xd2, 0x1d, 0xa3, 0xcc, 0x0b, 0x15, 0xf3, 0x61, 0x04, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, })]
[InlineData("2IO2HTALCXZWCBD2AAAAAAAA", new byte[] { 0xd2, 0x1d, 0xa3, 0xcc, 0x0b, 0x15, 0xf3, 0x61, 0x04, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, })]
[InlineData("2IO2HTALCXZWCBD2", new byte[] { 0xd2, 0x1d, 0xa3, 0xcc, 0x0b, 0x15, 0xf3, 0x61, 0x04, 0x7a, })]
[InlineData("ZFXJMF5N", new byte[] { 0b11001001, 0b01101110, 0b10010110, 0b00010111, 0b10101101, })]
[InlineData("CPIKTMY", new byte[] { 0b00010011, 0b11010000, 0b10101001, 0b10110011, })]
[InlineData("JVNJA", new byte[] { 0b01001101, 0b01011010, 0b10010000, })]
[InlineData("74OQ", new byte[] { 0b11111111, 0b00011101, })]
public void WhenCalledWithNotEmptyByteArray_ShouldReturnValidStringWithNoPadding(string expected, byte[] testArray)
{
var str = Base32.Encode(testArray, Base32EncodeOptions.NoPadding);
var strLowerCase = Base32.Encode(testArray, Base32EncodeOptions.NoPadding | Base32EncodeOptions.LowerCase);
str.ShouldBe(expected);
strLowerCase.ShouldBe(expected.ToLowerInvariant());
} }
[Theory] [Theory]
@@ -71,4 +96,20 @@ public class Encode
charsWritten.ShouldBe(0); charsWritten.ShouldBe(0);
output.ShouldBe(['1', '2', '3', '4']); output.ShouldBe(['1', '2', '3', '4']);
} }
[Theory]
[InlineData(new byte[] { 1, 2, 3 }, 3)]
[InlineData(new byte[] { 1, 2, 3 }, 4)]
[InlineData(new byte[] { 1, 2, 3 }, 5)]
[InlineData(new byte[] { 1, 2, 3 }, 6)]
[InlineData(new byte[] { 1, 2, 3 }, 7)]
[InlineData(new byte[] { 1, 2, 3, 4, 5, 6 }, 15)]
[InlineData(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }, 23)]
public void SpanTooSmall_ThrowsArgumentException(byte[] input, int outputLength)
{
var output = new char[outputLength];
Action action = () => _ = Base32.Encode(input, output);
action.ShouldThrow<ArgumentException>()
.WithMessage("Encoded input can not fit in output span. (Parameter 'output')");
}
} }
+2 -2
View File
@@ -16,8 +16,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Shouldly" Version="4.3.0" /> <PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="xunit.v3" Version="3.2.0" /> <PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="coverlet.collector" Version="6.0.4"> <PackageReference Include="coverlet.collector" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
+6 -3
View File
@@ -127,15 +127,18 @@ public class NextId
public void DefaultInstance_NextId_ShouldUseDefaultEpoch() public void DefaultInstance_NextId_ShouldUseDefaultEpoch()
{ {
// Arrange // Arrange
var now = DateTime.UtcNow; var now = new DateTime(2026, 6, 3, 21, 11, 1, DateTimeKind.Utc);
var defaultEpoch = SeqId.DefaultEpoch; SeqId.Default.UnsafeReplaceDefaultTimeFactory(() => now);
long expectedTimestamp = ((long)(now - defaultEpoch).TotalMilliseconds) & TimestampMask; // Mask handles overflow
// Act // Act
long id = SeqId.NextId(); long id = SeqId.NextId();
// Assert // Assert
long expectedTimestamp = GetExpectedTimestamp(now);
long timestampPart = (id >> TimestampShift) & TimestampMask; long timestampPart = (id >> TimestampShift) & TimestampMask;
timestampPart.ShouldBeInRange(expectedTimestamp, expectedTimestamp + 1); timestampPart.ShouldBeInRange(expectedTimestamp, expectedTimestamp + 1);
static long GetExpectedTimestamp(DateTime now) => ((long)(now - SeqId.DefaultEpoch).TotalMilliseconds) & TimestampMask; // Mask handles overflow
} }
} }
+88 -27
View File
@@ -1,28 +1,37 @@
namespace Just.Core; namespace Just.Core;
public enum Base32EncodeOptions
{
None = 0x00,
LowerCase = 0x01,
NoPadding = 0x02,
LowerCaseNoPadding = 0x03,
}
public static class Base32 public static class Base32
{ {
public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
public const string AlphabetLower = "abcdefghijklmnopqrstuvwxyz234567";
public const char Padding = '='; public const char Padding = '=';
public const int MaxBytesStack = 250; public const int MaxBytesStack = 250;
[Pure] [Pure]
public static string Encode(ReadOnlySpan<byte> input) public static string Encode(ReadOnlySpan<byte> input, Base32EncodeOptions options = Base32EncodeOptions.None)
{ {
if (input.IsEmpty) return string.Empty; if (input.IsEmpty) return string.Empty;
int outLength = 8 * ((input.Length + 4) / 5); int outLength = 8 * ((input.Length + 4) / 5);
Span<char> output = input.Length <= MaxBytesStack Span<char> output = input.Length > MaxBytesStack
? stackalloc char[outLength] ? new char[outLength]
: new char[outLength]; : stackalloc char[outLength];
var size = Encode(input, output); var size = Encode(input, output, options);
return new string(output[..size]); return new string(output[..size]);
} }
[Pure] [Pure]
public static int Encode(ReadOnlySpan<byte> input, Span<char> output) public static int Encode(ReadOnlySpan<byte> input, Span<char> output, Base32EncodeOptions options = Base32EncodeOptions.None)
{ {
if (input.IsEmpty) return 0; if (input.IsEmpty) return 0;
@@ -34,25 +43,63 @@ public static class Base32
output = output[..outputLength]; output = output[..outputLength];
int i = 0; ReadOnlySpan<char> alphabet = (options & Base32EncodeOptions.LowerCase) == Base32EncodeOptions.LowerCase
ReadOnlySpan<char> alphabet = Alphabet; ? AlphabetLower
: Alphabet;
Span<byte> alphabetKeys = stackalloc byte[8]; Span<byte> alphabetKeys = stackalloc byte[8];
int i = 0;
for (int offset = 0; offset < input.Length;) for (int offset = 0; offset < input.Length;)
{ {
alphabetKeys.Clear(); alphabetKeys.Clear();
int numCharsToOutput = GetNextGroup(input, ref offset, alphabetKeys); int numCharsToOutput = GetNextGroup(input, ref offset, alphabetKeys);
output[i++] = (numCharsToOutput > 0) ? alphabet[alphabetKeys[0]] : Padding; output[i++] = alphabet[alphabetKeys[0]];
output[i++] = (numCharsToOutput > 1) ? alphabet[alphabetKeys[1]] : Padding; output[i++] = alphabet[alphabetKeys[1]];
output[i++] = (numCharsToOutput > 2) ? alphabet[alphabetKeys[2]] : Padding;
output[i++] = (numCharsToOutput > 3) ? alphabet[alphabetKeys[3]] : Padding; if (numCharsToOutput < 3)
output[i++] = (numCharsToOutput > 4) ? alphabet[alphabetKeys[4]] : Padding; {
output[i++] = (numCharsToOutput > 5) ? alphabet[alphabetKeys[5]] : Padding; i = FillWithPadding(output, i, numCharsToOutput, options);
output[i++] = (numCharsToOutput > 6) ? alphabet[alphabetKeys[6]] : Padding; break;
output[i++] = (numCharsToOutput > 7) ? alphabet[alphabetKeys[7]] : Padding; }
output[i++] = alphabet[alphabetKeys[2]];
output[i++] = alphabet[alphabetKeys[3]];
if (numCharsToOutput < 5)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[4]];
if (numCharsToOutput < 6)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[5]];
output[i++] = alphabet[alphabetKeys[6]];
if (numCharsToOutput < 8)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[7]];
} }
return i; return i;
static int FillWithPadding(Span<char> output, int i, int numCharsToOutput, Base32EncodeOptions options)
{
if ((options & Base32EncodeOptions.NoPadding) == Base32EncodeOptions.NoPadding)
{
return i;
}
var pad = 8 - numCharsToOutput;
output[i..(i+pad)].Fill(Padding);
return i + pad;
}
} }
[Pure] [Pure]
@@ -61,10 +108,10 @@ public static class Base32
input = input.TrimEnd(Padding); input = input.TrimEnd(Padding);
if (input.IsEmpty) return []; if (input.IsEmpty) return [];
var outputLength = 5 * ((input.Length + 7) / 8); var outputLength = 5 * input.Length / 8;
Span<byte> output = outputLength <= MaxBytesStack Span<byte> output = outputLength > MaxBytesStack
? stackalloc byte[outputLength] ? new byte[outputLength]
: new byte[outputLength]; : stackalloc byte[outputLength];
var size = Decode(input, output); var size = Decode(input, output);
@@ -76,7 +123,13 @@ public static class Base32
{ {
input = input.TrimEnd(Padding); input = input.TrimEnd(Padding);
var outputLength = 5 * ((input.Length + 7) / 8); var rem = input.Length % 8;
if (rem == 1 || rem == 3 || rem == 6)
{
throw new FormatException("Invalid Base32 string length.");
}
var outputLength = 5 * input.Length / 8;
if (output.Length < outputLength) if (output.Length < outputLength)
{ {
throw new ArgumentException("Decoded input can not fit in output span.", nameof(output)); throw new ArgumentException("Decoded input can not fit in output span.", nameof(output));
@@ -85,9 +138,9 @@ public static class Base32
output = output[..outputLength]; output = output[..outputLength];
output.Clear(); output.Clear();
Span<char> inputspan = outputLength <= MaxBytesStack Span<char> inputspan = outputLength > MaxBytesStack
? stackalloc char[input.Length] ? new char[input.Length]
: new char[input.Length]; : stackalloc char[input.Length];
input.ToUpperInvariant(inputspan); input.ToUpperInvariant(inputspan);
int bitIndex = 0; int bitIndex = 0;
@@ -127,10 +180,17 @@ public static class Base32
inputIndex++; inputIndex++;
bitIndex = 0; bitIndex = 0;
} }
else if (inputIndex == input.Length -1) break; else if (inputIndex == input.Length -1)
{
if ((byteIndex & ~(0x1f << (bitPos - bits))) > 0)
{
throw new FormatException("Invalid Base32 string. Inconsistent tail bits.");
}
break;
}
} }
return outputIndex + (outputBits + 7) / 8; return outputIndex;
} }
// returns the number of bytes that were output // returns the number of bytes that were output
@@ -145,7 +205,8 @@ public static class Base32
4 => 7, 4 => 7,
_ => 8, _ => 8,
}; };
uint b1 = (offset < input.Length) ? input[offset++] : 0U;
uint b1 = input[offset++];
uint b2 = (offset < input.Length) ? input[offset++] : 0U; uint b2 = (offset < input.Length) ? input[offset++] : 0U;
uint b3 = (offset < input.Length) ? input[offset++] : 0U; uint b3 = (offset < input.Length) ? input[offset++] : 0U;
uint b4 = (offset < input.Length) ? input[offset++] : 0U; uint b4 = (offset < input.Length) ? input[offset++] : 0U;
+4 -5
View File
@@ -1,6 +1,5 @@
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Just.Core.Extensions;
namespace Just.Core.Collections; namespace Just.Core.Collections;
@@ -122,14 +121,14 @@ public static class DataMap
where T : unmanaged, IComparisonOperators<T, T, bool>, IEqualityOperators<T, T, bool> where T : unmanaged, IComparisonOperators<T, T, bool>, IEqualityOperators<T, T, bool>
{ {
byte[] headBytes = new byte[HeaderSize]; byte[] headBytes = new byte[HeaderSize];
await stream.PopulateAsync(headBytes, cancellationToken); await stream.ReadExactlyAsync(headBytes, cancellationToken);
var head = MemoryMarshal.Read<Header>(headBytes); var head = MemoryMarshal.Read<Header>(headBytes);
var bodySize = DataMap<T>.ElementSize * head.Width * head.Height; var bodySize = DataMap<T>.ElementSize * head.Width * head.Height;
if (bodySize != head.BodySize) throw new InvalidOperationException("Can not read DataMap. Element size mismatch."); if (bodySize != head.BodySize) throw new InvalidOperationException("Can not read DataMap. Element size mismatch.");
byte[] bodyBytes = new byte[bodySize]; byte[] bodyBytes = new byte[bodySize];
await stream.PopulateAsync(bodyBytes, cancellationToken); await stream.ReadExactlyAsync(bodyBytes, cancellationToken);
T[] body = MemoryMarshal.Cast<byte, T>(bodyBytes).ToArray(); T[] body = MemoryMarshal.Cast<byte, T>(bodyBytes).ToArray();
return new DataMap<T>((int)head.Width, (int)head.Height, body); return new DataMap<T>((int)head.Width, (int)head.Height, body);
@@ -157,7 +156,7 @@ public static class DataMap
{ {
Header head = default; Header head = default;
var headSpan = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref head, 1)); var headSpan = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref head, 1));
stream.Populate(headSpan); stream.ReadExactly(headSpan);
var bodySize = DataMap<T>.ElementSize * head.Width * head.Height; var bodySize = DataMap<T>.ElementSize * head.Width * head.Height;
if (bodySize != head.BodySize) throw new InvalidOperationException("Can not read DataMap. Element size mismatch."); if (bodySize != head.BodySize) throw new InvalidOperationException("Can not read DataMap. Element size mismatch.");
@@ -165,7 +164,7 @@ public static class DataMap
T[] body = new T[bodySize]; T[] body = new T[bodySize];
var bodySpan = MemoryMarshal.AsBytes(body.AsSpan()); var bodySpan = MemoryMarshal.AsBytes(body.AsSpan());
stream.Populate(bodySpan); stream.ReadExactly(bodySpan);
return new DataMap<T>((int)head.Width, (int)head.Height, body); return new DataMap<T>((int)head.Width, (int)head.Height, body);
} }
+117 -1
View File
@@ -3,6 +3,22 @@ using System.Collections.Immutable;
namespace Just.Core.Collections; namespace Just.Core.Collections;
/// <summary>
/// Represents an immutable, ordered sequence of items with valueequality semantics.
/// </summary>
/// <typeparam name="T">The type of elements in the sequence.</typeparam>
/// <remarks>
/// <para>
/// This class is a thin wrapper around <see cref="ImmutableList{T}"/> that implements
/// <see cref="IReadOnlyList{T}"/>, <see cref="IEquatable{T}"/>, and valuebased equality.
/// All modifications return new <see cref="ImmutableSequence{T}"/> instances, leaving the
/// original unchanged.
/// </para>
/// <para>
/// Subclasses may override <see cref="ConstructNew"/> to ensure mutation methods return
/// the correct derived type.
/// </para>
/// </remarks>
public class ImmutableSequence<T> : public class ImmutableSequence<T> :
IEnumerable<T>, IEnumerable<T>,
IReadOnlyList<T>, IReadOnlyList<T>,
@@ -11,17 +27,61 @@ public class ImmutableSequence<T> :
private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals; private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals;
private readonly ImmutableList<T> _values; private readonly ImmutableList<T> _values;
/// <summary>
/// Initializes a new empty instance of the <see cref="ImmutableSequence{T}"/> class.
/// </summary>
public ImmutableSequence() => _values = []; public ImmutableSequence() => _values = [];
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableSequence{T}"/> class that
/// wraps the specified <see cref="ImmutableList{T}"/>.
/// </summary>
/// <param name="values">The immutable list to wrap.</param>
public ImmutableSequence(ImmutableList<T> values) => _values = values; public ImmutableSequence(ImmutableList<T> values) => _values = values;
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableSequence{T}"/> class with
/// the elements from the provided enumerable sequence.
/// </summary>
/// <param name="values">The items to include in the sequence.</param>
public ImmutableSequence(IEnumerable<T> values) => _values = [..values]; public ImmutableSequence(IEnumerable<T> values) => _values = [..values];
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableSequence{T}"/> class with
/// the elements from the provided readonly span.
/// </summary>
/// <param name="values">The items to include in the sequence.</param>
public ImmutableSequence(ReadOnlySpan<T> values) : this(ImmutableList.Create(values)) public ImmutableSequence(ReadOnlySpan<T> values) : this(ImmutableList.Create(values))
{ {
} }
/// <summary>
/// Gets a value indicating whether the sequence contains any elements.
/// </summary>
public bool IsEmpty => _values.IsEmpty; public bool IsEmpty => _values.IsEmpty;
/// <summary>
/// Gets the number of elements in the sequence.
/// </summary>
public int Count => _values.Count; public int Count => _values.Count;
/// <summary>
/// Gets the element at the specified zerobased index.
/// </summary>
/// <param name="index">The zerobased index of the element to get.</param>
/// <returns>The element at the specified index.</returns>
public T this[int index] => _values[index]; public T this[int index] => _values[index];
/// <summary>
/// Gets the element at the specified position from the start or end of the sequence.
/// </summary>
/// <param name="index">An <see cref="Index"/> value (e.g., <c>^1</c> for the last element).</param>
/// <returns>The element at the specified position.</returns>
public T this[Index index] => _values[index]; public T this[Index index] => _values[index];
/// <summary>
/// Gets a new <see cref="ImmutableSequence{T}"/> containing the elements in the specified range.
/// </summary>
/// <param name="range">The range of elements to include.</param>
/// <returns>A new sequence representing the slice.</returns>
public ImmutableSequence<T> this[Range range] public ImmutableSequence<T> this[Range range]
{ {
get get
@@ -31,25 +91,60 @@ public class ImmutableSequence<T> :
} }
} }
/// <summary>
/// Creates a new <see cref="ImmutableSequence{T}"/> from the provided immutable list.
/// Subclasses can override this to return instances of a more specific type.
/// </summary>
/// <param name="values">The immutable list that will become the internal storage.</param>
/// <returns>A new sequence containing the given items.</returns>
protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => [..values]; protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => [..values];
/// <summary>
/// Returns a new sequence with the specified value appended to the end.
/// </summary>
/// <param name="value">The value to add.</param>
/// <returns>A new sequence containing the original items followed by <paramref name="value"/>.</returns>
public ImmutableSequence<T> Add(T value) => ConstructNew(_values.Add(value)); public ImmutableSequence<T> Add(T value) => ConstructNew(_values.Add(value));
/// <summary>
/// Returns a new sequence with the specified value inserted at the beginning.
/// </summary>
/// <param name="value">The value to add.</param>
/// <returns>A new sequence that starts with <paramref name="value"/> and then contains the original items.</returns>
public ImmutableSequence<T> AddFront(T value) => ConstructNew(_values.Insert(0, value)); public ImmutableSequence<T> AddFront(T value) => ConstructNew(_values.Insert(0, value));
/// <summary>
/// Returns an enumerator that iterates through the sequence.
/// </summary>
/// <returns>A <see cref="ImmutableList{T}.Enumerator"/> value type enumerator.</returns>
public ImmutableList<T>.Enumerator GetEnumerator() => _values.GetEnumerator(); public ImmutableList<T>.Enumerator GetEnumerator() => _values.GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator(); IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator();
public override string ToString() => string.Join(Environment.NewLine, _values); public override string ToString() => string.Join(Environment.NewLine, _values);
/// <summary>
/// Determines whether this sequence is equal to another <see cref="ImmutableSequence{T}"/>.
/// Equality is based on the number of elements and the elementwise equality comparison
/// using the default equality comparer for <typeparamref name="T"/>.
/// </summary>
/// <param name="other">The sequence to compare with this instance. Can be <c>null</c>.</param>
/// <returns>
/// <c>true</c> if the sequences have the same length and all elements are equal;
/// <c>false</c> otherwise.
/// </returns>
public virtual bool Equals([NotNullWhen(true)] ImmutableSequence<T>? other) public virtual bool Equals([NotNullWhen(true)] ImmutableSequence<T>? other)
{ {
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other)) if (ReferenceEquals(this, other))
{ {
return true; return true;
} }
if (_values.Count != other?._values.Count) if (_values.Count != other._values.Count)
{ {
return false; return false;
} }
@@ -65,7 +160,16 @@ public class ImmutableSequence<T> :
return true; return true;
} }
/// <summary>
/// Determines whether the specified object is equal to the current sequence.
/// </summary>
/// <param name="obj">The object to compare with the current sequence.</param>
/// <returns><c>true</c> if <paramref name="obj"/> is an <see cref="ImmutableSequence{T}"/> and equals this instance; otherwise <c>false</c>.</returns>
public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as ImmutableSequence<T>); public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as ImmutableSequence<T>);
/// <summary>
/// Serves as a hash function for the sequence.
/// </summary>
/// <returns>A hash code that incorporates all elements in order.</returns>
public override int GetHashCode() public override int GetHashCode()
{ {
HashCode hash = new(); HashCode hash = new();
@@ -78,6 +182,18 @@ public class ImmutableSequence<T> :
return hash.ToHashCode(); return hash.ToHashCode();
} }
/// <summary>
/// Determines whether two sequences are equal.
/// </summary>
/// <param name="left">The first sequence to compare.</param>
/// <param name="right">The second sequence to compare.</param>
/// <returns><c>true</c> if both sequences are <c>null</c> or they are considered equal; otherwise <c>false</c>.</returns>
public static bool operator ==(ImmutableSequence<T>? left, ImmutableSequence<T>? right) => left is null ? right is null : left.Equals(right); public static bool operator ==(ImmutableSequence<T>? left, ImmutableSequence<T>? right) => left is null ? right is null : left.Equals(right);
/// <summary>
/// Determines whether two sequences are not equal.
/// </summary>
/// <param name="left">The first sequence to compare.</param>
/// <param name="right">The second sequence to compare.</param>
/// <returns><c>true</c> if the sequences are not equal; otherwise <c>false</c>.</returns>
public static bool operator !=(ImmutableSequence<T>? left, ImmutableSequence<T>? right) => !(left == right); public static bool operator !=(ImmutableSequence<T>? left, ImmutableSequence<T>? right) => !(left == right);
} }
+2 -2
View File
@@ -68,8 +68,8 @@ public class Map<T> : IMap<T>
{ {
get get
{ {
x = Math.Clamp(x, 0, Width); x = Math.Clamp(x, 0, Width - 1);
y = Math.Clamp(y, 0, Height); y = Math.Clamp(y, 0, Height - 1);
return ref _values[(y * Width) + x]; return ref _values[(y * Width) + x];
} }
} }
+1 -1
View File
@@ -10,7 +10,7 @@
<Description>Small .Net library with useful helper classes, functions and extensions.</Description> <Description>Small .Net library with useful helper classes, functions and extensions.</Description>
<PackageTags>extensions;helpers;helper-functions</PackageTags> <PackageTags>extensions;helpers;helper-functions</PackageTags>
<Authors>JustFixMe</Authors> <Authors>JustFixMe</Authors>
<Copyright>Copyright (c) 2023-2025 JustFixMe</Copyright> <Copyright>Copyright (c) 2023-2026 JustFixMe</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/JustFixMe/Just.Core/</RepositoryUrl> <RepositoryUrl>https://github.com/JustFixMe/Just.Core/</RepositoryUrl>
@@ -3,6 +3,7 @@ namespace Just.Core.Extensions;
/// <summary> /// <summary>
/// Provides extension methods for <see cref="Stream"/> to fully populate buffers. /// Provides extension methods for <see cref="Stream"/> to fully populate buffers.
/// </summary> /// </summary>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static class SystemIOStreamExtensions public static class SystemIOStreamExtensions
{ {
/// <summary> /// <summary>
@@ -16,6 +17,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="offset"/> or <paramref name="length"/> is invalid</exception> /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="offset"/> or <paramref name="length"/> is invalid</exception>
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception> /// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception> /// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static void Populate(this Stream stream, byte[] buffer, int offset, int length) public static void Populate(this Stream stream, byte[] buffer, int offset, int length)
=> stream.Populate(buffer.AsSpan(offset, length)); => stream.Populate(buffer.AsSpan(offset, length));
/// <summary> /// <summary>
@@ -26,6 +28,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception> /// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception> /// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception> /// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static void Populate(this Stream stream, byte[] buffer) public static void Populate(this Stream stream, byte[] buffer)
=> stream.Populate(buffer.AsSpan()); => stream.Populate(buffer.AsSpan());
/// <summary> /// <summary>
@@ -36,6 +39,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception> /// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception> /// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception> /// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static void Populate(this Stream stream, Span<byte> buffer) public static void Populate(this Stream stream, Span<byte> buffer)
{ {
ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(stream);
@@ -64,6 +68,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception> /// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception> /// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception> /// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default) public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default)
=> stream.PopulateAsync(buffer.AsMemory(), cancellationToken); => stream.PopulateAsync(buffer.AsMemory(), cancellationToken);
/// <summary> /// <summary>
@@ -80,6 +85,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception> /// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception> /// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception> /// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default) public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default)
=> stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken); => stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken);
/// <summary> /// <summary>
@@ -93,6 +99,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception> /// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception> /// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception> /// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static async ValueTask PopulateAsync(this Stream stream, Memory<byte> buffer, CancellationToken cancellationToken = default) public static async ValueTask PopulateAsync(this Stream stream, Memory<byte> buffer, CancellationToken cancellationToken = default)
{ {
ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(stream);
+1
View File
@@ -7,6 +7,7 @@ public static class GuidV8
private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10; private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
[ExcludeFromCodeCoverage]
public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy); public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong) public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
+6 -3
View File
@@ -51,7 +51,7 @@ public sealed class SeqId(DateTime epoch)
/// Thrown if more than 255 IDs generated in 1ms /// Thrown if more than 255 IDs generated in 1ms
/// </exception> /// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(DateTime.UtcNow, entropy); public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(entropy);
#if NET9_0_OR_GREATER #if NET9_0_OR_GREATER
private readonly Lock _lock = new(); private readonly Lock _lock = new();
@@ -62,6 +62,7 @@ public sealed class SeqId(DateTime epoch)
private readonly DateTime _epoch = epoch; private readonly DateTime _epoch = epoch;
private int _seqId = 0; private int _seqId = 0;
private long _lastTimestamp = -1L; private long _lastTimestamp = -1L;
private Func<DateTime> _defaultTimeFactory = static () => DateTime.UtcNow;
/// <summary> /// <summary>
/// Generates next ID using current UTC time /// Generates next ID using current UTC time
@@ -72,7 +73,7 @@ public sealed class SeqId(DateTime epoch)
/// Thrown if more than 255 IDs generated in 1ms /// Thrown if more than 255 IDs generated in 1ms
/// </exception> /// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(DateTime.UtcNow, entropy); public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(_defaultTimeFactory(), entropy);
/// <summary> /// <summary>
/// Generates next ID with explicit timestamp /// Generates next ID with explicit timestamp
@@ -104,7 +105,7 @@ public sealed class SeqId(DateTime epoch)
throw new InvalidOperationException("Refused to create new SeqId. Last timestamp is in the future."); throw new InvalidOperationException("Refused to create new SeqId. Last timestamp is in the future.");
} }
if (_seqId == SeqMask) if (_seqId > SeqMask)
{ {
throw new IndexOutOfRangeException("Refused to create new SeqId. Sequence exhausted."); throw new IndexOutOfRangeException("Refused to create new SeqId. Sequence exhausted.");
} }
@@ -118,4 +119,6 @@ public sealed class SeqId(DateTime epoch)
return timestamp | currentSeq | currentRand; return timestamp | currentSeq | currentRand;
} }
internal void UnsafeReplaceDefaultTimeFactory(Func<DateTime> timeFactory) => _defaultTimeFactory = timeFactory;
} }
-28
View File
@@ -1,28 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{C709D8C9-FE18-4B70-ABE0-57A1850C3398}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Tests", "Core.Tests\Core.Tests.csproj", "{CBA236E8-5CAC-4587-AD7C-A480CD998EB8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C709D8C9-FE18-4B70-ABE0-57A1850C3398}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C709D8C9-FE18-4B70-ABE0-57A1850C3398}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C709D8C9-FE18-4B70-ABE0-57A1850C3398}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C709D8C9-FE18-4B70-ABE0-57A1850C3398}.Release|Any CPU.Build.0 = Release|Any CPU
{CBA236E8-5CAC-4587-AD7C-A480CD998EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CBA236E8-5CAC-4587-AD7C-A480CD998EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CBA236E8-5CAC-4587-AD7C-A480CD998EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CBA236E8-5CAC-4587-AD7C-A480CD998EB8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+4
View File
@@ -0,0 +1,4 @@
<Solution>
<Project Path="Core.Tests/Core.Tests.csproj" />
<Project Path="Core/Core.csproj" />
</Solution>
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2023-2025 JustFixMe Copyright (c) 2023-2026 JustFixMe
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal