Skip to content

Commit a4a1688

Browse files
authored
Code and project cleanup (#61)
* set net6.0 as 'host' for benchmark command * format csproj files * remove unused code * cleanup compiler directives * update nuget references * clear up comments about alphabet * run dotnet format * remove unnecessary static init, merge into ctor * improve hash confirmation check * renames, code style fixes
1 parent 4f017cf commit a4a1688

8 files changed

Lines changed: 114 additions & 181 deletions

File tree

benchmark.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
dotnet run --project .\test\Hashids.net.benchmark\Hashids.net.benchmark.csproj -c Release
1+
dotnet run --framework net6.0 --project .\test\Hashids.net.benchmark\Hashids.net.benchmark.csproj -c Release

src/Hashids.net/ArrayExtensions.cs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,5 @@ public static T[] Append<T>(this T[] array, T[] appendArray, int index, int leng
3131
Array.Copy(appendArray, index, newArray, array.Length, length - index);
3232
return newArray;
3333
}
34-
35-
#if NETCOREAPP3_1_OR_GREATER
36-
/// <remarks>This method exists because <see cref="ReadOnlySpan{T}"/> does not implement <see cref="IEnumerable{T}"/> and using <c>.AsEnumerable()</c> will cause boxing.</remarks>
37-
public static bool Any<T>(this ReadOnlySpan<T> span, Func<T,bool> predicate)
38-
{
39-
for(int i = 0; i < span.Length; i++)
40-
{
41-
if(predicate(span[i])) return true;
42-
}
43-
44-
return false;
45-
}
46-
#endif
4734
}
4835
}

src/Hashids.net/Hashids.cs

Lines changed: 57 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ public partial class Hashids : IHashids
1616

1717
private const double SEP_DIV = 3.5;
1818
private const double GUARD_DIV = 12.0;
19-
2019
private const int MaxNumberHashLength = 12; // Length of long.MaxValue;
2120

2221
private readonly char[] _alphabet;
@@ -25,11 +24,10 @@ public partial class Hashids : IHashids
2524
private readonly char[] _salt;
2625
private readonly int _minHashLength;
2726

28-
private readonly StringBuilderPool _sbPool = new();
29-
3027
// Creates the Regex in the first usage, speed up first use of non-hex methods
31-
private static readonly Lazy<Regex> hexValidator = new(() => new Regex("^[0-9a-fA-F]+$", RegexOptions.Compiled));
32-
private static readonly Lazy<Regex> hexSplitter = new(() => new Regex(@"[\w\W]{1,12}", RegexOptions.Compiled));
28+
private static readonly Lazy<Regex> HexValidator = new(() => new Regex("^[0-9a-fA-F]+$", RegexOptions.Compiled));
29+
private static readonly Lazy<Regex> HexSplitter = new(() => new Regex(@"[\w\W]{1,12}", RegexOptions.Compiled));
30+
private readonly StringBuilderPool StringBuilderPool = new();
3331

3432
/// <summary>
3533
/// Instantiates a new Hashids encoder/decoder with defaults.
@@ -53,101 +51,74 @@ public Hashids(
5351
string alphabet = DEFAULT_ALPHABET,
5452
string seps = DEFAULT_SEPS)
5553
{
56-
if (salt == null)
57-
throw new ArgumentNullException(nameof(salt));
58-
if (string.IsNullOrWhiteSpace(alphabet))
59-
throw new ArgumentNullException(nameof(alphabet));
60-
if (minHashLength < 0)
61-
throw new ArgumentOutOfRangeException(nameof(minHashLength), "Value must be zero or greater.");
62-
if (string.IsNullOrWhiteSpace(seps))
63-
throw new ArgumentNullException(nameof(seps));
54+
if (salt == null) throw new ArgumentNullException(nameof(salt));
55+
if (minHashLength < 0) throw new ArgumentOutOfRangeException(nameof(minHashLength), "Value must be zero or greater.");
56+
if (string.IsNullOrWhiteSpace(alphabet)) throw new ArgumentNullException(nameof(alphabet));
57+
if (string.IsNullOrWhiteSpace(seps)) throw new ArgumentNullException(nameof(seps));
6458

6559
_salt = salt.Trim().ToCharArray();
6660
_minHashLength = minHashLength;
61+
_alphabet = alphabet.ToCharArray().Distinct().ToArray();
62+
_seps = seps.ToCharArray();
6763

68-
InitCharArrays(alphabet: alphabet, seps: seps, salt: _salt, alphabetChars: out _alphabet, sepChars: out _seps, guardChars: out _guards);
69-
}
70-
71-
/// <remarks>This method uses <c>out</c> params instead of returning a ValueTuple so it works with .NET 4.6.1.</remarks>
72-
private static void InitCharArrays(string alphabet, string seps, ReadOnlySpan<char> salt, out char[] alphabetChars, out char[] sepChars, out char[] guardChars)
73-
{
74-
alphabetChars = alphabet.ToCharArray().Distinct().ToArray();
75-
sepChars = seps.ToCharArray();
76-
77-
if (alphabetChars.Length < MIN_ALPHABET_LENGTH)
78-
{
64+
if (_alphabet.Length < MIN_ALPHABET_LENGTH)
7965
throw new ArgumentException($"Alphabet must contain at least {MIN_ALPHABET_LENGTH:N0} unique characters.", paramName: nameof(alphabet));
80-
}
8166

82-
// SetupGuards():
67+
// separator characters can only be chosen from the characters in the alphabet
68+
if (_seps.Length > 0)
69+
_seps = _seps.Intersect(_alphabet).ToArray();
8370

84-
// seps should contain only characters present in alphabet:
85-
if (sepChars.Length > 0)
86-
{
87-
sepChars = sepChars.Intersect(alphabetChars).ToArray();
88-
}
89-
90-
// alphabet should not contain seps // TODO: This comment contradicts the above, it needs rephrasing.
91-
if (sepChars.Length > 0 )
92-
{
93-
alphabetChars = alphabetChars.Except(sepChars).ToArray();
94-
}
71+
// once separator characters are chosen, they must be removed from the alphabet available for hash generation
72+
if (_seps.Length > 0)
73+
_alphabet = _alphabet.Except(_seps).ToArray();
9574

96-
if (alphabetChars.Length < (MIN_ALPHABET_LENGTH - 6))
97-
{
75+
if (_alphabet.Length < (MIN_ALPHABET_LENGTH - 6))
9876
throw new ArgumentException($"Alphabet must contain at least {MIN_ALPHABET_LENGTH:N0} unique characters that are also not present in .", paramName: nameof(alphabet));
99-
}
10077

101-
ConsistentShuffle(alphabet: sepChars, salt: salt);
78+
ConsistentShuffle(alphabet: _seps, salt: _salt);
10279

103-
if (sepChars.Length == 0 || ((float)alphabetChars.Length / sepChars.Length) > SEP_DIV)
80+
if (_seps.Length == 0 || ((float)_alphabet.Length / _seps.Length) > SEP_DIV)
10481
{
105-
var sepsLength = (int)Math.Ceiling((float)alphabetChars.Length / SEP_DIV);
82+
var sepsLength = (int)Math.Ceiling((float)_alphabet.Length / SEP_DIV);
10683

10784
if (sepsLength == 1)
108-
{
10985
sepsLength = 2;
110-
}
11186

112-
if (sepsLength > sepChars.Length)
87+
if (sepsLength > _seps.Length)
11388
{
114-
var diff = sepsLength - sepChars.Length;
115-
sepChars = sepChars.Append(alphabetChars, 0, diff);
116-
alphabetChars = alphabetChars.SubArray(diff);
89+
var diff = sepsLength - _seps.Length;
90+
_seps = _seps.Append(_alphabet, 0, diff);
91+
_alphabet = _alphabet.SubArray(diff);
11792
}
11893
else
11994
{
120-
sepChars = sepChars.SubArray(0, sepsLength);
95+
_seps = _seps.SubArray(0, sepsLength);
12196
}
12297
}
12398

124-
ConsistentShuffle(alphabet: alphabetChars, salt: salt);
125-
126-
// SetupGuards():
127-
128-
var guardCount = (int)Math.Ceiling(alphabetChars.Length / GUARD_DIV);
99+
ConsistentShuffle(alphabet: _alphabet, salt: _salt);
129100

130-
if (alphabetChars.Length < 3)
101+
var guardCount = (int)Math.Ceiling(_alphabet.Length / GUARD_DIV);
102+
103+
if (_alphabet.Length < 3)
131104
{
132-
guardChars = sepChars.SubArray(index: 0, length: guardCount);
133-
sepChars = sepChars.SubArray(index: guardCount);
105+
_guards = _seps.SubArray(index: 0, length: guardCount);
106+
_seps = _seps.SubArray(index: guardCount);
134107
}
135108

136109
else
137110
{
138-
guardChars = alphabetChars.SubArray(index: 0, length: guardCount);
139-
alphabetChars = alphabetChars.SubArray(index: guardCount);
111+
_guards = _alphabet.SubArray(index: 0, length: guardCount);
112+
_alphabet = _alphabet.SubArray(index: guardCount);
140113
}
141114
}
142115

143-
#if NETCOREAPP3_1_OR_GREATER
144116
/// <summary>
145117
/// Encodes the provided number into a hashed string
146118
/// </summary>
147119
/// <param name="number">the number</param>
148120
/// <returns>the hashed string</returns>
149121
public string Encode(int number) => EncodeLong(number);
150-
#endif
151122

152123
/// <summary>
153124
/// Encodes the provided numbers into a hash string.
@@ -163,19 +134,12 @@ private static void InitCharArrays(string alphabet, string seps, ReadOnlySpan<ch
163134
/// <returns>Encoded hash string.</returns>
164135
public virtual string Encode(IEnumerable<int> numbers) => Encode(numbers.ToArray());
165136

166-
#if NETCOREAPP3_1_OR_GREATER
167137
/// <summary>
168138
/// Encodes the provided number into a hashed string
169139
/// </summary>
170140
/// <param name="number">the number</param>
171141
/// <returns>the hashed string</returns>
172-
public string EncodeLong(long number)
173-
{
174-
ReadOnlySpan<long> span = stackalloc[] { number };
175-
176-
return GenerateHashFrom(span);
177-
}
178-
#endif
142+
public string EncodeLong(long number) => GenerateHashFrom(stackalloc[] { number });
179143

180144
/// <summary>
181145
/// Encodes the provided numbers into a hash string.
@@ -230,7 +194,7 @@ public bool TryDecodeSingleLong(string hash, out long id)
230194
id = numbers[0];
231195
return true;
232196
}
233-
197+
234198
id = 0L;
235199
return false;
236200
}
@@ -259,7 +223,7 @@ public virtual bool TryDecodeSingle(string hash, out int id)
259223
id = (int)numbers[0];
260224
return true;
261225
}
262-
226+
263227
id = 0;
264228
return false;
265229
}
@@ -271,17 +235,17 @@ public virtual bool TryDecodeSingle(string hash, out int id)
271235
/// <returns>Encoded hash string.</returns>
272236
public virtual string EncodeHex(string hex)
273237
{
274-
if (string.IsNullOrWhiteSpace(hex) || !hexValidator.Value.IsMatch(hex))
238+
if (string.IsNullOrWhiteSpace(hex) || !HexValidator.Value.IsMatch(hex))
275239
return string.Empty;
276240

277-
var matches = hexSplitter.Value.Matches(hex);
241+
var matches = HexSplitter.Value.Matches(hex);
278242
if (matches.Count == 0) return string.Empty;
279243

280244
var numbers = new long[matches.Count];
281245
for (int i = 0; i < numbers.Length; i++)
282246
{
283-
Match match = matches[i];
284-
string concat = string.Concat("1", match.Value);
247+
var match = matches[i];
248+
var concat = string.Concat("1", match.Value);
285249
var number = Convert.ToInt64(concat, fromBase: 16);
286250
numbers[i] = number;
287251
}
@@ -296,40 +260,35 @@ public virtual string EncodeHex(string hex)
296260
/// <returns>Decoded hex string.</returns>
297261
public virtual string DecodeHex(string hash)
298262
{
299-
var builder = _sbPool.Get();
263+
var builder = StringBuilderPool.Get();
300264
var numbers = DecodeLong(hash);
301265

302266
foreach (var number in numbers)
303267
{
304268
var s = number.ToString("X");
305-
306269
for (var i = 1; i < s.Length; i++)
307-
{
308270
builder.Append(s[i]);
309-
}
310271
}
311272

312273
var result = builder.ToString();
313-
_sbPool.Return(builder);
274+
StringBuilderPool.Return(builder);
314275
return result;
315276
}
316277

317278
private string GenerateHashFrom(ReadOnlySpan<long> numbers)
318279
{
319-
if (numbers.Length == 0)
280+
if (numbers.Length == 0)
320281
return string.Empty;
321282

322-
foreach (var item in numbers)
323-
if (item < 0)
283+
foreach (var num in numbers)
284+
if (num < 0)
324285
return string.Empty;
325286

326287
long numbersHashInt = 0;
327288
for (var i = 0; i < numbers.Length; i++)
328-
{
329289
numbersHashInt += numbers[i] % (i + 100);
330-
}
331290

332-
var stringBuilder = _sbPool.Get();
291+
var stringBuilder = StringBuilderPool.Get();
333292

334293
Span<char> alphabet = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
335294
_alphabet.CopyTo(alphabet);
@@ -351,17 +310,13 @@ private string GenerateHashFrom(ReadOnlySpan<long> numbers)
351310
var number = numbers[i];
352311

353312
if (length > 0)
354-
{
355313
alphabet.Slice(0, length).CopyTo(shuffleBuffer.Slice(startIndex));
356-
}
357314

358315
ConsistentShuffle(alphabet, shuffleBuffer);
359316
var hashLength = BuildReversedHash(number, alphabet, hashBuffer);
360317

361318
for (var j = hashLength - 1; j > -1; j--)
362-
{
363319
stringBuilder.Append(hashBuffer[j]);
364-
}
365320

366321
if (i + 1 < numbers.Length)
367322
{
@@ -412,7 +367,7 @@ private string GenerateHashFrom(ReadOnlySpan<long> numbers)
412367
}
413368

414369
var result = stringBuilder.ToString();
415-
_sbPool.Return(stringBuilder);
370+
StringBuilderPool.Return(stringBuilder);
416371
return result;
417372
}
418373

@@ -453,17 +408,14 @@ private long[] GetNumbersFrom(string hash)
453408
if (hashArray.Length == 0)
454409
return Array.Empty<long>();
455410

456-
var i = (hashArray.Length is 3 or 2 ) ? 1 : 0;
411+
var unguardedIdx = (hashArray.Length is 3 or 2) ? 1 : 0;
412+
var hashBreakdown = hashArray[unguardedIdx];
457413

458-
var hashBreakdown = hashArray[i];
459414
var lottery = hashBreakdown[0];
460-
461-
if (lottery == '\0') /* default(char) == '\0' */
415+
if (lottery == '\0') // default(char) is '\0'
462416
return Array.Empty<long>();
463417

464-
hashBreakdown = hashBreakdown.Substring(1);
465-
466-
hashArray = hashBreakdown.Split(_seps, StringSplitOptions.RemoveEmptyEntries);
418+
hashArray = hashBreakdown.Substring(1).Split(_seps, StringSplitOptions.RemoveEmptyEntries);
467419

468420
var result = new long[hashArray.Length];
469421

@@ -482,31 +434,29 @@ private long[] GetNumbersFrom(string hash)
482434
var subHash = hashArray[j];
483435

484436
if (length > 0)
485-
{
486437
alphabet.Slice(0, length).CopyTo(buffer.Slice(startIndex));
487-
}
488438

489439
ConsistentShuffle(alphabet, buffer);
490440
result[j] = Unhash(subHash, alphabet);
491441
}
492442

493-
if (EncodeLong(result) == hash)
494-
{
443+
// regenerate hash from numbers and compare to given hash to ensure the correct parameters were used
444+
if (GenerateHashFrom(result).Equals(hash, StringComparison.Ordinal))
495445
return result;
496-
}
497446

498447
return Array.Empty<long>();
499448
}
500449

501-
/// <summary>NOTE: This method mutates the <paramref name="alphabet"/> argument in-place.</summary>
450+
/// <summary>
451+
/// NOTE: This method mutates the <paramref name="alphabet"/> argument in-place.
452+
/// </summary>
502453
private static void ConsistentShuffle(Span<char> alphabet, ReadOnlySpan<char> salt)
503454
{
504455
if (salt.Length == 0)
505456
return;
506457

507458
// TODO: Document or rename these cryptically-named variables: i, v, p, n.
508-
int n;
509-
for (int i = alphabet.Length - 1, v = 0, p = 0; i > 0; i--, v++)
459+
for (int i = alphabet.Length - 1, v = 0, n, p = 0; i > 0; i--, v++)
510460
{
511461
v %= salt.Length;
512462
n = salt[v];

0 commit comments

Comments
 (0)