1 Commits

Author SHA1 Message Date
3665abaab8 dotnet 9 and sequential id
All checks were successful
.NET Test / test (push) Successful in 49s
.NET Publish / publish (push) Successful in 41s
2025-08-01 22:21:42 +04:00
14 changed files with 428 additions and 68 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v3
with: with:
dotnet-version: 8.x dotnet-version: 9.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore Core/Core.csproj run: dotnet restore Core/Core.csproj

View File

@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: https://github.com/actions/setup-dotnet@v3 uses: https://github.com/actions/setup-dotnet@v3
with: with:
dotnet-version: 8.x dotnet-version: 9.x
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
@@ -30,12 +30,12 @@ jobs:
run: dotnet build --no-restore run: dotnet build --no-restore
- name: Test - name: Test
run: dotnet test --no-build --verbosity normal --logger trx --results-directory "TestResults-8.x" run: dotnet test --no-build --verbosity normal --logger trx --results-directory "TestResults-9.x"
- name: Upload dotnet test results - name: Upload dotnet test results
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: dotnet-results-8.x name: dotnet-results-9.x
path: TestResults-8.x path: TestResults-9.x
if: ${{ always() }} if: ${{ always() }}
retention-days: 30 retention-days: 30

View File

@@ -72,7 +72,7 @@ public class Decode
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("")] [InlineData("")]
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string testString) public void WhenCalledWithNullString_ShouldReturnEmptyArray(string? testString)
{ {
Base32.Decode(testString).Should().BeEmpty(); Base32.Decode(testString).Should().BeEmpty();
} }

View File

@@ -23,6 +23,9 @@ public class Encode
[InlineData("GSHXGB5ORKNDSFLSU2YWALI=")] [InlineData("GSHXGB5ORKNDSFLSU2YWALI=")]
[InlineData("TMFSC64ZZNPQSNGCFIODS7TR")] [InlineData("TMFSC64ZZNPQSNGCFIODS7TR")]
[InlineData("ZTDUBU4QZFFMDJKBII334EIB")] [InlineData("ZTDUBU4QZFFMDJKBII334EIB")]
[InlineData("2IO2HTALCXZWCBD2AAAAAAAAAAAA====")]
[InlineData("WXYEOQUZULMCY6ZQTDOLTRUZZMKQ====")]
[InlineData("FG4M3ZQM3TVWDMBUP5L7N7V3JS7KBM2E")]
public void WhenDecodedFromString_ShouldBeEncodedToTheSameString(string testString) public void WhenDecodedFromString_ShouldBeEncodedToTheSameString(string testString)
{ {
var resultBytes = Base32.Decode(testString); var resultBytes = Base32.Decode(testString);
@@ -34,6 +37,8 @@ public class Encode
[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("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("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("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("CPIKTMY=", new byte[] { 0b00010011, 0b11010000, 0b10101001, 0b10110011, })] [InlineData("CPIKTMY=", new byte[] { 0b00010011, 0b11010000, 0b10101001, 0b10110011, })]
@@ -48,7 +53,7 @@ public class Encode
[Theory] [Theory]
[InlineData(new byte[] { })] [InlineData(new byte[] { })]
[InlineData(null)] [InlineData(null)]
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[] testArray) public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[]? testArray)
{ {
var actualBase32 = Base32.Encode(testArray); var actualBase32 = Base32.Encode(testArray);
actualBase32.Should().Be(string.Empty); actualBase32.Should().Be(string.Empty);
@@ -57,7 +62,7 @@ public class Encode
[Theory] [Theory]
[InlineData(new byte[] { })] [InlineData(new byte[] { })]
[InlineData(null)] [InlineData(null)]
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[] testArray) public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[]? testArray)
{ {
char[] output = ['1', '2', '3', '4']; char[] output = ['1', '2', '3', '4'];

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -15,13 +15,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" /> <PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.4.2" /> <PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5"> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0"> <PackageReference Include="coverlet.collector" Version="6.0.4">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@@ -3,15 +3,15 @@ namespace Just.Core.Tests.GuidV8Tests;
public class NewGuid public class NewGuid
{ {
[Theory] [Theory]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
-25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
-25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449, -9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855, -8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454, -6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
@@ -22,7 +22,7 @@ public class NewGuid
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979, 4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099, 6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)] 8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449, -9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855, -8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454, -6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
@@ -33,7 +33,7 @@ public class NewGuid
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979, 4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099, 6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)] 8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781, 5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781,
36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932, 36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932,
76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414, 76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414,
@@ -44,7 +44,7 @@ public class NewGuid
292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171, 292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171,
324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343, 324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343,
379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)] 379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781, 5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781,
36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932, 36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932,
76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414, 76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414,
@@ -55,7 +55,7 @@ public class NewGuid
292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171, 292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171,
324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343, 324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343,
379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)] 379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)]
public void Guids_Differing_By_Minutes_Should_Be_Sortable(GuidV8Entropy entropy, params int[] seconds) public void Guids_Differing_By_Minutes_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
{ {
var rng = new Random(25); var rng = new Random(25);
var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc); var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
@@ -77,15 +77,15 @@ public class NewGuid
} }
[Theory] [Theory]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
-250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
-250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449, -9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855, -8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454, -6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
@@ -96,7 +96,7 @@ public class NewGuid
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979, 4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099, 6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)] 8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449, -9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855, -8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454, -6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
@@ -107,7 +107,7 @@ public class NewGuid
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979, 4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099, 6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)] 8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860, 6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514, 135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378, 200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
@@ -118,7 +118,7 @@ public class NewGuid
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469, 749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008, 830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)] 932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860, 6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514, 135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378, 200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
@@ -129,7 +129,7 @@ public class NewGuid
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469, 749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008, 830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)] 932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)]
public void Guids_Differing_By_Seconds_Should_Be_Sortable(GuidV8Entropy entropy, params int[] seconds) public void Guids_Differing_By_Seconds_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
{ {
var rng = new Random(25); var rng = new Random(25);
var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc); var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
@@ -151,15 +151,15 @@ public class NewGuid
} }
[Theory] [Theory]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
-250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
-250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449, -9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855, -8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454, -6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
@@ -170,7 +170,7 @@ public class NewGuid
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979, 4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099, 6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)] 8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449, -9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855, -8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454, -6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
@@ -181,7 +181,7 @@ public class NewGuid
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979, 4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099, 6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)] 8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
[InlineData(GuidV8Entropy.Weak, [InlineData(RngEntropy.Weak,
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860, 6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514, 135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378, 200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
@@ -192,7 +192,7 @@ public class NewGuid
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469, 749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008, 830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)] 932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)]
[InlineData(GuidV8Entropy.Strong, [InlineData(RngEntropy.Strong,
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860, 6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514, 135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378, 200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
@@ -203,7 +203,7 @@ public class NewGuid
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469, 749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008, 830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)] 932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)]
public void Guids_Differing_By_Milliseconds_Should_Be_Sortable(GuidV8Entropy entropy, params int[] seconds) public void Guids_Differing_By_Milliseconds_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
{ {
var rng = new Random(26); var rng = new Random(26);
var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc); var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);

View File

@@ -0,0 +1,140 @@
namespace Just.Core.Tests.SeqIdTests;
public class NextId
{
private static readonly DateTime TestEpoch = new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private readonly SeqId _seqId = new(TestEpoch);
private const int TimestampShift = 22; // 63 - 41 = 22
private const long TimestampMask = 0x1FFFFFFFFFF; // 41 bits mask
private const int SeqShift = 14; // TimestampShift - 8 = 14
private const long SeqMask = 0xFF; // 8 bits mask
private const long RandMask = 0x3FFF; // 14 bits mask (since 2^14 = 16384)
[Fact]
public void NextId_ShouldHaveCorrectBitStructure()
{
// Arrange
var time = TestEpoch.AddMilliseconds(500);
// Act
long id = _seqId.Next(time);
// Assert
long timestampPart = (id >> TimestampShift) & TimestampMask;
long sequencePart = (id >> SeqShift) & SeqMask;
long randomPart = id & RandMask;
timestampPart.Should().Be(500);
sequencePart.Should().Be(0);
randomPart.Should().BeInRange(0, RandMask);
}
[Fact]
public void NextId_ShouldIncrementSequenceForSameTimestamp()
{
// Arrange
var time = TestEpoch.AddMilliseconds(100);
// Act
long id1 = _seqId.Next(time);
long id2 = _seqId.Next(time);
// Assert
long sequence1 = (id1 >> SeqShift) & SeqMask;
long sequence2 = (id2 >> SeqShift) & SeqMask;
sequence2.Should().Be(sequence1 + 1);
}
[Fact]
public void NextId_ShouldResetSequenceForNewTimestamp()
{
// Arrange
var time1 = TestEpoch.AddMilliseconds(100);
var time2 = time1.AddMilliseconds(1);
// Act
_ = _seqId.Next(time1); // Sequence increments to 0
_ = _seqId.Next(time1); // Sequence increments to 1
long id = _seqId.Next(time2); // Should reset to 0
// Assert
long sequence = (id >> SeqShift) & SeqMask;
sequence.Should().Be(0);
}
[Fact]
public void NextId_ShouldThrowWhenTimestampDecreases()
{
// Arrange
var time1 = TestEpoch.AddMilliseconds(200);
var time2 = TestEpoch.AddMilliseconds(100);
// Act & Assert
_seqId.Next(time1); // First call sets last timestamp
Action act = () => _seqId.Next(time2);
act.Should().Throw<InvalidOperationException>()
.WithMessage("Refused to create new SeqId. Last timestamp is in the future.");
}
[Fact]
public void NextId_ShouldThrowWhenSequenceExhausted()
{
// Arrange
var time = TestEpoch.AddMilliseconds(200);
// Act & Assert
for (int i = 0; i < 255; i++)
{
_seqId.Next(time); // Exhauste sequence
}
Action act = () => _seqId.Next(time);
act.Should().Throw<IndexOutOfRangeException>()
.WithMessage("Refused to create new SeqId. Sequence exhausted.");
}
[Fact]
public void NextId_WithStrongEntropy_ShouldSetLower14Bits()
{
// Arrange
var time = TestEpoch.AddMilliseconds(300);
// Act
long id = _seqId.Next(time, RngEntropy.Strong);
// Assert
long randomPart = id & RandMask;
randomPart.Should().BeInRange(0, RandMask);
}
[Fact]
public void NextId_WithWeakEntropy_ShouldSetLower14Bits()
{
// Arrange
var time = TestEpoch.AddMilliseconds(400);
// Act
long id = _seqId.Next(time, RngEntropy.Weak);
// Assert
long randomPart = id & RandMask;
randomPart.Should().BeInRange(0, RandMask);
}
[Fact]
public void DefaultInstance_NextId_ShouldUseDefaultEpoch()
{
// Arrange
var now = DateTime.UtcNow;
var defaultEpoch = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
long expectedTimestamp = (long)(now - defaultEpoch).TotalMilliseconds;
// Act
long id = SeqId.NextId();
// Assert
long timestampPart = (id >> TimestampShift) & TimestampMask;
timestampPart.Should().BeCloseTo(expectedTimestamp & TimestampMask, 1); // Mask handles overflow
}
}

View File

@@ -4,17 +4,17 @@ public static class Base32
{ {
public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
public const char Padding = '='; public const char Padding = '=';
public const int MaxBytesStack = 250;
[Pure] [Pure]
public static string Encode(ReadOnlySpan<byte> input) public static string Encode(ReadOnlySpan<byte> input)
{ {
if (input.Length == 0) if (input.IsEmpty) return string.Empty;
{
return string.Empty;
}
int outLength = 8 * ((input.Length + 4) / 5); int outLength = 8 * ((input.Length + 4) / 5);
Span<char> output = stackalloc char[outLength]; Span<char> output = input.Length <= MaxBytesStack
? stackalloc char[outLength]
: new char[outLength];
_ = Encode(input, output); _ = Encode(input, output);
@@ -47,9 +47,13 @@ public static class Base32
[Pure] [Pure]
public static byte[] Decode(ReadOnlySpan<char> input) public static byte[] Decode(ReadOnlySpan<char> input)
{ {
input = input.TrimEnd(Padding);
if (input.IsEmpty) return []; if (input.IsEmpty) return [];
Span<byte> output = stackalloc byte[5 * input.Length / 8]; var outputLength = 5 * ((input.Length + 7) / 8);
Span<byte> output = outputLength <= MaxBytesStack
? stackalloc byte[outputLength]
: new byte[outputLength];
var size = Decode(input, output); var size = Decode(input, output);
@@ -60,7 +64,14 @@ public static class Base32
public static int Decode(ReadOnlySpan<char> input, Span<byte> output) public static int Decode(ReadOnlySpan<char> input, Span<byte> output)
{ {
input = input.TrimEnd(Padding); input = input.TrimEnd(Padding);
Span<char> inputspan = stackalloc char[input.Length];
var outputLength = 5 * ((input.Length + 7) / 8);
output = output[..outputLength];
output.Clear();
Span<char> inputspan = outputLength <= MaxBytesStack
? stackalloc char[input.Length]
: new char[input.Length];
input.ToUpperInvariant(inputspan); input.ToUpperInvariant(inputspan);
int bitIndex = 0; int bitIndex = 0;
@@ -106,7 +117,6 @@ public static class Base32
return outputIndex + (outputBits + 7) / 8; return outputIndex + (outputBits + 7) / 8;
} }
// returns the number of bytes that were output // returns the number of bytes that were output
[Pure] [Pure]
private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h) private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h)

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyName>Just.Core</AssemblyName> <AssemblyName>Just.Core</AssemblyName>
@@ -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,2024 JustFixMe</Copyright> <Copyright>Copyright (c) 2023-2025 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>

View File

@@ -1,44 +1,114 @@
namespace Just.Core.Extensions; namespace Just.Core.Extensions;
/// <summary>
/// Provides extension methods for <see cref="Stream"/> to fully populate buffers.
/// </summary>
public static class SystemIOStreamExtensions public static class SystemIOStreamExtensions
{ {
/// <summary>
/// Reads data from the stream until the specified section of the buffer is filled.
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <param name="buffer">The buffer to populate</param>
/// <param name="offset">The starting offset in the buffer</param>
/// <param name="length">The number of bytes to read</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</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="IOException">Thrown for I/O errors during reading</exception>
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>
/// Reads data from the stream until the entire buffer is filled.
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <param name="buffer">The buffer to populate</param>
/// <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="IOException">Thrown for I/O errors during reading</exception>
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>
/// Reads data from the stream until the specified span is filled.
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <param name="buffer">The span to populate</param>
/// <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="IOException">Thrown for I/O errors during reading</exception>
public static void Populate(this Stream stream, Span<byte> buffer) public static void Populate(this Stream stream, Span<byte> buffer)
{ {
while (buffer.Length > 0) ArgumentNullException.ThrowIfNull(stream);
{
var readed = stream.Read(buffer);
if (readed == 0) while (!buffer.IsEmpty)
{
var bytesRead = stream.Read(buffer);
if (bytesRead == 0)
{ {
throw new EndOfStreamException(); throw new EndOfStreamException();
} }
buffer = buffer[readed..]; buffer = buffer[bytesRead..];
} }
} }
public static async ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default) /// <summary>
=> await stream.PopulateAsync(buffer.AsMemory(), cancellationToken); /// Asynchronously reads data from the stream until the entire buffer is filled.
public static async ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default) /// </summary>
=> await stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken); /// <param name="stream">The stream to read from</param>
/// <param name="buffer">The buffer to populate</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A ValueTask representing the asynchronous operation</returns>
/// <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="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default)
=> stream.PopulateAsync(buffer.AsMemory(), cancellationToken);
/// <summary>
/// Asynchronously reads data from the stream until the specified section of the buffer is filled.
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <param name="buffer">The buffer to populate</param>
/// <param name="offset">The starting offset in the buffer</param>
/// <param name="length">The number of bytes to read</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A ValueTask representing the asynchronous operation</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</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="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default)
=> stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken);
/// <summary>
/// Asynchronously reads data from the stream until the specified memory region is filled.
/// </summary>
/// <param name="stream">The stream to read from</param>
/// <param name="buffer">The memory region to populate</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A ValueTask representing the asynchronous operation</returns>
/// <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="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
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)
{ {
while (buffer.Length > 0) ArgumentNullException.ThrowIfNull(stream);
while (!buffer.IsEmpty)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var readed = await stream.ReadAsync(buffer, cancellationToken); var bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
if (readed == 0) if (bytesRead == 0)
{ {
throw new EndOfStreamException(); throw new EndOfStreamException();
} }
buffer = buffer[readed..]; buffer = buffer[bytesRead..];
} }
} }
} }

View File

@@ -3,15 +3,13 @@ using System.Security.Cryptography;
namespace Just.Core; namespace Just.Core;
public enum GuidV8Entropy { Strong, Weak }
public static class GuidV8 public static class GuidV8
{ {
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Guid NewGuid(GuidV8Entropy entropy = GuidV8Entropy.Strong) => NewGuid(DateTime.UtcNow, entropy); public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
[Pure] [Pure]
public static Guid NewGuid(DateTime dateTime, GuidV8Entropy entropy = GuidV8Entropy.Strong) public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
{ {
var epoch = dateTime.Subtract(DateTime.UnixEpoch); var epoch = dateTime.Subtract(DateTime.UnixEpoch);
var timestamp = epoch.Ticks / (TimeSpan.TicksPerMillisecond / 10); var timestamp = epoch.Ticks / (TimeSpan.TicksPerMillisecond / 10);
@@ -24,7 +22,7 @@ public static class GuidV8
ts[0..2].CopyTo(bytes[4..6]); ts[0..2].CopyTo(bytes[4..6]);
ts[2..6].CopyTo(bytes[..4]); ts[2..6].CopyTo(bytes[..4]);
if (entropy == GuidV8Entropy.Strong) if (entropy == RngEntropy.Strong)
{ {
RandomNumberGenerator.Fill(bytes[6..]); RandomNumberGenerator.Fill(bytes[6..]);
} }

16
Core/RngEntropy.cs Normal file
View File

@@ -0,0 +1,16 @@
namespace Just.Core;
/// <summary>
/// Specifies the quality of random entropy used in ID generation
/// </summary>
public enum RngEntropy
{
/// <summary>
/// Cryptographically secure random numbers (slower but collision-resistant)
/// </summary>
Strong,
/// <summary>
/// Standard pseudo-random numbers (faster but less collision-resistant)
/// </summary>
Weak
}

121
Core/SeqId.cs Normal file
View File

@@ -0,0 +1,121 @@
using System.Security.Cryptography;
namespace Just.Core;
/// <summary>
/// Generates time-based sequential IDs with entropy
/// <para>ID Structure (64 bits):</para>
/// <list type="bullet">
/// <item><description>[1 bit] Always 0 (positive signed longs)</description></item>
/// <item><description>[41 bits] Milliseconds since epoch (covers ~69 years)</description></item>
/// <item><description>[8 bits] Sequence counter (0-255 per millisecond)</description></item>
/// <item><description>[14 bits] Random entropy (0-16383)</description></item>
/// </list>
/// </summary>
/// <remarks>
/// Important behaviors:
/// <list type="bullet">
/// <item><description>Guarantees monotonic ordering within same millisecond</description></item>
/// <item><description>Throws <see cref="InvalidOperationException"/> on backward time jumps</description></item>
/// <item><description>Sequence resets when timestamp advances</description></item>
/// <item><description>Thread-safe through locking</description></item>
/// </list>
/// </remarks>
public sealed class SeqId(DateTime epoch)
{
private const int TimestampBits = 41;
private const int TimestampShift = 63 - TimestampBits;
private const long TimestampMask = 0x000001FF_FFFFFFFF;
private const int SeqBits = 8;
private const int SeqShift = TimestampShift - SeqBits;
private const long SeqMask = 0x00000000_000000FF;
private const int RandExclusiveUpper = 1 << SeqShift;
/// <summary>
/// Default epoch (2025-01-01 UTC) used for <see cref="Default"/> instance
/// </summary>
public static DateTime DefaultEpoch { get; } = new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Default instance using <see cref="DefaultEpoch"/>
/// </summary>
public static SeqId Default { get; } = new(DefaultEpoch);
/// <summary>
/// Generates ID using default instance and current UTC time
/// </summary>
/// <param name="entropy">Entropy quality (default: Strong)</param>
/// <returns>64-bit sequential ID with random component</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown if more than 255 IDs generated in 1ms
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(DateTime.UtcNow, entropy);
#if NET9_0_OR_GREATER
private readonly Lock _lock = new();
#else
private readonly object _lock = new();
#endif
private readonly DateTime _epoch = epoch;
private int _seqId = 0;
private long _lastTimestamp = -1L;
/// <summary>
/// Generates next ID using current UTC time
/// </summary>
/// <param name="entropy">Entropy quality (default: Strong)</param>
/// <returns>64-bit sequential ID with random component</returns>
/// <exception cref="IndexOutOfRangeException">
/// Thrown if more than 255 IDs generated in 1ms
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(DateTime.UtcNow, entropy);
/// <summary>
/// Generates next ID with explicit timestamp
/// </summary>
/// <param name="dateTime">Timestamp basis for ID generation</param>
/// <param name="entropy">Entropy quality (default: Strong)</param>
/// <returns>64-bit sequential ID with random component</returns>
/// <exception cref="InvalidOperationException">
/// Thrown if <paramref name="dateTime"/> is earlier than last used timestamp
/// </exception>
/// <exception cref="IndexOutOfRangeException">
/// Thrown if more than 255 IDs generated in 1ms
/// </exception>
public long Next(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
{
var epoch = dateTime.Subtract(_epoch);
var timestamp = ((epoch.Ticks / TimeSpan.TicksPerMillisecond) & TimestampMask) << TimestampShift;
long currentSeq;
lock (_lock)
{
if (timestamp > _lastTimestamp)
{
_lastTimestamp = timestamp;
_seqId = 0;
}
else if (timestamp < _lastTimestamp)
{
throw new InvalidOperationException("Refused to create new SeqId. Last timestamp is in the future.");
}
if (_seqId == SeqMask)
{
throw new IndexOutOfRangeException("Refused to create new SeqId. Sequence exhausted.");
}
currentSeq = ((_seqId++) & SeqMask) << SeqShift;
}
long currentRand = entropy == RngEntropy.Strong
? RandomNumberGenerator.GetInt32(RandExclusiveUpper)
: Random.Shared.Next(RandExclusiveUpper);
return timestamp | currentSeq | currentRand;
}
}

View File

@@ -1,4 +1,4 @@
Copyright (c) 2023,2024 JustFixMe Copyright (c) 2023-2025 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