Skip to content

Commit a0f84ac

Browse files
committed
add Base2
1 parent 23d81ba commit a0f84ac

File tree

6 files changed

+286
-31
lines changed

6 files changed

+286
-31
lines changed

README.md

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -69,45 +69,47 @@ Small buffer sizes are used (64 characters). They are closer to real life
6969
applications. Base58 performs really bad in decoding of larger buffer sizes,
7070
due to polynomial complexity of numeric base conversions.
7171

72-
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3915)
72+
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.4061)
7373
AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores
74-
.NET SDK 9.0.203
75-
[Host] : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
76-
DefaultJob : .NET 8.0.15 (8.0.1525.16413), X64 RyuJIT AVX2
74+
.NET SDK 9.0.300
75+
[Host] : .NET 8.0.16 (8.0.1625.21506), X64 RyuJIT AVX2
76+
DefaultJob : .NET 8.0.16 (8.0.1625.21506), X64 RyuJIT AVX2
7777

7878
Encoding (64 byte buffer)
7979

80-
| Method | Mean | Error | StdDev | Gen0 | Allocated |
81-
|---------------------------- |----------:|---------:|---------:|-------:|----------:|
82-
| DotNet_Base64 | 28.52 ns | 0.501 ns | 0.469 ns | 0.0119 | 200 B |
83-
| Base16_UpperCase | 83.05 ns | 1.432 ns | 1.340 ns | 0.0167 | 280 B |
84-
| Multibase_Base16_UpperCase | 100.51 ns | 1.671 ns | 1.563 ns | 0.0334 | 560 B |
85-
| Base32_CrockfordWithPadding | 148.46 ns | 0.715 ns | 0.634 ns | 0.0138 | 232 B |
86-
| Base36_LowerCase | 44.05 ns | 0.039 ns | 0.037 ns | - | - |
87-
| Base45_Default | 121.33 ns | 0.702 ns | 0.657 ns | 0.0129 | 216 B |
88-
| Base58_Bitcoin | 44.62 ns | 0.297 ns | 0.232 ns | 0.0091 | 152 B |
89-
| Base58_Monero | 208.37 ns | 3.417 ns | 3.656 ns | 0.0119 | 200 B |
90-
| Base62_Default | 43.61 ns | 0.141 ns | 0.132 ns | - | - |
91-
| Base85_Z85 | 148.95 ns | 1.231 ns | 1.151 ns | 0.0110 | 184 B |
92-
| Base256Emoji_Default | 224.16 ns | 1.189 ns | 1.112 ns | 0.0167 | 280 B |
80+
| Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
81+
|---------------------------- |----------:|---------:|---------:|----------:|-------:|----------:|
82+
| DotNet_Base64 | 28.48 ns | 0.417 ns | 0.326 ns | 28.40 ns | 0.0119 | 200 B |
83+
| Base2_Default | 231.01 ns | 4.585 ns | 5.458 ns | 228.47 ns | 0.0625 | 1048 B |
84+
| Base16_UpperCase | 81.93 ns | 1.590 ns | 1.487 ns | 81.48 ns | 0.0167 | 280 B |
85+
| Multibase_Base16_UpperCase | 102.39 ns | 2.000 ns | 1.964 ns | 102.61 ns | 0.0334 | 560 B |
86+
| Base32_CrockfordWithPadding | 154.63 ns | 0.956 ns | 0.847 ns | 154.46 ns | 0.0138 | 232 B |
87+
| Base36_LowerCase | 46.77 ns | 0.983 ns | 1.346 ns | 46.80 ns | 0.0091 | 152 B |
88+
| Base45_Default | 125.77 ns | 1.949 ns | 1.823 ns | 126.01 ns | 0.0129 | 216 B |
89+
| Base58_Bitcoin | 46.75 ns | 0.961 ns | 1.106 ns | 46.46 ns | 0.0091 | 152 B |
90+
| Base58_Monero | 206.48 ns | 2.797 ns | 2.616 ns | 205.31 ns | 0.0119 | 200 B |
91+
| Base62_Default | 46.56 ns | 0.981 ns | 1.499 ns | 45.71 ns | 0.0091 | 152 B |
92+
| Base85_Z85 | 162.14 ns | 0.424 ns | 0.376 ns | 162.24 ns | 0.0110 | 184 B |
93+
| Base256Emoji_Default | 227.15 ns | 2.671 ns | 2.499 ns | 226.80 ns | 0.0167 | 280 B |
9394

9495
Decoding (80 character string, except Base45 which must use an 81 character string)
9596

9697
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Allocated |
9798
|------------------------------------- |------------:|----------:|----------:|-------:|-------:|----------:|
98-
| DotNet_Base64 | 105.76 ns | 1.258 ns | 1.177 ns | 0.0052 | - | 88 B |
99-
| Base16_UpperCase | 49.91 ns | 0.494 ns | 0.462 ns | 0.0038 | - | 64 B |
100-
| Base16_UpperCase_TextReader | 308.32 ns | 6.134 ns | 5.438 ns | 0.5007 | 0.0153 | 8376 B |
101-
| Multibase_Base16_UpperCase | 51.04 ns | 0.417 ns | 0.390 ns | 0.0038 | - | 64 B |
102-
| Multibase_TryDecode_Base16_UpperCase | 46.19 ns | 0.162 ns | 0.151 ns | - | - | - |
103-
| Base32_Crockford | 141.81 ns | 1.785 ns | 1.669 ns | 0.0048 | - | 80 B |
104-
| Base36_LowerCase | 4,039.85 ns | 7.350 ns | 6.875 ns | - | - | 80 B |
105-
| Base45_Default | 89.65 ns | 0.513 ns | 0.479 ns | 0.0048 | - | 80 B |
106-
| Base58_Bitcoin | 3,688.39 ns | 28.256 ns | 23.595 ns | 0.0038 | - | 88 B |
107-
| Base58_Monero | 109.46 ns | 1.426 ns | 1.334 ns | 0.0052 | - | 88 B |
108-
| Base62_Default | 4,563.37 ns | 39.847 ns | 37.273 ns | - | - | 88 B |
109-
| Base85_Z85 | 253.36 ns | 1.720 ns | 1.525 ns | 0.0052 | - | 88 B |
110-
| Base256Emoji_Default | 291.45 ns | 2.437 ns | 2.280 ns | 0.0062 | - | 104 B |
99+
| DotNet_Base64 | 103.55 ns | 1.500 ns | 1.403 ns | 0.0052 | - | 88 B |
100+
| Base2_Default | 103.01 ns | 0.395 ns | 0.369 ns | 0.0024 | - | 40 B |
101+
| Base16_UpperCase | 49.13 ns | 0.113 ns | 0.094 ns | 0.0038 | - | 64 B |
102+
| Base16_UpperCase_TextReader | 250.19 ns | 1.761 ns | 1.561 ns | 0.5007 | 0.0153 | 8376 B |
103+
| Multibase_Base16_UpperCase | 50.54 ns | 0.197 ns | 0.164 ns | 0.0038 | - | 64 B |
104+
| Multibase_TryDecode_Base16_UpperCase | 47.81 ns | 0.125 ns | 0.111 ns | - | - | - |
105+
| Base32_Crockford | 127.08 ns | 1.226 ns | 1.087 ns | 0.0048 | - | 80 B |
106+
| Base36_LowerCase | 4,050.63 ns | 18.155 ns | 16.094 ns | - | - | 80 B |
107+
| Base45_Default | 70.74 ns | 0.377 ns | 0.294 ns | 0.0048 | - | 80 B |
108+
| Base58_Bitcoin | 4,465.86 ns | 22.477 ns | 21.025 ns | - | - | 88 B |
109+
| Base58_Monero | 107.15 ns | 0.421 ns | 0.329 ns | 0.0052 | - | 88 B |
110+
| Base62_Default | 4,503.95 ns | 10.409 ns | 8.692 ns | - | - | 88 B |
111+
| Base85_Z85 | 257.27 ns | 1.279 ns | 1.196 ns | 0.0052 | - | 88 B |
112+
| Base256Emoji_Default | 287.07 ns | 1.805 ns | 1.600 ns | 0.0062 | - | 104 B |
111113

112114
Notes
113115
-----

benchmark/DecoderBenchmarks.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ public class DecoderBenchmarks
1414
readonly string ms = 'F' + new string('a', 80);
1515
readonly string base45str = new('A', 81);
1616
readonly string emojiStr = string.Concat(Enumerable.Repeat("🚀", 80));
17+
readonly string binaryEncoded = new('0', 80);
1718
readonly MemoryStream memoryStream = new();
1819
static readonly byte[] buffer = new byte[80];
1920

2021
[Benchmark]
2122
public byte[] DotNet_Base64() => Convert.FromBase64String(s);
2223

24+
[Benchmark]
25+
public byte[] Base2_Default() => Base2.Default.Decode(binaryEncoded);
26+
2327
[Benchmark]
2428
public byte[] Base16_UpperCase() => Base16.UpperCase.Decode(s);
2529

benchmark/EncoderBenchmarks.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public class EncoderBenchmarks
1313
[Benchmark]
1414
public string DotNet_Base64() => Convert.ToBase64String(buffer);
1515

16+
[Benchmark]
17+
public string Base2_Default() => Base2.Default.Encode(buffer);
18+
1619
[Benchmark]
1720
public string Base16_UpperCase() => Base16.UpperCase.Encode(buffer);
1821

src/Base2.cs

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// <copyright file="Base2.cs" company="Sedat Kapanoglu">
2+
// Copyright (c) 2014-2025 Sedat Kapanoglu
3+
// Licensed under Apache-2.0 License (see LICENSE.txt file for details)
4+
// </copyright>
5+
6+
using System;
7+
using System.IO;
8+
using System.Threading.Tasks;
9+
10+
namespace SimpleBase;
11+
12+
/// <summary>
13+
/// Base2 implementation that encodes a byte array into 1 and 0's by converting
14+
/// each byte indiviudually by encoding most-significant byte first.
15+
/// </summary>
16+
public class Base2 : IBaseCoder, INonAllocatingBaseCoder, IBaseStreamCoder
17+
{
18+
private static readonly Lazy<Base2> @default = new(() => new Base2());
19+
20+
/// <summary>
21+
/// Default instance of Base2
22+
/// </summary>
23+
public static Base2 Default => @default.Value;
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="Base2"/> class.
27+
/// </summary>
28+
public Base2()
29+
{
30+
}
31+
32+
/// <inheritdoc/>
33+
public byte[] Decode(ReadOnlySpan<char> text)
34+
{
35+
if (text.Length % 8 != 0)
36+
{
37+
throw new ArgumentException("Input length must be multiply of 8", nameof(text));
38+
}
39+
40+
var output = new byte[text.Length / 8];
41+
if (!internalDecode(text, output, out int bytesWritten))
42+
{
43+
throw new ArgumentException("Invalid Base2 character encountered", nameof(text));
44+
}
45+
46+
return output;
47+
}
48+
49+
/// <inheritdoc/>
50+
public void Decode(TextReader input, Stream output)
51+
{
52+
StreamHelper.Decode(input, output, input => Decode(input.Span));
53+
}
54+
55+
/// <inheritdoc/>
56+
public async Task DecodeAsync(TextReader input, Stream output)
57+
{
58+
await StreamHelper.DecodeAsync(input, output, input => Decode(input.Span));
59+
}
60+
61+
/// <inheritdoc/>
62+
public string Encode(ReadOnlySpan<byte> bytes)
63+
{
64+
int outputLen = bytes.Length * 8;
65+
var output = outputLen < Bits.SafeStackMaxAllocSize ? stackalloc char[outputLen] : new char[outputLen];
66+
internalEncode(bytes, output);
67+
return new string(output);
68+
}
69+
70+
/// <inheritdoc/>
71+
public void Encode(Stream input, TextWriter output)
72+
{
73+
StreamHelper.Encode(input, output, (input, lastBlock) => Encode(input.Span));
74+
}
75+
76+
/// <inheritdoc/>
77+
public async Task EncodeAsync(Stream input, TextWriter output)
78+
{
79+
await StreamHelper.EncodeAsync(input, output, (input, lastBlock) => Encode(input.Span));
80+
}
81+
82+
/// <inheritdoc/>
83+
public int GetSafeByteCountForDecoding(ReadOnlySpan<char> text)
84+
{
85+
return text.Length / 8;
86+
}
87+
88+
/// <inheritdoc/>
89+
public int GetSafeCharCountForEncoding(ReadOnlySpan<byte> buffer)
90+
{
91+
return buffer.Length * 8;
92+
}
93+
94+
/// <inheritdoc/>
95+
public bool TryDecode(ReadOnlySpan<char> input, Span<byte> output, out int bytesWritten)
96+
{
97+
bytesWritten = 0;
98+
int inputLen = input.Length;
99+
if (inputLen == 0)
100+
{
101+
return true;
102+
}
103+
104+
(int numBlocks, int remainder) = Math.DivRem(inputLen, 8);
105+
if (remainder != 0 || output.Length < numBlocks)
106+
{
107+
// we don't handle non-canonical encoding yet
108+
return false;
109+
}
110+
111+
return internalDecode(input, output, out bytesWritten);
112+
}
113+
114+
bool internalDecode(ReadOnlySpan<char> input, Span<byte> output, out int bytesWritten)
115+
{
116+
bytesWritten = 0;
117+
byte pad = 0;
118+
for (int i = 0; i < input.Length; i++)
119+
{
120+
int c = input[i] - '0';
121+
if ((c & 1) != c)
122+
{
123+
// invalid character
124+
return false;
125+
}
126+
int shift = 7 - (i % 8);
127+
pad |= (byte)(c << shift);
128+
if (shift == 0)
129+
{
130+
output[bytesWritten++] = pad;
131+
pad = 0;
132+
}
133+
}
134+
return true;
135+
}
136+
137+
/// <inheritdoc/>
138+
public bool TryEncode(ReadOnlySpan<byte> input, Span<char> output, out int numCharsWritten)
139+
{
140+
numCharsWritten = 0;
141+
int targetLen = input.Length * 8;
142+
if (output.Length < targetLen)
143+
{
144+
return false;
145+
}
146+
147+
internalEncode(input, output);
148+
numCharsWritten = targetLen;
149+
return true;
150+
}
151+
152+
static void internalEncode(ReadOnlySpan<byte> input, Span<char> output)
153+
{
154+
for (int i = 0, o = 0; i < input.Length; i++, o += 8)
155+
{
156+
var b = input[i];
157+
output[o + 0] = (b & 0b1000_0000) != 0 ? '1' : '0';
158+
output[o + 1] = (b & 0b0100_0000) != 0 ? '1' : '0';
159+
output[o + 2] = (b & 0b0010_0000) != 0 ? '1' : '0';
160+
output[o + 3] = (b & 0b0001_0000) != 0 ? '1' : '0';
161+
output[o + 4] = (b & 0b0000_1000) != 0 ? '1' : '0';
162+
output[o + 5] = (b & 0b0000_0100) != 0 ? '1' : '0';
163+
output[o + 6] = (b & 0b0000_0010) != 0 ? '1' : '0';
164+
output[o + 7] = (b & 0b0000_0001) != 0 ? '1' : '0';
165+
}
166+
}
167+
}

src/PublicAPI.Unshipped.txt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
1-

1+
SimpleBase.Base2
2+
SimpleBase.Base2.Base2() -> void
3+
SimpleBase.Base2.Decode(System.IO.TextReader! input, System.IO.Stream! output) -> void
4+
SimpleBase.Base2.Decode(System.ReadOnlySpan<char> text) -> byte[]!
5+
SimpleBase.Base2.DecodeAsync(System.IO.TextReader! input, System.IO.Stream! output) -> System.Threading.Tasks.Task!
6+
SimpleBase.Base2.Encode(System.IO.Stream! input, System.IO.TextWriter! output) -> void
7+
SimpleBase.Base2.Encode(System.ReadOnlySpan<byte> bytes) -> string!
8+
SimpleBase.Base2.EncodeAsync(System.IO.Stream! input, System.IO.TextWriter! output) -> System.Threading.Tasks.Task!
9+
SimpleBase.Base2.GetSafeByteCountForDecoding(System.ReadOnlySpan<char> text) -> int
10+
SimpleBase.Base2.GetSafeCharCountForEncoding(System.ReadOnlySpan<byte> buffer) -> int
11+
SimpleBase.Base2.TryDecode(System.ReadOnlySpan<char> input, System.Span<byte> output, out int bytesWritten) -> bool
12+
SimpleBase.Base2.TryEncode(System.ReadOnlySpan<byte> input, System.Span<char> output, out int numCharsWritten) -> bool
13+
static SimpleBase.Base2.Default.get -> SimpleBase.Base2!

test/Base2Test.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2014-2025 Sedat Kapanoglu
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
using System;
17+
using NUnit.Framework;
18+
using SimpleBase;
19+
20+
namespace SimpleBaseTest;
21+
22+
[TestFixture]
23+
class Base2Test
24+
{
25+
static readonly object[][] testData =
26+
[
27+
[new byte[] { }, ""],
28+
[new byte[] { 0x00, 0x01, 0x02, 0x03 }, "00000000" + "00000001" + "00000010" + "00000011"],
29+
[new byte[] { 0xFF, 0xFE, 0xFD, 0xFC }, "11111111" + "11111110" + "11111101" + "11111100"],
30+
[new byte[] { 0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD }, "00000000" + "00000001" + "00000010" + "00000011" + "11111111" + "11111110" + "11111101"]
31+
];
32+
33+
static readonly string[] nonCanonicalInput =
34+
[
35+
"1",
36+
"10",
37+
"101",
38+
"1010",
39+
"101010",
40+
"1010101",
41+
];
42+
43+
[Test]
44+
[TestCaseSource(nameof(testData))]
45+
public void Encode_EncodesCorrectly(byte[] decoded, string encoded)
46+
{
47+
string result = Base2.Default.Encode(decoded);
48+
Assert.That(result, Is.EqualTo(encoded));
49+
}
50+
51+
[Test]
52+
[TestCaseSource(nameof(testData))]
53+
public void Decode_DecodesCorrectly(byte[] decoded, string encoded)
54+
{
55+
var bytes = Base2.Default.Decode(encoded);
56+
Assert.That(bytes, Is.EqualTo(decoded));
57+
}
58+
59+
[Test]
60+
[TestCaseSource(nameof(nonCanonicalInput))]
61+
public void Decode_NonCanonicalInput_Throws(string nonCanonicalText)
62+
{
63+
Assert.Throws<ArgumentException>(() => Base2.Default.Decode(nonCanonicalText));
64+
}
65+
}
66+
67+

0 commit comments

Comments
 (0)