Skip to content

Commit e981f56

Browse files
Reverted to simpler more robust hashing.
1 parent 95b79ba commit e981f56

4 files changed

Lines changed: 142 additions & 23 deletions

File tree

Source/EnumValue.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ public void Deconstruct(out bool success, out TEnum value)
149149
success = Success;
150150
value = Value;
151151
}
152-
153152
}
154153

155154
private static Func<string, ValueLookupResult>? _valueLookup;
@@ -195,7 +194,7 @@ public void Deconstruct(out string name, out TEnum value)
195194
value = Value;
196195
}
197196

198-
public static int Find(Span<Entry> span, StringSegment name, StringComparison sc)
197+
public static int Find(Span<Entry> span, ReadOnlySpan<char> name, StringComparison sc)
199198
{
200199
// Small enough? Just brute force the index.
201200
var len = span.Length;
@@ -218,12 +217,12 @@ public static int Find(Span<Entry> span, StringSegment name, StringComparison sc
218217
while (left <= right)
219218
{
220219
int middle = left + (right - left) / 2;
221-
var middleKey = span[middle].Name;
220+
var middleKey = span[middle].Name.AsSpan();
222221

223222
if (right - left < 4 && name.Equals(middleKey, sc))
224223
return middle;
225224

226-
var comparison = StringSegment.Compare(middleKey, name, sc);
225+
var comparison = name.CompareTo(middleKey, sc);
227226
if (comparison < 0)
228227
left = middle + 1;
229228
else if (comparison > 0)
@@ -539,14 +538,13 @@ public static bool TryParse<TEnum>(StringSegment value, out TEnum e)
539538
public static bool TryParse<TEnum>(StringSegment name, bool ignoreCase, out TEnum e)
540539
where TEnum : Enum
541540
{
542-
if (!name.HasValue) goto notFound;
541+
var len = name.Length;
542+
if (len == 0) goto notFound;
543543

544544
// If this is a string, use the optimized version.
545-
if (!ignoreCase && name.Buffer.Length == name.Length)
545+
if (!ignoreCase && name.Buffer!.Length == len)
546546
return TryParse(name.Buffer, out e);
547547

548-
var len = name.Length;
549-
if (len == 0) goto notFound;
550548
var lookup = EnumValue<TEnum>.Lookup;
551549
if (len >= lookup.Length) goto notFound;
552550

Source/Extensions._.cs

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -435,33 +435,68 @@ public static string Supplant<T>(this string format, T[]? values, CultureInfo? c
435435
};
436436

437437
/// <summary>
438-
/// A hashing algorithm that is can be faster than the default string.GetHashCode()
439-
/// but can be used for any sequence of characters reliably.
438+
/// A hashing algorithm for a span of characters.
440439
/// </summary>
441440
/// <remarks>
442441
/// Setting the <paramref name="maxChars"/> parameter to a low number
443442
/// will dramatically increase the speed as more characters requires more iterations
444443
/// at the expense of accuracy and possible collisions.
445444
/// </remarks>
446-
public static int GetHashCodeFromChars(this ReadOnlySpan<char> chars, int maxChars = int.MaxValue)
445+
public static int GetHashCodeFromChars(this ReadOnlySpan<char> chars, StringComparison comparisonType = StringComparison.Ordinal, int maxChars = int.MaxValue)
447446
{
448447
int length = chars.Length > maxChars ? maxChars : chars.Length;
449448

450-
long hash = 0;
451-
for (int i = 0; i < length; i++)
449+
int hash = 17;
450+
switch(comparisonType)
452451
{
453-
ref readonly char c = ref chars[i];
454-
hash |= (long)c << (i * 8);
452+
case StringComparison.Ordinal:
453+
case StringComparison.CurrentCulture:
454+
case StringComparison.InvariantCulture:
455+
{
456+
for (int i = 0; i < length; i++)
457+
{
458+
ref readonly char c = ref chars[i];
459+
hash = (hash * 31) ^ c;
460+
}
461+
462+
break;
463+
}
464+
465+
case StringComparison.OrdinalIgnoreCase:
466+
case StringComparison.CurrentCultureIgnoreCase:
467+
{
468+
for (int i = 0; i < length; i++)
469+
{
470+
ref readonly char c = ref chars[i];
471+
hash = (hash * 31) ^ char.ToLower(c, CultureInfo.CurrentCulture);
472+
}
473+
474+
break;
475+
}
476+
477+
case StringComparison.InvariantCultureIgnoreCase:
478+
{
479+
for (int i = 0; i < length; i++)
480+
{
481+
ref readonly char c = ref chars[i];
482+
hash = (hash * 31) ^ char.ToLowerInvariant(c);
483+
}
484+
485+
break;
486+
}
487+
455488
}
456489

457-
return (int)(hash ^ (hash >> 32));
490+
return hash + length;
458491
}
459492

460-
/// <inheritdoc cref="GetHashCodeFromChars(ReadOnlySpan{char}, int)"/>
461-
public static int GetHashCodeFromChars(this StringSegment chars, int maxChars = int.MaxValue)
462-
=> chars.AsSpan().GetHashCodeFromChars(maxChars);
493+
/// <inheritdoc cref="GetHashCodeFromChars(ReadOnlySpan{char}, StringComparison, int)"/>
494+
public static int GetHashCodeFromChars(this StringSegment chars, StringComparison comparisonType = StringComparison.Ordinal, int maxChars = int.MaxValue)
495+
=> chars.HasValue
496+
? chars.AsSpan().GetHashCodeFromChars(comparisonType, maxChars)
497+
: throw new ArgumentNullException(nameof(chars), "The buffer must not be null.");
463498

464-
/// <inheritdoc cref="GetHashCodeFromChars(ReadOnlySpan{char}, int)"/>
465-
public static int GetHashCodeFromChars(this string chars, int maxChars = int.MaxValue)
466-
=> chars.AsSpan().GetHashCodeFromChars(maxChars);
499+
/// <inheritdoc cref="GetHashCodeFromChars(ReadOnlySpan{char}, StringComparison, int)"/>
500+
public static int GetHashCodeFromChars(this string chars, StringComparison comparisonType = StringComparison.Ordinal, int maxChars = int.MaxValue)
501+
=> chars.AsSpan().GetHashCodeFromChars(comparisonType, maxChars);
467502
}

Source/Open.Text.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<RepositoryUrl>https://github.com/Open-NET-Libraries/Open.Text</RepositoryUrl>
2020
<RepositoryType>git</RepositoryType>
2121
<PackageTags>string, span, enum, readonlyspan, text, format, split, trim, equals, trimmed equals, first, last, preceding, following, stringbuilder, extensions, stringcomparable, spancomparable, stringsegment, splitassegment</PackageTags>
22-
<Version>6.5.4</Version>
22+
<Version>6.6.1</Version>
2323
<PackageReleaseNotes></PackageReleaseNotes>
2424
<PackageLicenseExpression>MIT</PackageLicenseExpression>
2525
<PublishRepositoryUrl>true</PublishRepositoryUrl>

Source/StringSegmentComparer.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using Microsoft.Extensions.Primitives;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Runtime.CompilerServices;
5+
6+
namespace Open.Text;
7+
8+
/// <summary>
9+
/// A comparer for <see cref="StringSegment"/> that uses existing <see cref="StringSegment.Compare(StringSegment, StringSegment, StringComparison)"/>
10+
/// and <see cref="StringSegment.Equals(StringSegment, StringSegment, StringComparison)"/> for its comparisons
11+
/// and <see cref="TextExtensions.GetHashCodeFromChars(ReadOnlySpan{char}, StringComparison, int)"/> for hash codes.
12+
/// </summary>
13+
public class StringSegmentComparer
14+
: IComparer<StringSegment>, IEqualityComparer<StringSegment>
15+
{
16+
/// <summary>
17+
/// A <see cref="StringSegmentComparer"/> that uses <see cref="StringComparison.Ordinal"/>.
18+
/// </summary>
19+
public static readonly StringSegmentComparer Ordinal = new();
20+
21+
/// <summary>
22+
/// A <see cref="StringSegmentComparer"/> that uses <see cref="StringComparison.OrdinalIgnoreCase"/>.
23+
/// </summary>
24+
public static readonly StringSegmentComparer OrdinalIgnoreCase = new(StringComparison.OrdinalIgnoreCase);
25+
26+
/// <summary>
27+
/// A <see cref="StringSegmentComparer"/> that uses <see cref="StringComparison.CurrentCulture"/>.
28+
/// </summary>
29+
public static readonly StringSegmentComparer CurrentCulture = new(StringComparison.CurrentCulture);
30+
31+
/// <summary>
32+
/// A <see cref="StringSegmentComparer"/> that uses <see cref="StringComparison.CurrentCultureIgnoreCase"/>.
33+
/// </summary>
34+
public static readonly StringSegmentComparer CurrentCultureIgnoreCase = new(StringComparison.CurrentCultureIgnoreCase);
35+
36+
/// <summary>
37+
/// A <see cref="StringSegmentComparer"/> that uses <see cref="StringComparison.InvariantCulture"/>.
38+
/// </summary>
39+
public static readonly StringSegmentComparer InvariantCulture = new(StringComparison.InvariantCulture);
40+
41+
/// <summary>
42+
/// A <see cref="StringSegmentComparer"/> that uses <see cref="StringComparison.InvariantCultureIgnoreCase"/>.
43+
/// </summary>
44+
public static readonly StringSegmentComparer InvariantCultureIgnoreCase = new(StringComparison.InvariantCultureIgnoreCase);
45+
46+
/// <summary>
47+
/// Creates a new instance of <see cref="StringSegmentComparer"/>
48+
/// with the specified <paramref name="comparisonType"/>
49+
/// and <paramref name="maxHashChars"/>.
50+
/// </summary>
51+
/// <param name="comparisonType">The string comparison type.</param>
52+
/// <param name="maxHashChars">
53+
/// The max number of characters to generate a hash from.
54+
/// See <see cref="TextExtensions.GetHashCodeFromChars(ReadOnlySpan{char}, StringComparison, int)"/> for more details.
55+
/// </param>
56+
public StringSegmentComparer(StringComparison comparisonType = StringComparison.Ordinal, int maxHashChars = int.MaxValue)
57+
{
58+
ComparisonType = comparisonType;
59+
MaxHashChars = maxHashChars;
60+
}
61+
62+
/// <summary>
63+
/// The string comparison type.
64+
/// </summary>
65+
public StringComparison ComparisonType { get; }
66+
67+
/// <summary>
68+
/// The max number of characters to generate a hash from.
69+
/// </summary>
70+
public int MaxHashChars { get; }
71+
72+
/// <inheritdoc />
73+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
74+
public int Compare(StringSegment x, StringSegment y)
75+
=> StringSegment.Compare(x, y, ComparisonType);
76+
77+
/// <inheritdoc />
78+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
79+
public bool Equals(StringSegment x, StringSegment y)
80+
=> StringSegment.Equals(x, y, ComparisonType);
81+
82+
/// <inheritdoc />
83+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
84+
public int GetHashCode(StringSegment obj)
85+
=> obj.GetHashCodeFromChars(ComparisonType, MaxHashChars);
86+
}

0 commit comments

Comments
 (0)