Compare commits
18 Commits
d565c48084
...
v1.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
e37e9b65c3
|
|||
|
8e961352d2
|
|||
|
566c813e8d
|
|||
| d11c74e5d6 | |||
| 85721b9769 | |||
| f7484b35e2 | |||
| a490a9b328 | |||
| 034a88ba8f | |||
| e28fc62b31 | |||
| 312219d42f | |||
| 7eb3008738 | |||
| 3665abaab8 | |||
| 2b68ba982d | |||
| 43135a5ffb | |||
| 2afd66aa57 | |||
| 71ec5b2f35 | |||
| 30af957fc6 | |||
| d60b94dbaa |
@@ -10,13 +10,16 @@ jobs:
|
|||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: https://github.com/actions/setup-dotnet@v3
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.x
|
dotnet-version: 10.x
|
||||||
|
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore Core/Core.csproj
|
run: dotnet restore Core/Core.csproj
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -14,28 +15,47 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
name: .NET tests
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: https://github.com/actions/setup-dotnet@v3
|
uses: https://github.com/actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.x
|
dotnet-version: |
|
||||||
|
8.0.x
|
||||||
|
9.0.x
|
||||||
|
10.0.x
|
||||||
|
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore --disable-parallel
|
||||||
|
|
||||||
- name: Build
|
- name: Build .NET 10.0
|
||||||
run: dotnet build --no-restore
|
run: dotnet build --no-restore --framework net10.0 --configuration Release ./Core.Tests/Core.Tests.csproj
|
||||||
|
|
||||||
- name: Test
|
- name: Build .NET 9.0
|
||||||
run: dotnet test --no-build --verbosity normal --logger trx --results-directory "TestResults-8.x"
|
run: dotnet build --no-restore --framework net9.0 --configuration Release ./Core.Tests/Core.Tests.csproj
|
||||||
|
|
||||||
|
- name: Build .NET 8.0
|
||||||
|
run: dotnet build --no-restore --framework net8.0 --configuration Release ./Core.Tests/Core.Tests.csproj
|
||||||
|
|
||||||
|
- name: Test .NET 10.0
|
||||||
|
run: dotnet run --no-build --framework net10.0 --configuration Release --project ./Core.Tests/Core.Tests.csproj -- -trx TestResults/results-net10.trx
|
||||||
|
|
||||||
|
- name: Test .NET 9.0
|
||||||
|
run: dotnet run --no-build --framework net9.0 --configuration Release --project ./Core.Tests/Core.Tests.csproj -- -trx TestResults/results-net9.trx
|
||||||
|
|
||||||
|
- name: Test .NET 8.0
|
||||||
|
run: dotnet run --no-build --framework net8.0 --configuration Release --project ./Core.Tests/Core.Tests.csproj -- -trx TestResults/results-net8.trx
|
||||||
|
|
||||||
- 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: test-results
|
||||||
path: TestResults-8.x
|
path: TestResults
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
retention-days: 30
|
retention-days: 30
|
||||||
|
|||||||
Vendored
+5
-2
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"dotnet.defaultSolution": "JustDotNet.Core.sln",
|
"dotnet.defaultSolution": "JustDotNet.Core.slnx",
|
||||||
"dotnetAcquisitionExtension.enableTelemetry": false
|
"omnisharp.enableEditorConfigSupport": true,
|
||||||
|
"dotnetAcquisitionExtension.enableTelemetry": false,
|
||||||
|
"dotnet.testWindow.useTestingPlatformProtocol": true,
|
||||||
|
"dotnet.formatting.organizeImportsOnFormat": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,27 +36,35 @@ 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.Should().BeEquivalentTo(testBytes);
|
resultBytes.ShouldBeEquivalentTo(testBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[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)
|
||||||
{
|
{
|
||||||
var actualBytesArray = Base32.Decode(str);
|
var actualBytesArray = Base32.Decode(str);
|
||||||
actualBytesArray.Should().Equal(expected);
|
actualBytesArray.ShouldBe(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[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, })]
|
||||||
@@ -44,7 +72,7 @@ public class Decode
|
|||||||
public void WhenCalledWithValidStringThatEndsWithPaddingSign_ShouldReturnValidByteArray(string testString, byte[] expected)
|
public void WhenCalledWithValidStringThatEndsWithPaddingSign_ShouldReturnValidByteArray(string testString, byte[] expected)
|
||||||
{
|
{
|
||||||
var actualBytesArray = Base32.Decode(testString);
|
var actualBytesArray = Base32.Decode(testString);
|
||||||
actualBytesArray.Should().Equal(expected);
|
actualBytesArray.ShouldBe(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -54,26 +82,75 @@ public class Decode
|
|||||||
public void WhenCalledWithValidStringWithoutPaddingSign_ShouldReturnValidByteArray(string testString, byte[] expected)
|
public void WhenCalledWithValidStringWithoutPaddingSign_ShouldReturnValidByteArray(string testString, byte[] expected)
|
||||||
{
|
{
|
||||||
var actualBytesArray = Base32.Decode(testString);
|
var actualBytesArray = Base32.Decode(testString);
|
||||||
actualBytesArray.Should().Equal(expected);
|
actualBytesArray.ShouldBe(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[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.Should().Throw<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]
|
||||||
[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).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')");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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===")]
|
||||||
@@ -23,17 +24,24 @@ 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);
|
||||||
var resultString = Base32.Encode(resultBytes);
|
var resultString = Base32.Encode(resultBytes);
|
||||||
|
var resultStringLowerCase = Base32.Encode(resultBytes, Base32EncodeOptions.LowerCase);
|
||||||
|
|
||||||
resultString.Should().BeEquivalentTo(testString);
|
resultString.ShouldBe(testString);
|
||||||
|
resultStringLowerCase.ShouldBe(testString.ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
[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, })]
|
||||||
@@ -42,15 +50,66 @@ 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);
|
||||||
str.Should().Be(expected);
|
var strLowerCase = Base32.Encode(testArray, Base32EncodeOptions.LowerCase);
|
||||||
}
|
|
||||||
|
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]
|
||||||
[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.ShouldBe(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new byte[] { })]
|
||||||
|
[InlineData(null)]
|
||||||
|
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[]? testArray)
|
||||||
|
{
|
||||||
|
char[] output = ['1', '2', '3', '4'];
|
||||||
|
|
||||||
|
var charsWritten = Base32.Encode(testArray, output);
|
||||||
|
|
||||||
|
charsWritten.ShouldBe(0);
|
||||||
|
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')");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class Decode
|
|||||||
[InlineData(554121)]
|
[InlineData(554121)]
|
||||||
[InlineData(100454567)]
|
[InlineData(100454567)]
|
||||||
[InlineData(3210589)]
|
[InlineData(3210589)]
|
||||||
public void WhenEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed)
|
public void WhenBytesEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed)
|
||||||
{
|
{
|
||||||
var rng = new Random(seed);
|
var rng = new Random(seed);
|
||||||
|
|
||||||
@@ -19,10 +19,47 @@ public class Decode
|
|||||||
var resultString = Base64Url.Encode(testBytes);
|
var resultString = Base64Url.Encode(testBytes);
|
||||||
var resultBytes = Base64Url.Decode(resultString);
|
var resultBytes = Base64Url.Decode(resultString);
|
||||||
|
|
||||||
resultBytes.Should().BeEquivalentTo(testBytes);
|
resultBytes.ShouldBeEquivalentTo(testBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(72121)]
|
||||||
|
[InlineData(554121)]
|
||||||
|
[InlineData(100454567)]
|
||||||
|
[InlineData(3210589)]
|
||||||
|
public void WhenLongEncodedToString_ShouldBeDecodedToTheSameLongArray(int seed)
|
||||||
|
{
|
||||||
|
var rng = new Random(seed);
|
||||||
|
|
||||||
|
for (int i = 1; i <= 512; i++)
|
||||||
|
{
|
||||||
|
var testLong = rng.NextInt64();
|
||||||
|
|
||||||
|
var resultString = Base64Url.Encode(testLong);
|
||||||
|
var resultLong = Base64Url.DecodeLong(resultString);
|
||||||
|
|
||||||
|
resultLong.ShouldBe(testLong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("RgGxr0_n1ZI", -7866126844696657594L)]
|
||||||
|
[InlineData("sxAPfpKB5kY", 5108913293478531251L)]
|
||||||
|
[InlineData("lO4_uitvLCg", 2894810894091415188L)]
|
||||||
|
[InlineData("awxjIqZWz10", 6759716837247880299L)]
|
||||||
|
[InlineData("VjNe72vug4U", -8825948697371200682L)]
|
||||||
|
[InlineData("AAAAAAAAAAA", 0L)]
|
||||||
|
[InlineData("__________8", -1L)]
|
||||||
|
[InlineData("AQAAAAAAAAA", 1L)]
|
||||||
|
[InlineData("CgAAAAAAAAA", 10L)]
|
||||||
|
[InlineData("6AMAAAAAAAA", 1000L)]
|
||||||
|
public void WhenCalled_ShouldReturnValidLong(string testString, long expected)
|
||||||
|
{
|
||||||
|
var result = Base64Url.DecodeLong(testString);
|
||||||
|
result.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("5QrdUxDUVkCAEGw8pvLsEw", "53dd0ae5-d410-4056-8010-6c3ca6f2ec13")]
|
[InlineData("5QrdUxDUVkCAEGw8pvLsEw", "53dd0ae5-d410-4056-8010-6c3ca6f2ec13")]
|
||||||
[InlineData("6nE2uKQ4_0ar9kpmybgkdw", "b83671ea-38a4-46ff-abf6-4a66c9b82477")]
|
[InlineData("6nE2uKQ4_0ar9kpmybgkdw", "b83671ea-38a4-46ff-abf6-4a66c9b82477")]
|
||||||
@@ -33,7 +70,7 @@ public class Decode
|
|||||||
{
|
{
|
||||||
var result = Base64Url.DecodeGuid(testString);
|
var result = Base64Url.DecodeGuid(testString);
|
||||||
var expected = Guid.Parse(expectedStr);
|
var expected = Guid.Parse(expectedStr);
|
||||||
result.Should().Be(expected);
|
result.ShouldBe(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -48,6 +85,51 @@ public class Decode
|
|||||||
public void WhenCalled_ShouldReturnValidBytes(string testString, byte[] expected)
|
public void WhenCalled_ShouldReturnValidBytes(string testString, byte[] expected)
|
||||||
{
|
{
|
||||||
var result = Base64Url.Decode(testString);
|
var result = Base64Url.Decode(testString);
|
||||||
result.Should().BeEquivalentTo(expected);
|
result.ShouldBeEquivalentTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(" ")]
|
||||||
|
[InlineData("hg2&515i3215")]
|
||||||
|
[InlineData("hg712)21")]
|
||||||
|
[InlineData("hg712f 21")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")]
|
||||||
|
public void WhenCalledWithInvalidString_ShouldThrowFormatException(string testString)
|
||||||
|
{
|
||||||
|
Action action = () => Base64Url.Decode(testString);
|
||||||
|
action.ShouldThrow<FormatException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("5QrdUxDUV CAEGw8pvLsEw")]
|
||||||
|
[InlineData("6nE2uKQ4$0ar9kpmybgkdw")]
|
||||||
|
[InlineData("PyD6zwDqXkG*S1HPsp41wQ")]
|
||||||
|
[InlineData("!dOlPOh3wEe9PlyQgTMt2g")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")]
|
||||||
|
public void WhenCalledWithInvalidGuidString_ShouldThrowFormatException(string testString)
|
||||||
|
{
|
||||||
|
Action action = () => Base64Url.DecodeGuid(testString);
|
||||||
|
action.ShouldThrow<FormatException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("RgG&r0_n1ZI")]
|
||||||
|
[InlineData("sxA fpKB5kY")]
|
||||||
|
[InlineData("lO4_uitvL)g")]
|
||||||
|
[InlineData("awxjIqZ^z10")]
|
||||||
|
[InlineData("VjNe7!vug4U")]
|
||||||
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")]
|
||||||
|
public void WhenCalledWithInvalidLongString_ShouldThrowFormatException(string testString)
|
||||||
|
{
|
||||||
|
Action action = () => Base64Url.DecodeLong(testString);
|
||||||
|
action.ShouldThrow<FormatException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string? testString)
|
||||||
|
{
|
||||||
|
Base64Url.Decode(testString).ShouldBeEmpty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,24 @@ public class Encode
|
|||||||
{
|
{
|
||||||
var testGuid = Guid.Parse(testGuidString);
|
var testGuid = Guid.Parse(testGuidString);
|
||||||
var result = Base64Url.Encode(testGuid);
|
var result = Base64Url.Encode(testGuid);
|
||||||
result.Should().Be(expected);
|
result.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("RgGxr0_n1ZI", -7866126844696657594L)]
|
||||||
|
[InlineData("sxAPfpKB5kY", 5108913293478531251L)]
|
||||||
|
[InlineData("lO4_uitvLCg", 2894810894091415188L)]
|
||||||
|
[InlineData("awxjIqZWz10", 6759716837247880299L)]
|
||||||
|
[InlineData("VjNe72vug4U", -8825948697371200682L)]
|
||||||
|
[InlineData("AAAAAAAAAAA", 0L)]
|
||||||
|
[InlineData("__________8", -1L)]
|
||||||
|
[InlineData("AQAAAAAAAAA", 1L)]
|
||||||
|
[InlineData("CgAAAAAAAAA", 10L)]
|
||||||
|
[InlineData("6AMAAAAAAAA", 1000L)]
|
||||||
|
public void WhenCalledWithLong_ShouldReturnValidString(string expected, long testLong)
|
||||||
|
{
|
||||||
|
var result = Base64Url.Encode(testLong);
|
||||||
|
result.ShouldBe(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -27,6 +44,28 @@ public class Encode
|
|||||||
public void WhenCalled_ShouldReturnValidString(string expected, byte[] testBytes)
|
public void WhenCalled_ShouldReturnValidString(string expected, byte[] testBytes)
|
||||||
{
|
{
|
||||||
var result = Base64Url.Encode(testBytes);
|
var result = Base64Url.Encode(testBytes);
|
||||||
result.Should().Be(expected);
|
result.ShouldBe(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new byte[] { })]
|
||||||
|
[InlineData(null)]
|
||||||
|
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[]? testArray)
|
||||||
|
{
|
||||||
|
var actualBase32 = Base64Url.Encode(testArray);
|
||||||
|
actualBase32.ShouldBe(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new byte[] { })]
|
||||||
|
[InlineData(null)]
|
||||||
|
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[]? testArray)
|
||||||
|
{
|
||||||
|
char[] output = ['1', '2', '3', '4'];
|
||||||
|
|
||||||
|
var charsWritten = Base64Url.Encode(testArray, output);
|
||||||
|
|
||||||
|
charsWritten.ShouldBe(0);
|
||||||
|
output.ShouldBe((char[])['1', '2', '3', '4']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
|
||||||
<AssemblyName>Just.Core.Tests</AssemblyName>
|
<AssemblyName>Just.Core.Tests</AssemblyName>
|
||||||
<RootNamespace>Just.Core.Tests</RootNamespace>
|
<RootNamespace>Just.Core.Tests</RootNamespace>
|
||||||
|
|
||||||
@@ -14,14 +15,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
<PackageReference Include="xunit.v3" Version="3.2.2" />
|
||||||
<PackageReference Include="xunit" Version="2.4.2" />
|
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
global using Xunit;
|
global using Xunit;
|
||||||
global using FluentAssertions;
|
global using Shouldly;
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
namespace Just.Core.Tests.GuidV8Tests;
|
||||||
|
|
||||||
|
public class ExtractTimestamp
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RoundTrip_ShouldReturnOriginalTimestamp()
|
||||||
|
{
|
||||||
|
var original = new DateTime(2026, 7, 10, 21, 30, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
var guid = GuidV8.NewGuid(original, RngEntropy.Weak);
|
||||||
|
var extracted = GuidV8.ExtractTimestamp(guid);
|
||||||
|
|
||||||
|
extracted.ShouldBe(original);
|
||||||
|
extracted.Kind.ShouldBe(DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundTrip_ShouldPreserve100MicrosecondPrecision()
|
||||||
|
{
|
||||||
|
// 100-microsecond precision: 1234567 * 100µs = 123456.7ms
|
||||||
|
var original = new DateTime(2024, 3, 15, 8, 45, 30, 123, DateTimeKind.Utc)
|
||||||
|
.AddTicks(4567); // sub-millisecond ticks
|
||||||
|
|
||||||
|
var guid = GuidV8.NewGuid(original, RngEntropy.Weak);
|
||||||
|
var extracted = GuidV8.ExtractTimestamp(guid);
|
||||||
|
|
||||||
|
// 123ms + 4567 ticks = 123ms + 456.7µs
|
||||||
|
// timestamp = (ticks since epoch) / 1000 (100µs units)
|
||||||
|
// Ticks: 123ms = 1,230,000 ticks; + 4,567 ticks = 1,234,567 ticks
|
||||||
|
// timestamp = 1,234,567 / 1000 = 1234 (truncates last 3 digits)
|
||||||
|
// recovered = 1234 * 1000 = 1,234,000 ticks = 123.4ms
|
||||||
|
// So the sub-100µs part (67 ticks) is lost
|
||||||
|
extracted.ShouldBe(new DateTime(2024, 3, 15, 8, 45, 30, 123, DateTimeKind.Utc)
|
||||||
|
.AddTicks(4000)); // 123.4ms
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundTrip_UnixEpoch_ShouldReturnExactEpoch()
|
||||||
|
{
|
||||||
|
var original = DateTime.UnixEpoch;
|
||||||
|
|
||||||
|
var guid = GuidV8.NewGuid(original, RngEntropy.Weak);
|
||||||
|
var extracted = GuidV8.ExtractTimestamp(guid);
|
||||||
|
|
||||||
|
extracted.ShouldBe(original);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundTrip_PreUnixEpoch_ShouldThrowArgumentException()
|
||||||
|
{
|
||||||
|
// Negative timestamps cannot be encoded correctly because the uint casts
|
||||||
|
// in NewGuid corrupt the sign. The encoding should reject them.
|
||||||
|
var preEpoch = new DateTime(1969, 7, 20, 20, 17, 40, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
Action act = () => GuidV8.NewGuid(preEpoch);
|
||||||
|
|
||||||
|
act.ShouldThrow<ArgumentException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RoundTrip_FarFuture_ShouldPreserveTimestamp()
|
||||||
|
{
|
||||||
|
// Year 2200 — well within 48-bit range
|
||||||
|
var original = new DateTime(2200, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
var guid = GuidV8.NewGuid(original, RngEntropy.Weak);
|
||||||
|
var extracted = GuidV8.ExtractTimestamp(guid);
|
||||||
|
|
||||||
|
extracted.ShouldBe(original);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractTimestamp_WithStrongEntropy_ShouldStillRecoverTimestamp()
|
||||||
|
{
|
||||||
|
var original = new DateTime(2025, 12, 25, 10, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
var guid = GuidV8.NewGuid(original, RngEntropy.Strong);
|
||||||
|
var extracted = GuidV8.ExtractTimestamp(guid);
|
||||||
|
|
||||||
|
extracted.ShouldBe(original);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonV8Guid_ShouldThrowArgumentException()
|
||||||
|
{
|
||||||
|
var guid = Guid.NewGuid(); // UUID v4
|
||||||
|
|
||||||
|
Action act = () => GuidV8.ExtractTimestamp(guid);
|
||||||
|
|
||||||
|
act.ShouldThrow<ArgumentException>()
|
||||||
|
.WithMessage("The provided GUID is not a UUID v8 (version=4). (Parameter 'guid')");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GuidEmpty_ShouldThrowArgumentException()
|
||||||
|
{
|
||||||
|
Action act = () => GuidV8.ExtractTimestamp(Guid.Empty);
|
||||||
|
|
||||||
|
act.ShouldThrow<ArgumentException>()
|
||||||
|
.WithMessage("The provided GUID is not a UUID v8 (version=0). (Parameter 'guid')");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MultipleRoundTrips_ShouldAllMatch()
|
||||||
|
{
|
||||||
|
var baseTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
var original = baseTime.AddMinutes(i * 37 + 13); // varied intervals
|
||||||
|
var guid = GuidV8.NewGuid(original, RngEntropy.Weak);
|
||||||
|
var extracted = GuidV8.ExtractTimestamp(guid);
|
||||||
|
|
||||||
|
extracted.ShouldBe(original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExtractTimestamp_ThenNewGuid_ShouldProduceSortableSequence()
|
||||||
|
{
|
||||||
|
var t1 = new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
var t2 = t1.AddSeconds(1);
|
||||||
|
var t3 = t2.AddSeconds(1);
|
||||||
|
|
||||||
|
var g1 = GuidV8.NewGuid(t1, RngEntropy.Weak);
|
||||||
|
var g2 = GuidV8.NewGuid(t2, RngEntropy.Weak);
|
||||||
|
var g3 = GuidV8.NewGuid(t3, RngEntropy.Weak);
|
||||||
|
|
||||||
|
var extracted1 = GuidV8.ExtractTimestamp(g1);
|
||||||
|
var extracted2 = GuidV8.ExtractTimestamp(g2);
|
||||||
|
var extracted3 = GuidV8.ExtractTimestamp(g3);
|
||||||
|
|
||||||
|
extracted1.ShouldBe(t1);
|
||||||
|
extracted2.ShouldBe(t2);
|
||||||
|
extracted3.ShouldBe(t3);
|
||||||
|
|
||||||
|
// Verify they sort in order
|
||||||
|
new[] { g3, g1, g2 }.Order().ShouldBe([g1, g2, g3]);
|
||||||
|
new[] { extracted3, extracted1, extracted2 }.Order().ShouldBe([t1, t2, t3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
-101
@@ -3,15 +3,43 @@ 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,
|
[InlineData(RngEntropy.Strong)]
|
||||||
|
public void Version_And_Variant_Should_Be_Correct(RngEntropy entropy)
|
||||||
|
{
|
||||||
|
var rng = new Random(25);
|
||||||
|
var referenceTime = new DateTime(2020, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
for (int i = 0; i < 2000; i++)
|
||||||
|
{
|
||||||
|
var timestamp = referenceTime.AddSeconds(rng.Next());
|
||||||
|
var result = GuidV8.NewGuid(timestamp, entropy);
|
||||||
|
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
result.Version.ShouldBe(8);
|
||||||
|
(result.Variant & 0b1100).ShouldBe(0b1000);
|
||||||
|
#else
|
||||||
|
var bytes = result.ToByteArray();
|
||||||
|
// Check version (bits 4-7 of the 7th byte)
|
||||||
|
var version = (bytes[7] >> 4) & 0x0F;
|
||||||
|
version.ShouldBe(8); // UUID version 8
|
||||||
|
// Check variant (bits 6-7 of the 8th byte)
|
||||||
|
var variant = bytes[8] >> 6;
|
||||||
|
variant.ShouldBe(0b10); // Standard UUID variant
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(RngEntropy.Weak,
|
||||||
|
-25000000, -20000000, -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)]
|
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 50000000, 100000000)]
|
||||||
[InlineData(GuidV8Entropy.Strong,
|
[InlineData(RngEntropy.Strong,
|
||||||
-25000000 -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
|
-25000000, -20000000, -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)]
|
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 50000000, 100000000)]
|
||||||
[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 +50,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 +61,81 @@ 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,
|
||||||
|
10024, 28660, 289641, 356015, 443164, 478759, 599586, 705860, 791271, 876512,
|
||||||
|
884503, 894899, 898584, 980136, 1007927, 1680328, 1690193, 1804615, 1847117, 2005534,
|
||||||
|
2106684, 2111936, 2252935, 2271396, 2298685, 2385409, 2414094, 2451706, 2549138, 2605538,
|
||||||
|
2864629, 2923476, 3004288, 3182875, 3266693, 3379019, 3542110, 3851467, 3871420, 4035463,
|
||||||
|
4316004, 4726381, 4814068, 4902666, 4979292, 4993443, 5117765, 5240585, 5249671, 5319528,
|
||||||
|
5387595, 5434544, 5504506, 5531264, 5546173, 5780381, 5889341, 6066328, 6167883, 6185073,
|
||||||
|
6299021, 6412136, 6621498, 6666562, 6741169, 6870279, 6949855, 6965427, 7153467, 7184150,
|
||||||
|
7300456, 7311381, 7413697, 7505235, 7802811, 7979134, 8053665, 8177676, 8260284, 8260773,
|
||||||
|
8269944, 8406526, 8442932, 8475162, 8555250, 8853347, 8861733, 8892200, 9069869, 9117839,
|
||||||
|
9225445, 9245837, 9378644, 9497874, 9553625, 9650968, 9704053, 9713592, 9715054, 9735988)]
|
||||||
|
[InlineData(RngEntropy.Strong,
|
||||||
|
10024, 28660, 289641, 356015, 443164, 478759, 599586, 705860, 791271, 876512,
|
||||||
|
884503, 894899, 898584, 980136, 1007927, 1680328, 1690193, 1804615, 1847117, 2005534,
|
||||||
|
2106684, 2111936, 2252935, 2271396, 2298685, 2385409, 2414094, 2451706, 2549138, 2605538,
|
||||||
|
2864629, 2923476, 3004288, 3182875, 3266693, 3379019, 3542110, 3851467, 3871420, 4035463,
|
||||||
|
4316004, 4726381, 4814068, 4902666, 4979292, 4993443, 5117765, 5240585, 5249671, 5319528,
|
||||||
|
5387595, 5434544, 5504506, 5531264, 5546173, 5780381, 5889341, 6066328, 6167883, 6185073,
|
||||||
|
6299021, 6412136, 6621498, 6666562, 6741169, 6870279, 6949855, 6965427, 7153467, 7184150,
|
||||||
|
7300456, 7311381, 7413697, 7505235, 7802811, 7979134, 8053665, 8177676, 8260284, 8260773,
|
||||||
|
8269944, 8406526, 8442932, 8475162, 8555250, 8853347, 8861733, 8892200, 9069869, 9117839,
|
||||||
|
9225445, 9245837, 9378644, 9497874, 9553625, 9650968, 9704053, 9713592, 9715054, 9735988)]
|
||||||
|
public void Guids_Differing_By_Minutes_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
|
||||||
|
{
|
||||||
|
var rng = new Random(25);
|
||||||
|
var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
|
||||||
|
SortedList<DateTime, Guid> expected = new(seconds.Length);
|
||||||
|
foreach (var s in seconds)
|
||||||
|
{
|
||||||
|
var timestamp = referenceTime.AddMinutes(s);
|
||||||
|
expected.Add(timestamp, GuidV8.NewGuid(timestamp, entropy));
|
||||||
|
}
|
||||||
|
|
||||||
|
var sut = expected.Values.ToArray();
|
||||||
|
rng.Shuffle(sut);
|
||||||
|
|
||||||
|
sut.Order().ShouldBe(expected.Select(x => x.Value));
|
||||||
|
sut.OrderBy(x => x.ToString()).ShouldBe(expected.Select(x => x.Value));
|
||||||
|
|
||||||
|
sut.OrderDescending().ShouldBe(expected.Reverse().Select(x => x.Value));
|
||||||
|
sut.OrderByDescending(x => x.ToString()).ShouldBe(expected.Reverse().Select(x => x.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(RngEntropy.Weak,
|
||||||
|
-100000000, -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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 200000000)]
|
||||||
|
[InlineData(RngEntropy.Strong,
|
||||||
|
-100000000, -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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 200000000)]
|
||||||
|
[InlineData(RngEntropy.Weak,
|
||||||
|
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
||||||
|
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
||||||
|
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
||||||
|
-4314, -3920, -3251, -3233, -3116, -2792, -2685, -2574, -2338, -2173,
|
||||||
|
-1934, -1914, -1528, -1483, -1410, -870, -757, -730, -263, -220,
|
||||||
|
151, 426, 588, 970, 1213, 1269, 1376, 1397, 1450, 1770,
|
||||||
|
2063, 2577, 2750, 2860, 3139, 3161, 3488, 3630, 3774, 4004,
|
||||||
|
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
||||||
|
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
||||||
|
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
||||||
|
[InlineData(RngEntropy.Strong,
|
||||||
|
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
||||||
|
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
||||||
|
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
||||||
|
-4314, -3920, -3251, -3233, -3116, -2792, -2685, -2574, -2338, -2173,
|
||||||
|
-1934, -1914, -1528, -1483, -1410, -870, -757, -730, -263, -220,
|
||||||
|
151, 426, 588, 970, 1213, 1269, 1376, 1397, 1450, 1770,
|
||||||
|
2063, 2577, 2750, 2860, 3139, 3161, 3488, 3630, 3774, 4004,
|
||||||
|
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
||||||
|
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
||||||
|
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
||||||
|
[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,
|
||||||
@@ -43,8 +145,8 @@ public class NewGuid
|
|||||||
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
||||||
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)]
|
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,
|
||||||
@@ -54,8 +156,8 @@ public class NewGuid
|
|||||||
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
||||||
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)]
|
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);
|
||||||
@@ -69,20 +171,23 @@ public class NewGuid
|
|||||||
var sut = expected.Values.ToArray();
|
var sut = expected.Values.ToArray();
|
||||||
rng.Shuffle(sut);
|
rng.Shuffle(sut);
|
||||||
|
|
||||||
sut.Order().Should().BeEquivalentTo(expected.Select(x => x.Value));
|
sut.Order().ShouldBe(expected.Select(x => x.Value));
|
||||||
sut.OrderDescending().Should().BeEquivalentTo(expected.Reverse().Select(x => x.Value));
|
sut.OrderBy(x => x.ToString()).ShouldBe(expected.Select(x => x.Value));
|
||||||
|
|
||||||
|
sut.OrderDescending().ShouldBe(expected.Reverse().Select(x => x.Value));
|
||||||
|
sut.OrderByDescending(x => x.ToString()).ShouldBe(expected.Reverse().Select(x => x.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(GuidV8Entropy.Weak,
|
[InlineData(RngEntropy.Weak,
|
||||||
-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)]
|
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
|
||||||
[InlineData(GuidV8Entropy.Strong,
|
[InlineData(RngEntropy.Strong,
|
||||||
-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)]
|
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,
|
||||||
@@ -93,7 +198,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,
|
||||||
@@ -104,7 +209,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,
|
||||||
@@ -114,8 +219,8 @@ public class NewGuid
|
|||||||
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
||||||
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)]
|
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,
|
||||||
@@ -125,10 +230,10 @@ public class NewGuid
|
|||||||
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
||||||
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)]
|
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(25);
|
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);
|
||||||
SortedList<DateTime, Guid> expected = new(seconds.Length);
|
SortedList<DateTime, Guid> expected = new(seconds.Length);
|
||||||
foreach (var s in seconds)
|
foreach (var s in seconds)
|
||||||
@@ -140,78 +245,10 @@ public class NewGuid
|
|||||||
var sut = expected.Values.ToArray();
|
var sut = expected.Values.ToArray();
|
||||||
rng.Shuffle(sut);
|
rng.Shuffle(sut);
|
||||||
|
|
||||||
sut.Order().Should().BeEquivalentTo(expected.Select(x => x.Value));
|
sut.Order().ShouldBe(expected.Select(x => x.Value));
|
||||||
sut.OrderDescending().Should().BeEquivalentTo(expected.Reverse().Select(x => x.Value));
|
sut.OrderBy(x => x.ToString()).ShouldBe(expected.Select(x => x.Value));
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
sut.OrderDescending().ShouldBe(expected.Reverse().Select(x => x.Value));
|
||||||
[InlineData(GuidV8Entropy.Weak,
|
sut.OrderByDescending(x => x.ToString()).ShouldBe(expected.Reverse().Select(x => x.Value));
|
||||||
-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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000)]
|
|
||||||
[InlineData(GuidV8Entropy.Strong,
|
|
||||||
-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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000)]
|
|
||||||
[InlineData(GuidV8Entropy.Weak,
|
|
||||||
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
|
||||||
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
|
||||||
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
|
||||||
-4314, -3920, -3251, -3233, -3116, -2792, -2685, -2574, -2338, -2173,
|
|
||||||
-1934, -1914, -1528, -1483, -1410, -870, -757, -730, -263, -220,
|
|
||||||
151, 426, 588, 970, 1213, 1269, 1376, 1397, 1450, 1770,
|
|
||||||
2063, 2577, 2750, 2860, 3139, 3161, 3488, 3630, 3774, 4004,
|
|
||||||
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
|
||||||
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
|
||||||
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
|
||||||
[InlineData(GuidV8Entropy.Strong,
|
|
||||||
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
|
||||||
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
|
||||||
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
|
||||||
-4314, -3920, -3251, -3233, -3116, -2792, -2685, -2574, -2338, -2173,
|
|
||||||
-1934, -1914, -1528, -1483, -1410, -870, -757, -730, -263, -220,
|
|
||||||
151, 426, 588, 970, 1213, 1269, 1376, 1397, 1450, 1770,
|
|
||||||
2063, 2577, 2750, 2860, 3139, 3161, 3488, 3630, 3774, 4004,
|
|
||||||
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
|
||||||
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
|
||||||
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
|
||||||
[InlineData(GuidV8Entropy.Weak,
|
|
||||||
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
|
|
||||||
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
|
|
||||||
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
|
|
||||||
320988273, 322638028, 324110126, 326855208, 332533719, 336668313, 350798512, 366520367, 380595181, 405033666,
|
|
||||||
410929500, 414099488, 417697882, 421269768, 431907031, 435262715, 442168482, 449797406, 458508845, 489039529,
|
|
||||||
503344305, 514934509, 537515867, 555210743, 563951463, 578783864, 580560518, 580902916, 605801607, 635231377,
|
|
||||||
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
|
||||||
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
|
|
||||||
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
|
|
||||||
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484)]
|
|
||||||
[InlineData(GuidV8Entropy.Strong,
|
|
||||||
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
|
|
||||||
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
|
|
||||||
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
|
|
||||||
320988273, 322638028, 324110126, 326855208, 332533719, 336668313, 350798512, 366520367, 380595181, 405033666,
|
|
||||||
410929500, 414099488, 417697882, 421269768, 431907031, 435262715, 442168482, 449797406, 458508845, 489039529,
|
|
||||||
503344305, 514934509, 537515867, 555210743, 563951463, 578783864, 580560518, 580902916, 605801607, 635231377,
|
|
||||||
636537435, 637241486, 676077268, 691630262, 696158602, 714007596, 715537265, 718873134, 724266320, 741049828,
|
|
||||||
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
|
|
||||||
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
|
|
||||||
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484)]
|
|
||||||
public void Guids_Differing_By_Microseconds_Should_Be_Sortable(GuidV8Entropy entropy, params int[] seconds)
|
|
||||||
{
|
|
||||||
var rng = new Random(25);
|
|
||||||
var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
|
|
||||||
SortedList<DateTime, Guid> expected = new(seconds.Length);
|
|
||||||
foreach (var s in seconds)
|
|
||||||
{
|
|
||||||
var timestamp = referenceTime.AddMicroseconds(s);
|
|
||||||
expected.Add(timestamp, GuidV8.NewGuid(timestamp, entropy));
|
|
||||||
}
|
|
||||||
|
|
||||||
var sut = expected.Values.ToArray();
|
|
||||||
rng.Shuffle(sut);
|
|
||||||
|
|
||||||
sut.Order().Should().BeEquivalentTo(expected.Select(x => x.Value));
|
|
||||||
sut.OrderDescending().Should().BeEquivalentTo(expected.Reverse().Select(x => x.Value));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
|
||||||
|
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.ShouldBe(500);
|
||||||
|
sequencePart.ShouldBe(0);
|
||||||
|
randomPart.ShouldBeInRange(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.ShouldBe(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.ShouldBe(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.ShouldThrow<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 < 256; i++)
|
||||||
|
{
|
||||||
|
_seqId.Next(time); // Use all 256 sequence values (0-255)
|
||||||
|
}
|
||||||
|
Action act = () => _seqId.Next(time); // 257th call should throw
|
||||||
|
act.ShouldThrow<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.ShouldBeInRange(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.ShouldBeInRange(0, RandMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultInstance_NextId_ShouldUseDefaultEpoch()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var now = new DateTime(2026, 6, 3, 21, 11, 1, DateTimeKind.Utc);
|
||||||
|
SeqId.Default.UnsafeReplaceDefaultTimeFactory(() => now);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
long id = SeqId.NextId();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
long expectedTimestamp = GetExpectedTimestamp(now);
|
||||||
|
|
||||||
|
long timestampPart = (id >> TimestampShift) & TimestampMask;
|
||||||
|
timestampPart.ShouldBeInRange(expectedTimestamp, expectedTimestamp + 1);
|
||||||
|
|
||||||
|
static long GetExpectedTimestamp(DateTime now) => ((long)(now - SeqId.DefaultEpoch).TotalMilliseconds) & TimestampMask; // Mask handles overflow
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Just.Core.Tests;
|
||||||
|
|
||||||
|
public static class ShouldlyExtensions
|
||||||
|
{
|
||||||
|
public static TException WithMessage<TException>(this TException exception, string expectedMessage, string? customMessage = null)
|
||||||
|
where TException : Exception
|
||||||
|
{
|
||||||
|
exception.Message.ShouldBe(expectedMessage, customMessage: customMessage);
|
||||||
|
return exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Just.Core.Extensions;
|
||||||
|
|
||||||
|
namespace Just.Core.Tests.SystemIOStreamExtensionsTests;
|
||||||
|
|
||||||
|
public class Populate
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 0)]
|
||||||
|
[InlineData(0, 1)]
|
||||||
|
[InlineData(0, 3)]
|
||||||
|
[InlineData(0, 5)]
|
||||||
|
[InlineData(3, 0)]
|
||||||
|
[InlineData(3, 1)]
|
||||||
|
[InlineData(3, 5)]
|
||||||
|
[InlineData(5, 0)]
|
||||||
|
[InlineData(5, 1)]
|
||||||
|
[InlineData(5, 5)]
|
||||||
|
public void WhenCalled_ShouldPopulateSpecifiedRange(int offset, int length)
|
||||||
|
{
|
||||||
|
byte[] streamContent = [0x01, 0x02, 0x03, 0x04, 0x05,];
|
||||||
|
using var stream = new MemoryStream(streamContent);
|
||||||
|
var buffer = new byte[10];
|
||||||
|
|
||||||
|
stream.Populate(buffer, offset, length);
|
||||||
|
|
||||||
|
buffer.Skip(offset).Take(length).ShouldBe(streamContent.Take(length));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, }, 4)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 4)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 5)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 5)]
|
||||||
|
public void WhenStreamContainsSameOrGreaterAmmountOfItems_ShouldPopulateBuffer(byte[] streamContent, int bufferSize)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(streamContent);
|
||||||
|
var buffer = new byte[bufferSize];
|
||||||
|
|
||||||
|
stream.Populate(buffer);
|
||||||
|
|
||||||
|
buffer.ShouldBe(streamContent.Take(bufferSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, }, 5)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 6)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 10)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 9)]
|
||||||
|
public void WhenStreamContainsLessItems_ShouldThrowEndOfStreamException(byte[] streamContent, int bufferSize)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(streamContent);
|
||||||
|
var buffer = new byte[bufferSize];
|
||||||
|
|
||||||
|
Action action = () => stream.Populate(buffer);
|
||||||
|
|
||||||
|
action.ShouldThrow<EndOfStreamException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using Just.Core.Extensions;
|
||||||
|
|
||||||
|
namespace Just.Core.Tests.SystemIOStreamExtensionsTests;
|
||||||
|
|
||||||
|
public class PopulateAsync
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task WhenCancellationRequested_ShouldThrowOperationCanceledException()
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
byte[] streamContent = [0x01, 0x02, 0x03, 0x04, 0x05,];
|
||||||
|
using var stream = new MemoryStream(streamContent);
|
||||||
|
var buffer = new byte[5];
|
||||||
|
|
||||||
|
Func<Task> action = async () => await stream.PopulateAsync(buffer, cts.Token);
|
||||||
|
cts.Cancel();
|
||||||
|
|
||||||
|
await action.ShouldThrowAsync<OperationCanceledException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 0)]
|
||||||
|
[InlineData(0, 1)]
|
||||||
|
[InlineData(0, 3)]
|
||||||
|
[InlineData(0, 5)]
|
||||||
|
[InlineData(3, 0)]
|
||||||
|
[InlineData(3, 1)]
|
||||||
|
[InlineData(3, 5)]
|
||||||
|
[InlineData(5, 0)]
|
||||||
|
[InlineData(5, 1)]
|
||||||
|
[InlineData(5, 5)]
|
||||||
|
public async Task WhenCalled_ShouldPopulateSpecifiedRange(int offset, int length)
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||||
|
byte[] streamContent = [0x01, 0x02, 0x03, 0x04, 0x05,];
|
||||||
|
using var stream = new MemoryStream(streamContent);
|
||||||
|
var buffer = new byte[10];
|
||||||
|
|
||||||
|
await stream.PopulateAsync(buffer, offset, length, cts.Token);
|
||||||
|
|
||||||
|
buffer.Skip(offset).Take(length).ShouldBe(streamContent.Take(length));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, }, 4)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 4)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 5)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 5)]
|
||||||
|
public async Task WhenStreamContainsSameOrGreaterAmmountOfItems_ShouldPopulateBuffer(byte[] streamContent, int bufferSize)
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
|
||||||
|
using var stream = new MemoryStream(streamContent);
|
||||||
|
var buffer = new byte[bufferSize];
|
||||||
|
|
||||||
|
await stream.PopulateAsync(buffer, cts.Token);
|
||||||
|
|
||||||
|
buffer.ShouldBe(streamContent.Take(bufferSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, }, 5)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 6)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 10)]
|
||||||
|
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 9)]
|
||||||
|
public async Task WhenStreamContainsLessItems_ShouldThrowEndOfStreamException(byte[] streamContent, int bufferSize)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(streamContent);
|
||||||
|
var buffer = new byte[bufferSize];
|
||||||
|
|
||||||
|
Func<Task> action = async () => await stream.PopulateAsync(buffer);
|
||||||
|
|
||||||
|
await action.ShouldThrowAsync<EndOfStreamException>();
|
||||||
|
}
|
||||||
|
}
|
||||||
+149
-38
@@ -1,54 +1,140 @@
|
|||||||
namespace Just.Core;
|
namespace Just.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encoding options for <see cref="Base32.Encode(ReadOnlySpan{byte}, Base32EncodeOptions)"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum Base32EncodeOptions
|
||||||
|
{
|
||||||
|
/// <summary>Standard RFC 4648 encoding (uppercase with padding).</summary>
|
||||||
|
None = 0x00,
|
||||||
|
/// <summary>Use lowercase alphabet.</summary>
|
||||||
|
LowerCase = 0x01,
|
||||||
|
/// <summary>Omit padding characters.</summary>
|
||||||
|
NoPadding = 0x02,
|
||||||
|
/// <summary>Lowercase alphabet without padding (combines <see cref="LowerCase"/> | <see cref="NoPadding"/>).</summary>
|
||||||
|
LowerCaseNoPadding = 0x03,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFC 4648 Base32 encoder/decoder with span-based APIs.
|
||||||
|
/// Uses stack allocation for inputs up to <see cref="MaxBytesStack"/> bytes; heap allocation otherwise.
|
||||||
|
/// </summary>
|
||||||
public static class Base32
|
public static class Base32
|
||||||
{
|
{
|
||||||
|
/// <summary>RFC 4648 Base32 alphabet (uppercase A-Z, 2-7).</summary>
|
||||||
public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||||
|
/// <summary>Lowercase variant of the RFC 4648 alphabet.</summary>
|
||||||
|
public const string AlphabetLower = "abcdefghijklmnopqrstuvwxyz234567";
|
||||||
|
/// <summary>Padding character (=).</summary>
|
||||||
public const char Padding = '=';
|
public const char Padding = '=';
|
||||||
|
/// <summary>Maximum input byte count that uses stack allocation instead of heap.</summary>
|
||||||
|
public const int MaxBytesStack = 250;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes a byte span into a Base32 string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">The bytes to encode.</param>
|
||||||
|
/// <param name="options">Encoding options (casing, padding). Defaults to standard RFC 4648.</param>
|
||||||
|
/// <returns>The Base32-encoded string.</returns>
|
||||||
[Pure]
|
[Pure]
|
||||||
public static string Encode(ReadOnlySpan<byte> input)
|
public static string Encode(ReadOnlySpan<byte> input, Base32EncodeOptions options = Base32EncodeOptions.None)
|
||||||
{
|
{
|
||||||
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
|
||||||
|
? new char[outLength]
|
||||||
|
: stackalloc char[outLength];
|
||||||
|
|
||||||
_ = Encode(input, output);
|
var size = Encode(input, output, options);
|
||||||
|
|
||||||
return new string(output);
|
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;
|
||||||
|
|
||||||
|
int outputLength = 8 * ((input.Length + 4) / 5);
|
||||||
|
if (output.Length < outputLength)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Encoded input can not fit in output span.", nameof(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
output = output[..outputLength];
|
||||||
|
|
||||||
|
ReadOnlySpan<char> alphabet = (options & Base32EncodeOptions.LowerCase) == Base32EncodeOptions.LowerCase
|
||||||
|
? AlphabetLower
|
||||||
|
: Alphabet;
|
||||||
|
Span<byte> alphabetKeys = stackalloc byte[8];
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
ReadOnlySpan<char> alphabet = Alphabet;
|
|
||||||
for (int offset = 0; offset < input.Length;)
|
for (int offset = 0; offset < input.Length;)
|
||||||
{
|
{
|
||||||
int numCharsToOutput = GetNextGroup(input, ref 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);
|
alphabetKeys.Clear();
|
||||||
|
int numCharsToOutput = GetNextGroup(input, ref offset, alphabetKeys);
|
||||||
|
|
||||||
output[i++] = (numCharsToOutput > 0) ? alphabet[a] : Padding;
|
output[i++] = alphabet[alphabetKeys[0]];
|
||||||
output[i++] = (numCharsToOutput > 1) ? alphabet[b] : Padding;
|
output[i++] = alphabet[alphabetKeys[1]];
|
||||||
output[i++] = (numCharsToOutput > 2) ? alphabet[c] : Padding;
|
|
||||||
output[i++] = (numCharsToOutput > 3) ? alphabet[d] : Padding;
|
if (numCharsToOutput < 3)
|
||||||
output[i++] = (numCharsToOutput > 4) ? alphabet[e] : Padding;
|
{
|
||||||
output[i++] = (numCharsToOutput > 5) ? alphabet[f] : Padding;
|
i = FillWithPadding(output, i, numCharsToOutput, options);
|
||||||
output[i++] = (numCharsToOutput > 6) ? alphabet[g] : Padding;
|
break;
|
||||||
output[i++] = (numCharsToOutput > 7) ? alphabet[h] : 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]
|
||||||
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 / 8;
|
||||||
|
Span<byte> output = outputLength > MaxBytesStack
|
||||||
|
? new byte[outputLength]
|
||||||
|
: stackalloc byte[outputLength];
|
||||||
|
|
||||||
var size = Decode(input, output);
|
var size = Decode(input, output);
|
||||||
|
|
||||||
return output[..size].ToArray();
|
return output[..size].ToArray();
|
||||||
@@ -58,7 +144,25 @@ 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 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)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Decoded input can not fit in output span.", nameof(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
output = output[..outputLength];
|
||||||
|
output.Clear();
|
||||||
|
|
||||||
|
Span<char> inputspan = outputLength > MaxBytesStack
|
||||||
|
? new char[input.Length]
|
||||||
|
: stackalloc char[input.Length];
|
||||||
input.ToUpperInvariant(inputspan);
|
input.ToUpperInvariant(inputspan);
|
||||||
|
|
||||||
int bitIndex = 0;
|
int bitIndex = 0;
|
||||||
@@ -77,7 +181,7 @@ public static class Base32
|
|||||||
{
|
{
|
||||||
throw new FormatException("Provided string contains invalid characters.");
|
throw new FormatException("Provided string contains invalid characters.");
|
||||||
}
|
}
|
||||||
|
|
||||||
bitPos = 5 - bitIndex;
|
bitPos = 5 - bitIndex;
|
||||||
outBitPos = 8 - outputBits;
|
outBitPos = 8 - outputBits;
|
||||||
bits = bitPos < outBitPos ? bitPos : outBitPos;
|
bits = bitPos < outBitPos ? bitPos : outBitPos;
|
||||||
@@ -98,16 +202,22 @@ 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
|
||||||
[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, Span<byte> alphabetKeys)
|
||||||
{
|
{
|
||||||
var retVal = (input.Length - offset) switch
|
var retVal = (input.Length - offset) switch
|
||||||
{
|
{
|
||||||
@@ -117,20 +227,21 @@ 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;
|
||||||
uint b5 = (offset < input.Length) ? input[offset++] : 0U;
|
uint b5 = (offset < input.Length) ? input[offset++] : 0U;
|
||||||
|
|
||||||
a = (byte)(b1 >> 3);
|
alphabetKeys[0] = (byte)(b1 >> 3);
|
||||||
b = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
|
alphabetKeys[1] = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
|
||||||
c = (byte)((b2 >> 1) & 0x1f);
|
alphabetKeys[2] = (byte)((b2 >> 1) & 0x1f);
|
||||||
d = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
|
alphabetKeys[3] = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
|
||||||
e = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
|
alphabetKeys[4] = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
|
||||||
f = (byte)((b4 >> 2) & 0x1f);
|
alphabetKeys[5] = (byte)((b4 >> 2) & 0x1f);
|
||||||
g = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
|
alphabetKeys[6] = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
|
||||||
h = (byte)(b5 & 0x1f);
|
alphabetKeys[7] = (byte)(b5 & 0x1f);
|
||||||
|
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-40
@@ -1,67 +1,99 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Just.Core;
|
namespace Just.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL-safe Base64 encoder/decoder for bytes, <see cref="long"/>, and <see cref="Guid"/>.
|
||||||
|
/// Uses the standard Base64Url character set (- and _ instead of + and /), padding stripped by default.
|
||||||
|
/// </summary>
|
||||||
public static class Base64Url
|
public static class Base64Url
|
||||||
{
|
{
|
||||||
|
private const char Padding = '=';
|
||||||
|
|
||||||
|
/// <summary>Decodes an 11-character Base64Url string into a <see cref="long"/>.</summary>
|
||||||
|
[Pure] public static long DecodeLong(ReadOnlySpan<char> value)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 11);
|
||||||
|
|
||||||
|
Span<byte> longBytes = stackalloc byte[8];
|
||||||
|
Span<char> chars = stackalloc char[12];
|
||||||
|
|
||||||
|
value.CopyTo(chars);
|
||||||
|
chars[^1] = Padding;
|
||||||
|
|
||||||
|
ReplaceNonUrlChars(chars);
|
||||||
|
|
||||||
|
if (!Convert.TryFromBase64Chars(chars, longBytes, out int _))
|
||||||
|
throw new FormatException("Invalid Base64 string.");
|
||||||
|
|
||||||
|
return MemoryMarshal.Read<long>(longBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Decodes a 22-character Base64Url string into a <see cref="Guid"/>.</summary>
|
||||||
[Pure] public static Guid DecodeGuid(ReadOnlySpan<char> value)
|
[Pure] public static Guid DecodeGuid(ReadOnlySpan<char> value)
|
||||||
{
|
{
|
||||||
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 22);
|
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 22);
|
||||||
|
|
||||||
Span<byte> guidBytes = stackalloc byte[16];
|
Span<byte> guidBytes = stackalloc byte[16];
|
||||||
Span<char> chars = stackalloc char[24];
|
Span<char> chars = stackalloc char[24];
|
||||||
|
|
||||||
value.CopyTo(chars);
|
value.CopyTo(chars);
|
||||||
for (int i = 0; i < value.Length; i++)
|
chars[^2..].Fill(Padding);
|
||||||
{
|
|
||||||
switch (value[i])
|
ReplaceNonUrlChars(chars);
|
||||||
{
|
|
||||||
case '-': chars[i] = '+'; continue;
|
|
||||||
case '_': chars[i] = '/'; continue;
|
|
||||||
default: continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chars[^2..].Fill('=');
|
|
||||||
if (!Convert.TryFromBase64Chars(chars, guidBytes, out int _))
|
if (!Convert.TryFromBase64Chars(chars, guidBytes, out int _))
|
||||||
throw new FormatException("Invalid Base64 string.");
|
throw new FormatException("Invalid Base64 string.");
|
||||||
|
|
||||||
return new Guid(guidBytes);
|
return new Guid(guidBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Decodes a Base64Url string into a byte array.</summary>
|
||||||
[Pure] public static byte[] Decode(ReadOnlySpan<char> input)
|
[Pure] public static byte[] Decode(ReadOnlySpan<char> input)
|
||||||
{
|
{
|
||||||
if (input.IsEmpty) return [];
|
if (input.IsEmpty) return [];
|
||||||
|
|
||||||
Span<byte> output = stackalloc byte[3 * ((input.Length + 3) / 4)];
|
Span<byte> output = stackalloc byte[3 * ((input.Length + 3) / 4)];
|
||||||
|
|
||||||
var size = Decode(input, output);
|
var size = Decode(input, output);
|
||||||
|
|
||||||
return output[..size].ToArray();
|
return output[..size].ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Decodes a Base64Url string into a pre-allocated byte span. Returns the number of bytes written.</summary>
|
||||||
[Pure] public static int Decode(ReadOnlySpan<char> value, Span<byte> output)
|
[Pure] public static int Decode(ReadOnlySpan<char> value, Span<byte> output)
|
||||||
{
|
{
|
||||||
var padding = (4 - (value.Length & 3)) & 3;
|
var padding = (4 - (value.Length & 3)) & 3;
|
||||||
var charlen = value.Length + padding;
|
var charlen = value.Length + padding;
|
||||||
var outputBytes = charlen / 4;
|
var outputBytes = 3 * (charlen / 4);
|
||||||
ArgumentOutOfRangeException.ThrowIfLessThan(output.Length, outputBytes);
|
ArgumentOutOfRangeException.ThrowIfLessThan(output.Length, outputBytes);
|
||||||
Span<char> chars = stackalloc char[charlen];
|
Span<char> chars = stackalloc char[charlen];
|
||||||
|
|
||||||
value.CopyTo(chars);
|
value.CopyTo(chars);
|
||||||
for (int i = 0; i < value.Length; i++)
|
chars[^padding..].Fill(Padding);
|
||||||
{
|
|
||||||
switch (value[i])
|
ReplaceNonUrlChars(chars);
|
||||||
{
|
|
||||||
case '-': chars[i] = '+'; continue;
|
|
||||||
case '_': chars[i] = '/'; continue;
|
|
||||||
default: continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chars[^padding..].Fill('=');
|
|
||||||
|
|
||||||
if (!Convert.TryFromBase64Chars(chars, output, out outputBytes))
|
if (!Convert.TryFromBase64Chars(chars, output, out outputBytes))
|
||||||
throw new FormatException("Invalid Base64 string.");
|
throw new FormatException("Invalid Base64 string.");
|
||||||
|
|
||||||
return outputBytes;
|
return outputBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Encodes a <see cref="long"/> into an 11-character Base64Url string.</summary>
|
||||||
|
[Pure] public static string Encode(in long id)
|
||||||
|
{
|
||||||
|
Span<byte> longBytes = stackalloc byte[8];
|
||||||
|
MemoryMarshal.Write(longBytes, id);
|
||||||
|
|
||||||
|
Span<char> chars = stackalloc char[12];
|
||||||
|
Convert.TryToBase64Chars(longBytes, chars, out int _);
|
||||||
|
ReplaceUrlChars(chars[..^1]);
|
||||||
|
|
||||||
|
return new string(chars[..^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Encodes a <see cref="Guid"/> into a 22-character Base64Url string.</summary>
|
||||||
[Pure] public static string Encode(in Guid id)
|
[Pure] public static string Encode(in Guid id)
|
||||||
{
|
{
|
||||||
Span<byte> guidBytes = stackalloc byte[16];
|
Span<byte> guidBytes = stackalloc byte[16];
|
||||||
@@ -69,50 +101,69 @@ public static class Base64Url
|
|||||||
Span<char> chars = stackalloc char[24];
|
Span<char> chars = stackalloc char[24];
|
||||||
Convert.TryToBase64Chars(guidBytes, chars, out int _);
|
Convert.TryToBase64Chars(guidBytes, chars, out int _);
|
||||||
|
|
||||||
for (int i = 0; i < chars.Length - 2; i++)
|
ReplaceUrlChars(chars[..^2]);
|
||||||
{
|
|
||||||
switch (chars[i])
|
|
||||||
{
|
|
||||||
case '+': chars[i] = '-'; continue;
|
|
||||||
case '/': chars[i] = '_'; continue;
|
|
||||||
default: continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new string(chars[..^2]);
|
return new string(chars[..^2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Encodes a byte span into a Base64Url string.</summary>
|
||||||
[Pure] public static string Encode(ReadOnlySpan<byte> input)
|
[Pure] public static string Encode(ReadOnlySpan<byte> input)
|
||||||
{
|
{
|
||||||
if (input.IsEmpty) return string.Empty;
|
if (input.IsEmpty) return string.Empty;
|
||||||
|
|
||||||
int outLength = 8 * ((input.Length + 5) / 6);
|
int outLength = 4 * ((input.Length + 2) / 3);
|
||||||
Span<char> output = stackalloc char[outLength];
|
Span<char> output = stackalloc char[outLength];
|
||||||
|
|
||||||
int strlen = Encode(input, output);
|
int strlen = Encode(input, output);
|
||||||
return new string(output[..strlen]);
|
return new string(output[..strlen]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Encodes a byte span into a pre-allocated char span. Returns the number of characters written.</summary>
|
||||||
[Pure] public static int Encode(ReadOnlySpan<byte> input, Span<char> output)
|
[Pure] public static int Encode(ReadOnlySpan<byte> input, Span<char> output)
|
||||||
{
|
{
|
||||||
var charlen = 8 * ((input.Length + 5) / 6);
|
if (input.IsEmpty) return 0;
|
||||||
|
|
||||||
|
var charlen = 4 * ((input.Length + 2) / 3);
|
||||||
Span<char> chars = stackalloc char[charlen];
|
Span<char> chars = stackalloc char[charlen];
|
||||||
Convert.TryToBase64Chars(input, chars, out int charsWritten);
|
Convert.TryToBase64Chars(input, chars, out int charsWritten);
|
||||||
|
|
||||||
int i;
|
int i = ReplaceUrlChars(chars[..charsWritten]);
|
||||||
for (i = 0; i < charsWritten; i++)
|
chars[..i].CopyTo(output);
|
||||||
|
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReplaceUrlChars(Span<char> chars)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
for (; i < chars.Length; i++)
|
||||||
{
|
{
|
||||||
switch (chars[i])
|
switch (chars[i])
|
||||||
{
|
{
|
||||||
case '+': chars[i] = '-'; continue;
|
case '+': chars[i] = '-'; continue;
|
||||||
case '/': chars[i] = '_'; continue;
|
case '/': chars[i] = '_'; continue;
|
||||||
case '=': goto exitLoop;
|
case Padding: goto break_loop;
|
||||||
default: continue;
|
default: continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
exitLoop:
|
break_loop:
|
||||||
chars[..i].CopyTo(output);
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReplaceNonUrlChars(Span<char> chars)
|
||||||
|
{
|
||||||
|
int i = 0;
|
||||||
|
for (; i < chars.Length; i++)
|
||||||
|
{
|
||||||
|
switch (chars[i])
|
||||||
|
{
|
||||||
|
case '-': chars[i] = '+'; continue;
|
||||||
|
case '_': chars[i] = '/'; continue;
|
||||||
|
case Padding: goto break_loop;
|
||||||
|
default: continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break_loop:
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A 2D grid with lazy min/max caching, cloning, and binary stream serialization.
|
||||||
|
/// Requires <typeparamref name="T"/> to support comparison and equality operators.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The element type; must implement <c>IComparisonOperators<T, T, bool></c> and <c>IEqualityOperators<T, T, bool></c>.</typeparam>
|
||||||
public class DataMap<T> : Map<T>, IDataMap<T>, ICloneable
|
public class DataMap<T> : Map<T>, IDataMap<T>, ICloneable
|
||||||
where T : IComparisonOperators<T, T, bool>, IEqualityOperators<T, T, bool>
|
where T : IComparisonOperators<T, T, bool>, IEqualityOperators<T, T, bool>
|
||||||
{
|
{
|
||||||
@@ -122,14 +126,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 +161,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 +169,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System.Numerics;
|
|||||||
|
|
||||||
namespace Just.Core.Collections;
|
namespace Just.Core.Collections;
|
||||||
|
|
||||||
|
/// <summary>Read-only interface for a 2D grid with min/max tracking and cloning.</summary>
|
||||||
|
/// <typeparam name="T">The element type; must support comparison and equality operators.</typeparam>
|
||||||
public interface IDataMap<T> : IMap<T>, ICloneable
|
public interface IDataMap<T> : IMap<T>, ICloneable
|
||||||
where T : IComparisonOperators<T, T, bool>, IEqualityOperators<T, T, bool>
|
where T : IComparisonOperators<T, T, bool>, IEqualityOperators<T, T, bool>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
namespace Just.Core.Collections;
|
namespace Just.Core.Collections;
|
||||||
|
|
||||||
|
/// <summary>Read-only interface for a 2D grid with coordinate-based access and enumeration.</summary>
|
||||||
|
/// <typeparam name="T">The type of elements in the map.</typeparam>
|
||||||
public interface IMap<T> : IReadOnlyCollection<MapPoint<T>>
|
public interface IMap<T> : IReadOnlyCollection<MapPoint<T>>
|
||||||
{
|
{
|
||||||
int Width { get; }
|
int Width { get; }
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace Just.Core.Collections;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an immutable, ordered sequence of items with value‑equality 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 value‑based 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> :
|
||||||
|
IEnumerable<T>,
|
||||||
|
IReadOnlyList<T>,
|
||||||
|
IEquatable<ImmutableSequence<T>>
|
||||||
|
{
|
||||||
|
private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals;
|
||||||
|
private readonly ImmutableList<T> _values;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new empty instance of the <see cref="ImmutableSequence{T}"/> class.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <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];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ImmutableSequence{T}"/> class with
|
||||||
|
/// the elements from the provided read‑only span.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">The items to include in the sequence.</param>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of elements in the sequence.
|
||||||
|
/// </summary>
|
||||||
|
public int Count => _values.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the element at the specified zero‑based index.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">The zero‑based index of the element to get.</param>
|
||||||
|
/// <returns>The element at the specified index.</returns>
|
||||||
|
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];
|
||||||
|
/// <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]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var (offset, count) = range.GetOffsetAndLength(_values.Count);
|
||||||
|
return ConstructNew(_values.GetRange(offset, count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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];
|
||||||
|
|
||||||
|
/// <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));
|
||||||
|
/// <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));
|
||||||
|
|
||||||
|
/// <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();
|
||||||
|
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator();
|
||||||
|
|
||||||
|
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 element‑wise 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)
|
||||||
|
{
|
||||||
|
if (other is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReferenceEquals(this, other))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_values.Count != other._values.Count)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _values.Count; i++)
|
||||||
|
{
|
||||||
|
if (!CompareItem(_values[i], other._values[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>);
|
||||||
|
/// <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()
|
||||||
|
{
|
||||||
|
HashCode hash = new();
|
||||||
|
|
||||||
|
foreach (var value in _values)
|
||||||
|
{
|
||||||
|
hash.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
@@ -2,6 +2,11 @@ using System.Collections;
|
|||||||
|
|
||||||
namespace Just.Core.Collections;
|
namespace Just.Core.Collections;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A 2D grid container providing indexer access, enumeration as <see cref="MapPoint{T}"/> values,
|
||||||
|
/// and conversion to a 2D array. Coordinates are clamped to valid ranges.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of elements in the map.</typeparam>
|
||||||
public class Map<T> : IMap<T>
|
public class Map<T> : IMap<T>
|
||||||
{
|
{
|
||||||
internal readonly T[] _values;
|
internal readonly T[] _values;
|
||||||
@@ -68,8 +73,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,5 +1,7 @@
|
|||||||
namespace Just.Core.Collections;
|
namespace Just.Core.Collections;
|
||||||
|
|
||||||
|
/// <summary>A value and its coordinates within a <see cref="IMap{T}"/>.</summary>
|
||||||
|
/// <typeparam name="T">The type of the map element.</typeparam>
|
||||||
public readonly struct MapPoint<T>(T value, int x, int y, IMap<T> map)
|
public readonly struct MapPoint<T>(T value, int x, int y, IMap<T> map)
|
||||||
{
|
{
|
||||||
public readonly T Value = value;
|
public readonly T Value = value;
|
||||||
|
|||||||
+4
-2
@@ -1,16 +1,18 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AssemblyName>Just.Core</AssemblyName>
|
<AssemblyName>Just.Core</AssemblyName>
|
||||||
<RootNamespace>Just.Core</RootNamespace>
|
<RootNamespace>Just.Core</RootNamespace>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
|
|
||||||
<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 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>
|
||||||
|
|||||||
@@ -1,46 +1,121 @@
|
|||||||
namespace Just.Core.Extensions;
|
namespace Just.Core.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides extension methods for <see cref="Stream"/> to fully populate buffers.
|
||||||
|
/// </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>
|
||||||
|
/// 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>
|
||||||
|
[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>
|
||||||
|
/// 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>
|
||||||
|
[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>
|
||||||
|
/// 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>
|
||||||
|
[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)
|
||||||
{
|
{
|
||||||
do
|
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..];
|
||||||
}
|
}
|
||||||
while (buffer.Length > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>
|
||||||
|
[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)
|
||||||
|
=> 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>
|
||||||
|
[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)
|
||||||
|
=> 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>
|
||||||
|
[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)
|
||||||
{
|
{
|
||||||
do
|
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..];
|
||||||
}
|
}
|
||||||
while (buffer.Length > 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+65
-22
@@ -3,38 +3,81 @@ 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)]
|
private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10; // 100-microsecond units
|
||||||
public static Guid NewGuid(GuidV8Entropy entropy = GuidV8Entropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
|
|
||||||
|
|
||||||
[Pure]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Guid NewGuid(DateTime dateTime, GuidV8Entropy entropy = GuidV8Entropy.Strong)
|
[ExcludeFromCodeCoverage]
|
||||||
|
public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
|
||||||
|
|
||||||
|
public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
|
||||||
{
|
{
|
||||||
var timestamp = dateTime.ToBinary() & 0x3FFF_FFFF_FFFF_FFFF;
|
var epoch = dateTime.Subtract(DateTime.UnixEpoch);
|
||||||
|
var timestamp = epoch.Ticks / TicksPrecision;
|
||||||
|
|
||||||
Span<byte> ts = stackalloc byte[8];
|
// Negative timestamps can't be encoded correctly due to unsigned bit operations
|
||||||
MemoryMarshal.Write(ts, timestamp);
|
if (timestamp < 0)
|
||||||
|
|
||||||
Span<byte> bytes = stackalloc byte[16];
|
|
||||||
|
|
||||||
ts[4..].CopyTo(bytes[..4]);
|
|
||||||
ts[2..4].CopyTo(bytes[4..6]);
|
|
||||||
ts[..2].CopyTo(bytes[6..8]);
|
|
||||||
|
|
||||||
bytes[7] = (byte)((bytes[7] & 0x0f) | 0x80);
|
|
||||||
|
|
||||||
if (entropy == GuidV8Entropy.Strong)
|
|
||||||
{
|
{
|
||||||
RandomNumberGenerator.Fill(bytes[8..]);
|
throw new ArgumentException("Timestamp must be on or after UnixEpoch (1970-01-01 UTC).", nameof(dateTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint tsHigh = (uint)((timestamp >> 16) & 0xFFFFFFFF);
|
||||||
|
ushort tsLow = (ushort)(timestamp & 0x0000FFFF);
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[10];
|
||||||
|
|
||||||
|
if (entropy == RngEntropy.Strong)
|
||||||
|
{
|
||||||
|
RandomNumberGenerator.Fill(bytes);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Random.Shared.NextBytes(bytes[8..]);
|
Random.Shared.NextBytes(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Guid(bytes);
|
bytes[0] = (byte)((bytes[0] & 0x0F) | 0x80); // Version 8
|
||||||
|
bytes[2] = (byte)((bytes[2] & 0x1F) | 0x80); // Variant 0b1000
|
||||||
|
|
||||||
|
ushort version = (ushort)((bytes[0] << 8) | bytes[1]);
|
||||||
|
|
||||||
|
return new Guid(
|
||||||
|
tsHigh,
|
||||||
|
tsLow,
|
||||||
|
version,
|
||||||
|
bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the timestamp from a UUID v8 generated by <see cref="NewGuid(DateTime, RngEntropy)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guid">The UUID v8 to extract the timestamp from.</param>
|
||||||
|
/// <returns>The <see cref="DateTime"/> (UTC) encoded in the GUID's timestamp fields.</returns>
|
||||||
|
/// <exception cref="ArgumentException">
|
||||||
|
/// Thrown if <paramref name="guid"/> is not a UUID v8.
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// The returned timestamp has 100-microsecond precision (the resolution used by
|
||||||
|
/// <see cref="NewGuid(DateTime, RngEntropy)"/>). Sub-100µs components of the original
|
||||||
|
/// <see cref="DateTime"/> are not recoverable.
|
||||||
|
/// </remarks>
|
||||||
|
[Pure]
|
||||||
|
public static DateTime ExtractTimestamp(Guid guid)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[16];
|
||||||
|
guid.TryWriteBytes(bytes);
|
||||||
|
|
||||||
|
// Version is the high nibble of byte 7 (parameter 'c' high byte, little-endian byte 7)
|
||||||
|
var version = bytes[7] >> 4;
|
||||||
|
if (version != 8)
|
||||||
|
throw new ArgumentException($"The provided GUID is not a UUID v8 (version={version}).", nameof(guid));
|
||||||
|
|
||||||
|
// tsHigh is stored as Int32 at bytes 0-3
|
||||||
|
uint tsHigh = MemoryMarshal.Read<uint>(bytes);
|
||||||
|
// tsLow is stored as Int16 at bytes 4-5
|
||||||
|
ushort tsLow = MemoryMarshal.Read<ushort>(bytes[4..]);
|
||||||
|
|
||||||
|
long timestamp = ((long)tsHigh << 16) | tsLow;
|
||||||
|
return DateTime.UnixEpoch.AddTicks(timestamp * TicksPrecision);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
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 256 IDs generated in 1ms
|
||||||
|
/// </exception>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(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;
|
||||||
|
private Func<DateTime> _defaultTimeFactory = static () => DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <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 256 IDs generated in 1ms
|
||||||
|
/// </exception>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(_defaultTimeFactory(), 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 256 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void UnsafeReplaceDefaultTimeFactory(Func<DateTime> timeFactory) => _defaultTimeFactory = timeFactory;
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="Core.Tests/Core.Tests.csproj" />
|
||||||
|
<Project Path="Core/Core.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2023 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
|
||||||
|
|||||||
@@ -1,18 +1,92 @@
|
|||||||
# .Net library with helpers, functions and extensions
|
# Just.Core
|
||||||
|
|
||||||
Just some stuff that is used in different projects...
|
Small .NET library with useful helper classes, functions, and extensions — stuff used across multiple projects.
|
||||||
|
|
||||||
|
[](https://www.nuget.org/packages/Just.Core)
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotnet add package Just.Core
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ```Map``` and ```DataMap``` collection types
|
### Encoding
|
||||||
- Extensions for ```System.String``` and ```System.IO.Stream```
|
|
||||||
- Small converters for **Base32** and **Base64Url** encodings
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Install from NuGet.org
|
**Base32** — RFC 4648 encoder/decoder with lowercase and no-padding options.
|
||||||
|
|
||||||
```sh
|
```csharp
|
||||||
# install the package using NuGet
|
var encoded = Base32.Encode(bytes); // "ABCDEFGH..."
|
||||||
dotnet add package Just.Core
|
var encoded = Base32.Encode(bytes, Base32EncodeOptions.LowerCaseNoPadding);
|
||||||
|
var bytes = Base32.Decode("ABCDEFGH...");
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Base64Url** — URL-safe Base64 for bytes, `long`, and `Guid`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var str = Base64Url.Encode(myGuid); // "5QrdUxDUVkCAEGw8pvLsEw"
|
||||||
|
var guid = Base64Url.DecodeGuid(str);
|
||||||
|
var str = Base64Url.Encode(123456789L); // "7NcVAAAAAA"
|
||||||
|
var val = Base64Url.DecodeLong(str);
|
||||||
|
```
|
||||||
|
|
||||||
|
### IDs
|
||||||
|
|
||||||
|
**SeqId** — Time-based 64-bit sequential ID with configurable entropy. Thread-safe.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var id = SeqId.NextId(); // default instance, Strong entropy
|
||||||
|
var id = SeqId.NextId(RngEntropy.Weak); // faster, less collision-resistant
|
||||||
|
|
||||||
|
// Custom epoch
|
||||||
|
var generator = new SeqId(new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
var id = generator.Next();
|
||||||
|
```
|
||||||
|
|
||||||
|
Structure: `[1 bit reserved][41 bits ms since epoch][8 bits sequence][14 bits random]`
|
||||||
|
|
||||||
|
**GuidV8** — Time-sortable UUID v8 with extractable timestamp.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var guid = GuidV8.NewGuid(); // current UTC, Strong entropy
|
||||||
|
var guid = GuidV8.NewGuid(specificTime);
|
||||||
|
var when = GuidV8.ExtractTimestamp(guid); // recover timestamp (100µs precision)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collections
|
||||||
|
|
||||||
|
**Map<T> / DataMap<T>** — 2D grid container with indexing, enumeration, and min/max.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var map = new Map<int>(width: 10, height: 10, values);
|
||||||
|
var value = map[3, 5]; // integer or double coordinates
|
||||||
|
var point = map.Get(3.5, 5.2); // MapPoint<T> with position metadata
|
||||||
|
|
||||||
|
var dmap = new DataMap<double>(values, 10, 10);
|
||||||
|
var min = dmap.Min; // MapPoint<double> — cached
|
||||||
|
var max = dmap.Max; // lazy, thread-safe on first access
|
||||||
|
|
||||||
|
// Stream serialization (sync + async)
|
||||||
|
dmap.WriteToStream(stream);
|
||||||
|
var copy = DataMap.ReadFromStream<double>(stream);
|
||||||
|
```
|
||||||
|
|
||||||
|
**ImmutableSequence<T>** — Immutable list with structural value equality.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var seq = new ImmutableSequence<int>([1, 2, 3]);
|
||||||
|
var seq2 = seq.Add(4); // new instance, seq unchanged
|
||||||
|
seq == seq2; // false — 3 vs 4 elements
|
||||||
|
new ImmutableSequence<int>([1, 2, 3]) == seq; // true — value equality
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Targets
|
||||||
|
|
||||||
|
- .NET 8.0 (LTS)
|
||||||
|
- .NET 9.0
|
||||||
|
- .NET 10.0
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](LICENSE).
|
||||||
|
|||||||
Reference in New Issue
Block a user