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