<ItemGroup>
<Compile Include="Properties\InternalsVisibleTo.cs" />
+ <Compile Include="System\Collections\Frozen\String\KeyAnalyzer.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_FullCaseInsensitive.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_Full.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedSingleChar.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedSingleChar.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
+ <Compile Include="System\Collections\Frozen\String\Hashing.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_FullCaseInsensitiveAscii.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_FullCaseInsensitive.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_Full.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstrring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstrring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_LeftJustifiedSubstrring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_LeftJustifiedSingleChar.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedSubstring.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedSingleChar.cs" />
+ <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet.cs" />
<Compile Include="System\Polyfills.cs" />
<Compile Include="System\Collections\ThrowHelper.cs" />
<Compile Include="System\Collections\Frozen\KeysAndValuesFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\LengthBucketsFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\LengthBucketsFrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
- <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet.cs" />
<Compile Include="System\Collections\Frozen\SmallFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\SmallFrozenSet.cs" />
<Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenSet.cs" />
<Compile Include="System\Collections\Frozen\Int32\Int32FrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\Int32\Int32FrozenSet.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\ComparerPicker.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\FullCaseInsensitiveAsciiStringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\FullCaseInsensitiveStringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\FullStringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\LeftJustifiedCaseInsensitiveSubstringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\LeftJustifiedSingleCharComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\LeftJustifiedSubstringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\RightJustifiedCaseInsensitiveSubstringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\RightJustifiedSingleCharComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\RightJustifiedSubstringComparer.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\StringComparerBase.cs" />
- <Compile Include="System\Collections\Frozen\StringComparers\SubstringComparerBase.cs" />
<Compile Include="System\Collections\Generic\IHashKeyCollection.cs" />
<Compile Include="System\Collections\Generic\ISortKeyCollection.cs" />
<None Include="Interfaces.cd" />
</ItemGroup>
- <ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))" >
+ <ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<Compile Include="System\Collections\Frozen\Integer\IntegerFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\Integer\IntegerFrozenSet.cs" />
<Compile Include="System\Collections\Frozen\Integer\SmallIntegerFrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\Integer\SparseRangeIntegerFrozenSet.cs" />
</ItemGroup>
- <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))" >
+ <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
<Compile Include="System\Collections\Frozen\Int32\SmallInt32FrozenDictionary.cs" />
<Compile Include="System\Collections\Frozen\Int32\SmallInt32FrozenSet.cs" />
<Compile Include="System\Collections\Frozen\Int32\SparseRangeInt32FrozenSet.cs" />
Dictionary<string, TValue> stringEntries = (Dictionary<string, TValue>)(object)source;
IEqualityComparer<string> stringComparer = (IEqualityComparer<string>)(object)comparer;
- FrozenDictionary<string, TValue> frozenDictionary =
- LengthBucketsFrozenDictionary<TValue>.TryCreateLengthBucketsFrozenSet(stringEntries, stringComparer) ??
- (FrozenDictionary<string, TValue>)new OrdinalStringFrozenDictionary<TValue>(stringEntries, stringComparer);
+ FrozenDictionary<string, TValue>? frozenDictionary = LengthBucketsFrozenDictionary<TValue>.CreateLengthBucketsFrozenDictionaryIfAppropriate(stringEntries, stringComparer);
+ if (frozenDictionary is not null)
+ {
+ return (FrozenDictionary<TKey, TValue>)(object)frozenDictionary;
+ }
+
+ var entries = (string[])(object)source.Keys.ToArray();
+
+ KeyAnalyzer.Analyze(entries, ReferenceEquals(stringComparer, StringComparer.OrdinalIgnoreCase), out KeyAnalyzer.AnalysisResults results);
+ if (results.SubstringHashing)
+ {
+ if (results.RightJustifiedSubstring)
+ {
+ if (results.IgnoreCase)
+ {
+ frozenDictionary = results.AllAscii
+ ? new OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount)
+ : new OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+ }
+ else
+ {
+ frozenDictionary = results.HashCount == 1
+ ? new OrdinalStringFrozenDictionary_RightJustifiedSingleChar<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex)
+ : new OrdinalStringFrozenDictionary_RightJustifiedSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+ }
+ }
+ else
+ {
+ if (results.IgnoreCase)
+ {
+ frozenDictionary = results.AllAscii
+ ? new OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount)
+ : new OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+ }
+ else
+ {
+ frozenDictionary = results.HashCount == 1
+ ? new OrdinalStringFrozenDictionary_LeftJustifiedSingleChar<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex)
+ : new OrdinalStringFrozenDictionary_LeftJustifiedSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+ }
+ }
+ }
+ else
+ {
+ if (results.IgnoreCase)
+ {
+ frozenDictionary = results.AllAscii
+ ? new OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff)
+ : new OrdinalStringFrozenDictionary_FullCaseInsensitive<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff);
+ }
+ else
+ {
+ frozenDictionary = new OrdinalStringFrozenDictionary_Full<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff);
+ }
+ }
return (FrozenDictionary<TKey, TValue>)(object)frozenDictionary;
}
ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase))
{
HashSet<string> stringValues = (HashSet<string>)(object)uniqueValues;
+ string[] entries = new string[stringValues.Count];
+ stringValues.CopyTo(entries);
+
IEqualityComparer<string> stringComparer = (IEqualityComparer<string>)(object)comparer;
- FrozenSet<string> frozenSet =
- LengthBucketsFrozenSet.TryCreateLengthBucketsFrozenSet(stringValues, stringComparer) ??
- (FrozenSet<string>)new OrdinalStringFrozenSet(stringValues, stringComparer);
+ FrozenSet<string>? frozenSet = LengthBucketsFrozenSet.CreateLengthBucketsFrozenSetIfAppropriate(entries, stringComparer);
+ if (frozenSet is not null)
+ {
+ return (FrozenSet<T>)(object)frozenSet;
+ }
+
+ KeyAnalyzer.Analyze(entries, ReferenceEquals(stringComparer, StringComparer.OrdinalIgnoreCase), out KeyAnalyzer.AnalysisResults results);
+ if (results.SubstringHashing)
+ {
+ if (results.RightJustifiedSubstring)
+ {
+ if (results.IgnoreCase)
+ {
+ frozenSet = results.AllAscii
+ ? new OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount)
+ : new OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+ }
+ else
+ {
+ frozenSet = results.HashCount == 1
+ ? new OrdinalStringFrozenSet_RightJustifiedSingleChar(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex)
+ : new OrdinalStringFrozenSet_RightJustifiedSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+ }
+ }
+ else
+ {
+ if (results.IgnoreCase)
+ {
+ frozenSet = results.AllAscii
+ ? new OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount)
+ : new OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+ }
+ else
+ {
+ frozenSet = results.HashCount == 1
+ ? new OrdinalStringFrozenSet_LeftJustifiedSingleChar(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex)
+ : new OrdinalStringFrozenSet_LeftJustifiedSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+ }
+ }
+ }
+ else
+ {
+ if (results.IgnoreCase)
+ {
+ frozenSet = results.AllAscii
+ ? new OrdinalStringFrozenSet_FullCaseInsensitiveAscii(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff)
+ : new OrdinalStringFrozenSet_FullCaseInsensitive(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff);
+ }
+ else
+ {
+ frozenSet = new OrdinalStringFrozenSet_Full(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff);
+ }
+ }
return (FrozenSet<T>)(object)frozenSet;
}
other switch
{
HashSet<T> hs => _thisSet.Comparer.Equals(hs.Comparer),
- SortedSet<T> ss => _thisSet.Comparer.Equals(ss.Comparer),
+ SortedSet<T> sortedSet => _thisSet.Comparer.Equals(sortedSet.Comparer),
ImmutableHashSet<T> ihs => _thisSet.Comparer.Equals(ihs.KeyComparer),
ImmutableSortedSet<T> iss => _thisSet.Comparer.Equals(iss.KeyComparer),
FrozenSet<T> fs => _thisSet.Comparer.Equals(fs.Comparer),
_ignoreCase = ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase);
}
- internal static LengthBucketsFrozenDictionary<TValue>? TryCreateLengthBucketsFrozenSet(Dictionary<string, TValue> source, IEqualityComparer<string> comparer)
+ internal static LengthBucketsFrozenDictionary<TValue>? CreateLengthBucketsFrozenDictionaryIfAppropriate(Dictionary<string, TValue> source, IEqualityComparer<string> comparer)
{
Debug.Assert(source.Count != 0);
Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
_ignoreCase = ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase);
}
- internal static LengthBucketsFrozenSet? TryCreateLengthBucketsFrozenSet(HashSet<string> source, IEqualityComparer<string> comparer)
+ internal static LengthBucketsFrozenSet? CreateLengthBucketsFrozenSetIfAppropriate(string[] entries, IEqualityComparer<string> comparer)
{
- Debug.Assert(source.Count != 0);
+ Debug.Assert(entries.Length != 0);
Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
// Iterate through all of the inputs, bucketing them based on the length of the string.
var groupedByLength = new Dictionary<int, List<string>>();
int minLength = int.MaxValue, maxLength = int.MinValue;
- foreach (string s in source)
+ foreach (string s in entries)
{
Debug.Assert(s is not null, "This implementation should not be used with null source values.");
return null;
}
- string[] items = new string[source.Count];
var lengthBuckets = new KeyValuePair<string, int>[maxLength - minLength + 1][];
// Iterate through each bucket, filling the items array, and creating a lookup array such that
foreach (string value in group.Value)
{
length[i] = new KeyValuePair<string, int>(value, index);
- items[index] = value;
+ entries[index] = value;
i++;
index++;
}
}
- return new LengthBucketsFrozenSet(items, lengthBuckets, minLength, comparer);
+ return new LengthBucketsFrozenSet(entries, lengthBuckets, minLength, comparer);
}
/// <inheritdoc />
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
-using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
namespace System.Collections.Frozen
{
- // We define this rather than using IEqualityComparer<string>, since virtual dispatch is faster than interface dispatch
- internal abstract class StringComparerBase : EqualityComparer<string>
+ internal static class Hashing
{
// TODO https://github.com/dotnet/runtime/issues/77679:
// Replace these once non-randomized implementations are available.
- protected static unsafe int GetHashCodeOrdinal(ReadOnlySpan<char> s)
+ public static unsafe int GetHashCodeOrdinal(ReadOnlySpan<char> s)
{
int length = s.Length;
fixed (char* src = &MemoryMarshal.GetReference(s))
}
// useful if the string only contains ASCII characters
- protected static unsafe int GetHashCodeOrdinalIgnoreCaseAscii(ReadOnlySpan<char> s)
+ public static unsafe int GetHashCodeOrdinalIgnoreCaseAscii(ReadOnlySpan<char> s)
{
int length = s.Length;
fixed (char* src = &MemoryMarshal.GetReference(s))
}
}
- protected static unsafe int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> s)
+ public static unsafe int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> s)
{
int length = s.Length;
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace System.Collections.Frozen
+{
+ internal static class KeyAnalyzer
+ {
+ /// <summary>
+ /// Look for well-known patterns we can optimize for in a set of dictionary or set keys.
+ /// </summary>
+ /// <remarks>
+ /// The idea here is to find the shortest substring slice across all the input strings which yields a set of
+ /// strings which are maximally unique. The optimal slice is then applied to incoming strings being hashed to
+ /// perform dictionary/set lookups. Keeping the slices as small as possible minimizes the number of characters
+ /// involved in hashing, speeding up the whole process.
+ ///
+ /// What we do here is pretty simple. We loop over the input strings, looking for the shortest slice with a good
+ /// enough uniqueness factor. We look at all the strings both left-justified and right-justified as this maximizes
+ /// the opportunities to find unique slices, especially in the case of many strings with the same prefix or suffix.
+ ///
+ /// In whatever slice we end up with, if all the characters involved in the slice are ASCII and we're doing case-insensitive
+ /// operations, then we can select an ASCII-specific case-insensitive comparer which yields faster overall performance.
+ /// </remarks>
+ public static void Analyze(ReadOnlySpan<string> uniqueStrings, bool ignoreCase, out AnalysisResults results)
+ {
+ // First, try to pick a substring comparer.
+ // if we can't find a good substring comparer, fallback to a full string comparer.
+ if (!UseSubstring(uniqueStrings, ignoreCase, out results))
+ {
+ UseFullString(uniqueStrings, ignoreCase, out results);
+ }
+
+ // Calculate the trivial rejection boundaries.
+ int min = int.MaxValue, max = 0;
+ foreach (string s in uniqueStrings)
+ {
+ if (s.Length < min)
+ {
+ min = s.Length;
+ }
+
+ if (s.Length > max)
+ {
+ max = s.Length;
+ }
+ }
+
+ results.MinimumLength = min;
+ results.MaximumLengthDiff = max - min;
+ }
+
+ private static bool UseSubstring(ReadOnlySpan<string> uniqueStrings, bool ignoreCase, out AnalysisResults results)
+ {
+ const double SufficientUniquenessFactor = 0.95; // 95% is good enough
+
+ // What is the shortest string? This represents the maximum substring length we consider
+ int maxSubstringLength = int.MaxValue;
+ foreach (string s in uniqueStrings)
+ {
+ if (s.Length < maxSubstringLength)
+ {
+ maxSubstringLength = s.Length;
+ }
+ }
+
+ SubstringComparer leftComparer = ignoreCase ? new LeftJustifiedCaseInsensitiveSubstringComparer() : new LeftJustifiedSubstringComparer();
+ SubstringComparer rightComparer = ignoreCase ? new RightJustifiedCaseInsensitiveSubstringComparer() : new RightJustifiedSubstringComparer();
+
+ // try to find the minimal unique substring to use for comparisons
+ var leftSet = new HashSet<string>(leftComparer);
+ var rightSet = new HashSet<string>(rightComparer);
+ for (int count = 1; count <= maxSubstringLength; count++)
+ {
+ for (int index = 0; index <= maxSubstringLength - count; index++)
+ {
+ leftComparer.Index = index;
+ leftComparer.Count = count;
+
+ double factor = GetUniquenessFactor(leftSet, uniqueStrings);
+ if (factor >= SufficientUniquenessFactor)
+ {
+ bool allAscii = true;
+ foreach (string s in uniqueStrings)
+ {
+ if (!IsAllAscii(s.AsSpan(leftComparer.Index, leftComparer.Count)))
+ {
+ allAscii = false;
+ break;
+ }
+ }
+
+ results = new(allAscii, ignoreCase, 0, 0, leftComparer.Index, leftComparer.Count);
+ return true;
+ }
+
+ rightComparer.Index = -index - count;
+ rightComparer.Count = count;
+
+ factor = GetUniquenessFactor(rightSet, uniqueStrings);
+ if (factor >= SufficientUniquenessFactor)
+ {
+ bool allAscii = true;
+ foreach (string s in uniqueStrings)
+ {
+ if (!IsAllAscii(s.AsSpan(s.Length + rightComparer.Index, rightComparer.Count)))
+ {
+ allAscii = false;
+ break;
+ }
+ }
+
+ results = new(allAscii, ignoreCase, 0, 0, rightComparer.Index, rightComparer.Count);
+ return true;
+ }
+ }
+ }
+
+ results = default;
+ return false;
+ }
+
+ private static void UseFullString(ReadOnlySpan<string> uniqueStrings, bool ignoreCase, out AnalysisResults results)
+ {
+ bool allAscii = true;
+ foreach (string s in uniqueStrings)
+ {
+ if (!IsAllAscii(s.AsSpan()))
+ {
+ allAscii = false;
+ break;
+ }
+ }
+
+ results = new(allAscii, ignoreCase, 0, 0, 0, 0);
+ }
+
+ // TODO https://github.com/dotnet/runtime/issues/28230:
+ // Replace this once Ascii.IsValid exists.
+ internal static unsafe bool IsAllAscii(ReadOnlySpan<char> s)
+ {
+ fixed (char* src = s)
+ {
+ uint* ptrUInt32 = (uint*)src;
+ int length = s.Length;
+
+ while (length > 3)
+ {
+ if (!AllCharsInUInt32AreAscii(ptrUInt32[0] | ptrUInt32[1]))
+ {
+ return false;
+ }
+
+ ptrUInt32 += 2;
+ length -= 4;
+ }
+
+ char* ptrChar = (char*)ptrUInt32;
+ while (length-- > 0)
+ {
+ char ch = *ptrChar++;
+ if (ch >= 0x7f)
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static bool AllCharsInUInt32AreAscii(uint value) => (value & ~0x007F_007Fu) == 0;
+ }
+
+ private static double GetUniquenessFactor(HashSet<string> set, ReadOnlySpan<string> uniqueStrings)
+ {
+ set.Clear();
+ foreach (string s in uniqueStrings)
+ {
+ set.Add(s);
+ }
+
+ return set.Count / (double)uniqueStrings.Length;
+ }
+
+ internal struct AnalysisResults
+ {
+ public AnalysisResults(
+ bool allAscii,
+ bool ignoreCase,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ {
+ AllAscii = allAscii;
+ IgnoreCase = ignoreCase;
+ MinimumLength = minimumLength;
+ MaximumLengthDiff = maximumLengthDiff;
+ HashIndex = hashIndex;
+ HashCount = hashCount;
+ }
+
+ public bool AllAscii { get; }
+ public bool IgnoreCase { get; }
+ public int MinimumLength { get; set; }
+ public int MaximumLengthDiff { get; set; }
+ public int HashIndex { get; }
+ public int HashCount { get; }
+
+ public bool SubstringHashing => HashCount != 0;
+ public bool RightJustifiedSubstring => HashIndex < 0;
+ }
+
+ private abstract class SubstringComparer : IEqualityComparer<string>
+ {
+ public int Index;
+ public int Count;
+ public abstract bool Equals(string? x, string? y);
+ public abstract int GetHashCode(string s);
+ }
+
+ private sealed class LeftJustifiedSubstringComparer : SubstringComparer
+ {
+ public override bool Equals(string? x, string? y) => x.AsSpan(Index, Count).SequenceEqual(y.AsSpan(Index, Count));
+ public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(Index, Count));
+ }
+
+ private sealed class LeftJustifiedCaseInsensitiveSubstringComparer : SubstringComparer
+ {
+ public override bool Equals(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase);
+ public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(Index, Count));
+ }
+
+ private sealed class RightJustifiedSubstringComparer : SubstringComparer
+ {
+ public override bool Equals(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).SequenceEqual(y.AsSpan(y!.Length + Index, Count));
+ public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(s.Length + Index, Count));
+ }
+
+ private sealed class RightJustifiedCaseInsensitiveSubstringComparer : SubstringComparer
+ {
+ public override bool Equals(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase);
+ public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + Index, Count));
+ }
+ }
+}
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace System.Collections.Frozen
{
- /// <summary>Provides a frozen dictionary optimized for ordinal (case-sensitive or case-insensitive) lookup of strings.</summary>
- /// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
- internal sealed class OrdinalStringFrozenDictionary<TValue> : FrozenDictionary<string, TValue>
+ /// <summary>The base class for the specialized frozen string dictionaries.</summary>
+ internal abstract class OrdinalStringFrozenDictionary<TValue> : FrozenDictionary<string, TValue>
{
private readonly FrozenHashTable _hashTable;
private readonly string[] _keys;
private readonly TValue[] _values;
- private readonly StringComparerBase _partialComparer;
private readonly int _minimumLength;
private readonly int _maximumLengthDiff;
- internal OrdinalStringFrozenDictionary(Dictionary<string, TValue> source, IEqualityComparer<string> comparer) :
+ internal OrdinalStringFrozenDictionary(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex = -1,
+ int hashCount = -1) :
base(comparer)
{
Debug.Assert(source.Count != 0);
var entries = new KeyValuePair<string, TValue>[source.Count];
((ICollection<KeyValuePair<string, TValue>>)source).CopyTo(entries, 0);
- _keys = new string[entries.Length];
+ _keys = keys;
_values = new TValue[entries.Length];
+ _minimumLength = minimumLength;
+ _maximumLengthDiff = maximumLengthDiff;
- _partialComparer = ComparerPicker.Pick(
- Array.ConvertAll(entries, pair => pair.Key),
- ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase),
- out _minimumLength,
- out _maximumLengthDiff);
+ HashIndex = hashIndex;
+ HashCount = hashCount;
_hashTable = FrozenHashTable.Create(
entries,
- pair => _partialComparer.GetHashCode(pair.Key),
+ pair => GetHashCode(pair.Key),
(index, pair) =>
{
_keys[index] = pair.Key;
});
}
- /// <inheritdoc />
+ private protected int HashIndex { get; }
+ private protected int HashCount { get; }
+ private protected abstract bool Equals(string? x, string? y);
+ private protected abstract int GetHashCode(string s);
private protected override string[] KeysCore => _keys;
-
- /// <inheritdoc />
private protected override TValue[] ValuesCore => _values;
-
- /// <inheritdoc />
private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values);
-
- /// <inheritdoc />
private protected override int CountCore => _hashTable.Count;
- /// <inheritdoc />
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected override ref readonly TValue GetValueRefOrNullRefCore(string key)
{
if ((uint)(key.Length - _minimumLength) <= (uint)_maximumLengthDiff)
{
- StringComparerBase partialComparer = _partialComparer;
-
- int hashCode = partialComparer.GetHashCode(key);
+ int hashCode = GetHashCode(key);
_hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex);
while (index <= endIndex)
{
if (hashCode == _hashTable.HashCodes[index])
{
- if (partialComparer.Equals(key, _keys[index])) // partialComparer.Equals always compares the full input (EqualsPartial/GetHashCode don't)
+ if (Equals(key, _keys[index]))
{
return ref _values[index];
}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_Full<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_Full(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan());
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_FullCaseInsensitive<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_FullCaseInsensitive(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan());
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan());
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_LeftJustifiedSingleChar<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_LeftJustifiedSingleChar(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, 1)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => s[HashIndex];
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_LeftJustifiedSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_LeftJustifiedSubstring(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(s.Length + HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_RightJustifiedSingleChar<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_RightJustifiedSingleChar(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, 1)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => s[s.Length + HashIndex];
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenDictionary_RightJustifiedSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+ {
+ internal OrdinalStringFrozenDictionary_RightJustifiedSubstring(
+ Dictionary<string, TValue> source,
+ string[] keys,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(s.Length + HashIndex, HashCount));
+ }
+}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Buffers;
using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace System.Collections.Frozen
{
- /// <summary>Provides a frozen set optimized for ordinal (case-sensitive or case-insensitive) lookup of strings.</summary>
- internal sealed class OrdinalStringFrozenSet : FrozenSetInternalBase<string, OrdinalStringFrozenSet.GSW>
+ /// <summary>The base class for the specialized frozen string sets.</summary>
+ internal abstract class OrdinalStringFrozenSet : FrozenSetInternalBase<string, OrdinalStringFrozenSet.GSW>
{
private readonly FrozenHashTable _hashTable;
private readonly string[] _items;
- private readonly StringComparerBase _partialComparer;
private readonly int _minimumLength;
private readonly int _maximumLengthDiff;
- internal OrdinalStringFrozenSet(HashSet<string> source, IEqualityComparer<string> comparer) :
- base(comparer)
+ internal OrdinalStringFrozenSet(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex = -1,
+ int hashCount = -1)
+ : base(comparer)
{
- Debug.Assert(source.Count != 0);
- Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
-
- string[] entries = new string[source.Count];
- source.CopyTo(entries);
-
_items = new string[entries.Length];
+ _minimumLength = minimumLength;
+ _maximumLengthDiff = maximumLengthDiff;
- _partialComparer = ComparerPicker.Pick(
- entries,
- ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase),
- out _minimumLength,
- out _maximumLengthDiff);
+ HashIndex = hashIndex;
+ HashCount = hashCount;
_hashTable = FrozenHashTable.Create(
entries,
- _partialComparer.GetHashCode,
+ GetHashCode,
(index, item) => _items[index] = item);
}
- /// <inheritdoc />
+ private protected int HashIndex { get; }
+ private protected int HashCount { get; }
+ private protected abstract bool Equals(string? x, string? y);
+ private protected abstract int GetHashCode(string s);
private protected override string[] ItemsCore => _items;
-
- /// <inheritdoc />
private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items);
-
- /// <inheritdoc />
private protected override int CountCore => _hashTable.Count;
- /// <inheritdoc />
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected override int FindItemIndex(string item)
{
if (item is not null && // this implementation won't be used for null values
(uint)(item.Length - _minimumLength) <= (uint)_maximumLengthDiff)
{
- StringComparerBase partialComparer = _partialComparer;
-
- int hashCode = partialComparer.GetHashCode(item);
+ int hashCode = GetHashCode(item);
_hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex);
while (index <= endIndex)
{
if (hashCode == _hashTable.HashCodes[index])
{
- if (partialComparer.Equals(item, _items[index])) // partialComparer.Equals always compares the full input (EqualsPartial/GetHashCode don't)
+ if (Equals(item, _items[index]))
{
return index;
}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_Full : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_Full(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff)
+ : base(entries, comparer, minimumLength, maximumLengthDiff)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan());
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_FullCaseInsensitive : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_FullCaseInsensitive(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff)
+ : base(entries, comparer, minimumLength, maximumLengthDiff)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan());
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_FullCaseInsensitiveAscii : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_FullCaseInsensitiveAscii(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff)
+ : base(entries, comparer, minimumLength, maximumLengthDiff)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan());
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstring : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstring(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstring : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstring(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_LeftJustifiedSingleChar : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_LeftJustifiedSingleChar(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex)
+ : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, 1)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => s[HashIndex];
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_LeftJustifiedSubstring : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_LeftJustifiedSubstring(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(s.Length + HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + HashIndex, HashCount));
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_RightJustifiedSingleChar : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_RightJustifiedSingleChar(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex)
+ : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, 1)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => s[s.Length + HashIndex];
+ }
+}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Collections.Frozen
+{
+ internal sealed class OrdinalStringFrozenSet_RightJustifiedSubstring : OrdinalStringFrozenSet
+ {
+ internal OrdinalStringFrozenSet_RightJustifiedSubstring(
+ string[] entries,
+ IEqualityComparer<string> comparer,
+ int minimumLength,
+ int maximumLengthDiff,
+ int hashIndex,
+ int hashCount)
+ : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+ {
+ }
+
+ // This override is necessary to force the jit to emit the code in such a way that it
+ // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+ // remove this, or you'll tank performance.
+ private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+ private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+ private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(s.Length + HashIndex, HashCount));
+ }
+}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-
-namespace System.Collections.Frozen
-{
- internal static class ComparerPicker
- {
- /// <summary>
- /// Pick an optimal comparer for the set of strings and case-sensitivity mode.
- /// </summary>
- /// <remarks>
- /// The idea here is to find the shortest substring slice across all the input strings which yields a set of
- /// strings which are maximally unique. The optimal slice is then applied to incoming strings being hashed to
- /// perform the dictionary lookup. Keeping the slices as small as possible minimizes the number of characters
- /// involved in hashing, speeding up the whole process.
- ///
- /// What we do here is pretty simple. We loop over the input strings, looking for the shortest slice with a good
- /// enough uniqueness factor. We look at all the strings both left-justified and right-justified as this maximizes
- /// the opportunities to find unique slices, especially in the case of many strings with the same prefix or suffix.
- ///
- /// In whatever slice we end up with, if all the characters involved in the slice are ASCII and we're doing case-insensitive
- /// operations, then we can select an ASCII-specific case-insensitive comparer which yields faster overall performance.
- ///
- /// Warning: This code may reorganize (e.g. sort) the entries in the input array. It will not delete or add anything though.
- /// </remarks>
- public static StringComparerBase Pick(ReadOnlySpan<string> uniqueStrings, bool ignoreCase, out int minimumLength, out int maximumLengthDiff)
- {
- Debug.Assert(uniqueStrings.Length != 0);
-
- // First, try to pick a substring comparer.
- // if we couldn't find a good substring comparer, fallback to a full string comparer.
- StringComparerBase? c =
- PickSubstringComparer(uniqueStrings, ignoreCase) ??
- PickFullStringComparer(uniqueStrings, ignoreCase);
-
- // Calculate the trivial rejection boundaries.
- int min = int.MaxValue, max = 0;
- foreach (string s in uniqueStrings)
- {
- if (s.Length < min)
- {
- min = s.Length;
- }
-
- if (s.Length > max)
- {
- max = s.Length;
- }
- }
-
- minimumLength = min;
- maximumLengthDiff = max - min;
- return c;
- }
-
- private static StringComparerBase? PickSubstringComparer(ReadOnlySpan<string> uniqueStrings, bool ignoreCase)
- {
- const double SufficientUniquenessFactor = 0.95; // 95% is good enough
-
- // What is the shortest string? This represent the maximum substring length we consider
- int maxSubstringLength = int.MaxValue;
- foreach (string s in uniqueStrings)
- {
- if (s.Length < maxSubstringLength)
- {
- maxSubstringLength = s.Length;
- }
- }
-
- SubstringComparerBase leftComparer = ignoreCase ? new LeftJustifiedCaseInsensitiveSubstringComparer() : new LeftJustifiedSubstringComparer();
- SubstringComparerBase rightComparer = ignoreCase ? new RightJustifiedCaseInsensitiveSubstringComparer() : new RightJustifiedSubstringComparer();
-
- // try to find the minimal unique substring to use for comparisons
- var leftSet = new HashSet<string>(new ComparerWrapper(leftComparer));
- var rightSet = new HashSet<string>(new ComparerWrapper(rightComparer));
- for (int count = 1; count <= maxSubstringLength; count++)
- {
- for (int index = 0; index <= maxSubstringLength - count; index++)
- {
- leftComparer.Index = index;
- leftComparer.Count = count;
-
- double factor = GetUniquenessFactor(leftSet, uniqueStrings);
- if (factor >= SufficientUniquenessFactor)
- {
- if (ignoreCase)
- {
- foreach (string ss in uniqueStrings)
- {
- if (!IsAllAscii(ss.AsSpan(leftComparer.Index, leftComparer.Count)))
- {
- // keep the slower non-ascii comparer since we have some non-ascii text
- return leftComparer;
- }
- }
-
- // optimize for all-ascii case
- return new LeftJustifiedCaseInsensitiveAsciiSubstringComparer
- {
- Index = leftComparer.Index,
- Count = leftComparer.Count,
- };
- }
-
- // Optimize the single char case
- if (leftComparer.Count == 1)
- {
- return new LeftJustifiedSingleCharComparer
- {
- Index = leftComparer.Index,
- Count = 1,
- };
- }
-
- return leftComparer;
- }
-
- rightComparer.Index = -index - count;
- rightComparer.Count = count;
-
- factor = GetUniquenessFactor(rightSet, uniqueStrings);
- if (factor >= SufficientUniquenessFactor)
- {
- if (ignoreCase)
- {
- foreach (string ss in uniqueStrings)
- {
- if (!IsAllAscii(ss.AsSpan(ss.Length + rightComparer.Index, rightComparer.Count)))
- {
- // keep the slower non-ascii comparer since we have some non-ascii text
- return rightComparer;
- }
- }
-
- // optimize for all-ascii case
- return new RightJustifiedCaseInsensitiveAsciiSubstringComparer
- {
- Index = rightComparer.Index,
- Count = rightComparer.Count,
- };
- }
-
- // Optimize the single char case
- if (rightComparer.Count == 1)
- {
- return new RightJustifiedSingleCharComparer
- {
- Index = rightComparer.Index,
- Count = 1,
- };
- }
-
- return rightComparer;
- }
- }
- }
-
- return null;
- }
-
- private static StringComparerBase PickFullStringComparer(ReadOnlySpan<string> uniqueStrings, bool ignoreCase)
- {
- if (!ignoreCase)
- {
- return new FullStringComparer();
- }
-
- foreach (string s in uniqueStrings)
- {
- if (!IsAllAscii(s.AsSpan()))
- {
- return new FullCaseInsensitiveStringComparer();
- }
- }
-
- return new FullCaseInsensitiveAsciiStringComparer();
- }
-
- private sealed class ComparerWrapper : IEqualityComparer<string>
- {
- private readonly SubstringComparerBase _comp;
-
- public ComparerWrapper(SubstringComparerBase comp) => _comp = comp;
-
- public bool Equals(string? x, string? y) => _comp.EqualsPartial(x, y);
- public int GetHashCode([DisallowNull] string obj) => _comp.GetHashCode(obj);
- }
-
- // TODO https://github.com/dotnet/runtime/issues/28230:
- // Replace this once Ascii.IsValid exists.
- internal static unsafe bool IsAllAscii(ReadOnlySpan<char> s)
- {
- fixed (char* src = s)
- {
- uint* ptrUInt32 = (uint*)src;
- int length = s.Length;
-
- while (length > 3)
- {
- if (!AllCharsInUInt32AreAscii(ptrUInt32[0] | ptrUInt32[1]))
- {
- return false;
- }
-
- ptrUInt32 += 2;
- length -= 4;
- }
-
- char* ptrChar = (char*)ptrUInt32;
- while (length-- > 0)
- {
- char ch = *ptrChar++;
- if (ch >= 0x7f)
- {
- return false;
- }
- }
- }
-
- return true;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- static bool AllCharsInUInt32AreAscii(uint value) => (value & ~0x007F_007Fu) == 0;
- }
-
- private static double GetUniquenessFactor(HashSet<string> set, ReadOnlySpan<string> uniqueStrings)
- {
- set.Clear();
- foreach (string s in uniqueStrings)
- {
- set.Add(s);
- }
-
- return set.Count / (double)uniqueStrings.Length;
- }
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer for ordinal case-insensitive ascii-only string comparisons.
- /// </summary>
- /// <remarks>
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class FullCaseInsensitiveAsciiStringComparer : StringComparerBase
- {
- public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
- public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan());
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer for ordinal case-insensitive string comparisons.
- /// </summary>
- /// <remarks>
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class FullCaseInsensitiveStringComparer : StringComparerBase
- {
- public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
- public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan());
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer for ordinal string comparisons.
- /// </summary>
- /// <remarks>
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class FullStringComparer : StringComparerBase
- {
- public override bool Equals(string? x, string? y) => string.Equals(x, y);
- public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan());
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer that operates over a portion of the input strings.
- /// </summary>
- /// <remarks>
- /// This comparer looks from the start of input strings.
- ///
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class LeftJustifiedCaseInsensitiveAsciiSubstringComparer : SubstringComparerBase
- {
- public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
- public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase);
- public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(Index, Count));
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer that operates over a portion of the input strings.
- /// </summary>
- /// <remarks>
- /// This comparer looks from the start of input strings.
- ///
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class LeftJustifiedCaseInsensitiveSubstringComparer : SubstringComparerBase
- {
- public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
- public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase);
- public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan(Index, Count));
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer that operates over a single char of the input strings.
- /// </summary>
- /// <remarks>
- /// This comparer looks from the start of input strings.
- ///
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class LeftJustifiedSingleCharComparer : SubstringComparerBase
- {
- public override bool Equals(string? x, string? y) => string.Equals(x, y);
- public override bool EqualsPartial(string? x, string? y) => x![Index] == y![Index];
- public override int GetHashCode(string s) => s[Index];
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer that operates over a portion of the input strings.
- /// </summary>
- /// <remarks>
- /// This comparer looks from the start of input strings.
- ///
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class LeftJustifiedSubstringComparer : SubstringComparerBase
- {
- public override bool Equals(string? x, string? y) => string.Equals(x, y);
- public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).SequenceEqual(y.AsSpan(Index, Count));
- public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan(Index, Count));
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer that operates over a portion of the input strings.
- /// </summary>
- /// <remarks>
- /// This comparer looks at the end of input strings.
- ///
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class RightJustifiedCaseInsensitiveAsciiSubstringComparer : SubstringComparerBase
- {
- public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
- public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase);
- public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(s.Length + Index, Count));
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer that operates over a portion of the input strings.
- /// </summary>
- /// <remarks>
- /// This comparer looks at the end of input strings.
- ///
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class RightJustifiedCaseInsensitiveSubstringComparer : SubstringComparerBase
- {
- public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
- public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase);
- public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + Index, Count));
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer that operates over a single character of the input strings.
- /// </summary>
- /// <remarks>
- /// This comparer looks at the end of input strings.
- ///
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class RightJustifiedSingleCharComparer : SubstringComparerBase
- {
- public override bool Equals(string? x, string? y) => string.Equals(x, y);
- public override bool EqualsPartial(string? x, string? y) => x![x.Length + Index] == y![y.Length + Index];
- public override int GetHashCode(string s) => s[s.Length + Index];
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- /// <summary>
- /// A comparer that operates over a portion of the input strings.
- /// </summary>
- /// <remarks>
- /// This comparer looks at the end of input strings.
- ///
- /// This code doesn't perform any error checks on the input as it assumes
- /// the data is always valid. This is ensured by precondition checks before
- /// a key is used to perform a dictionary lookup.
- /// </remarks>
- internal sealed class RightJustifiedSubstringComparer : SubstringComparerBase
- {
- public override bool Equals(string? x, string? y) => string.Equals(x, y);
- public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).SequenceEqual(y.AsSpan(y!.Length + Index, Count));
- public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan(s.Length + Index, Count));
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
- internal abstract class SubstringComparerBase : StringComparerBase
- {
- public int Index;
- public int Count;
-
- public abstract bool EqualsPartial(string? x, string? y);
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using Xunit;
-
-namespace System.Collections.Frozen.Tests
-{
- public static class ComparerPickerTests
- {
- private static StringComparerBase NewPicker(string[] values, bool ignoreCase)
- {
- StringComparerBase c = ComparerPicker.Pick(values, ignoreCase, out int minLen, out int maxLenDiff);
-
- foreach (string s in values)
- {
- Assert.True(s.Length >= minLen);
- Assert.True(s.Length <= minLen + maxLenDiff);
- }
-
- return c;
- }
-
- [Fact]
- public static void LeftHand()
- {
- StringComparerBase c = NewPicker(new[] { "K0", "K20", "K300" }, false);
- Assert.IsType<LeftJustifiedSingleCharComparer>(c);
- Assert.Equal(1, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "S1" }, false);
- Assert.IsType<LeftJustifiedSingleCharComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "S1", "T1" }, false);
- Assert.IsType<LeftJustifiedSingleCharComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "SA1", "TA1", "SB1" }, false);
- Assert.IsType<LeftJustifiedSubstringComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(2, ((SubstringComparerBase)c).Count);
- }
-
- [Fact]
- public static void LeftHandCaseInsensitive()
- {
- StringComparerBase c = NewPicker(new[] { "É1" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "É1", "T1" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "ÉA1", "TA1", "ÉB1" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(2, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "ABCDEÉ1ABCDEF", "ABCDETA1ABCDEF", "ABCDESB1ABCDEF" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
- Assert.Equal(5, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "ABCDEFÉ1ABCDEF", "ABCDEFTA1ABCDEF", "ABCDEFSB1ABCDEF" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
- Assert.Equal(6, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "ABCÉDEFÉ1ABCDEF", "ABCÉDEFTA1ABCDEF", "ABCÉDEFSB1ABCDEF" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
- Assert.Equal(7, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
- }
-
- [Fact]
- public static void LeftHandCaseInsensitiveAscii()
- {
- StringComparerBase c = NewPicker(new[] { "S1" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "S1", "T1" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "SA1", "TA1", "SB1" }, true);
- Assert.IsType<LeftJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
- Assert.Equal(0, ((SubstringComparerBase)c).Index);
- Assert.Equal(2, ((SubstringComparerBase)c).Count);
- }
-
- [Fact]
- public static void RightHand()
- {
- StringComparerBase c = NewPicker(new[] { "1S", "1T" }, false);
- Assert.IsType<RightJustifiedSingleCharComparer>(c);
- Assert.Equal(-1, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "1AS", "1AT", "1BS" }, false);
- Assert.IsType<RightJustifiedSubstringComparer>(c);
- Assert.Equal(-2, ((SubstringComparerBase)c).Index);
- Assert.Equal(2, ((SubstringComparerBase)c).Count);
- }
-
- [Fact]
- public static void RightHandCaseInsensitive()
- {
- StringComparerBase c = NewPicker(new[] { "1É", "1T" }, true);
- Assert.IsType<RightJustifiedCaseInsensitiveSubstringComparer>(c);
- Assert.Equal(-1, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "1AÉ", "1AT", "1BÉ" }, true);
- Assert.IsType<RightJustifiedCaseInsensitiveSubstringComparer>(c);
- Assert.Equal(-2, ((SubstringComparerBase)c).Index);
- Assert.Equal(2, ((SubstringComparerBase)c).Count);
- }
-
- [Fact]
- public static void RightHandCaseInsensitiveAscii()
- {
- StringComparerBase c = NewPicker(new[] { "1S", "1T" }, true);
- Assert.IsType<RightJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
- Assert.Equal(-1, ((SubstringComparerBase)c).Index);
- Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
- c = NewPicker(new[] { "1AS", "1AT", "1BS" }, true);
- Assert.IsType<RightJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
- Assert.Equal(-2, ((SubstringComparerBase)c).Index);
- Assert.Equal(2, ((SubstringComparerBase)c).Count);
- }
-
- [Fact]
- public static void Full()
- {
- StringComparerBase c = NewPicker(new[] { "ABC", "DBC", "ADC", "ABD", "ABDABD" }, false);
- Assert.IsType<FullStringComparer>(c);
- }
-
- [Fact]
- public static void FullCaseInsensitive()
- {
- StringComparerBase c = NewPicker(new[] { "æbc", "DBC", "æDC", "æbd", "æbdæbd" }, true);
- Assert.IsType<FullCaseInsensitiveStringComparer>(c);
- }
-
- [Fact]
- public static void FullCaseInsensitiveAscii()
- {
- StringComparerBase c = NewPicker(new[] { "abc", "DBC", "aDC", "abd", "abdabd" }, true);
- Assert.IsType<FullCaseInsensitiveAsciiStringComparer>(c);
- }
-
- [Fact]
- public static void IsAllAscii()
- {
- Assert.True(ComparerPicker.IsAllAscii("abc".AsSpan()));
- Assert.True(ComparerPicker.IsAllAscii("abcdefghij".AsSpan()));
- Assert.False(ComparerPicker.IsAllAscii("abcdéfghij".AsSpan()));
- }
- }
-}
+++ /dev/null
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using Xunit;
-
-namespace System.Collections.Frozen.Tests
-{
- public static class ComparerTests
- {
- private static void Equal(SubstringComparerBase c, string a, string b, bool fullEqual)
- {
- Assert.True(c.EqualsPartial(a, b));
- Assert.Equal(c.GetHashCode(a), c.GetHashCode(b));
- Assert.Equal(fullEqual, c.Equals(a, b));
- }
-
- private static void Equal(StringComparerBase c, string a, string b, bool fullEqual)
- {
- Assert.Equal(c.GetHashCode(a), c.GetHashCode(b));
- Assert.Equal(fullEqual, c.Equals(a, b));
- }
-
- private static void NotEqual(SubstringComparerBase c, string a, string b)
- {
- Assert.False(c.EqualsPartial(a, b));
- Assert.False(c.Equals(a, b));
- Assert.NotEqual(c.GetHashCode(a), c.GetHashCode(b));
- }
-
- private static void NotEqual(StringComparerBase c, string a, string b)
- {
- Assert.False(c.Equals(a, b));
- Assert.NotEqual(c.GetHashCode(a), c.GetHashCode(b));
- }
-
- [Fact]
- public static void LeftHand()
- {
- var c = new LeftJustifiedSubstringComparer
- {
- Index = 0,
- Count = 1
- };
-
- Equal(c, "a", "a", true);
- Equal(c, "a", "aa", false);
- Equal(c, "a", "ab", false);
- NotEqual(c, "a", "A");
- NotEqual(c, "a", "b");
-
- c.Index = 1;
- c.Count = 1;
- Equal(c, "Aa", "Ba", false);
- Equal(c, "Aa", "Baa", false);
- Equal(c, "aa", "Bab", false);
- Equal(c, "Aa", "Aa", true);
- Equal(c, "Aab", "Aab", true);
- NotEqual(c, "Aa", "BA");
- NotEqual(c, "Aa", "Bb");
-
- c.Index = 1;
- c.Count = 2;
- Equal(c, "Aaa", "Baa", false);
- Equal(c, "Aaa", "Baaa", false);
- Equal(c, "aaa", "Baab", false);
- Equal(c, "Aaa", "Aaa", true);
- Equal(c, "Aaab", "Aaab", true);
- NotEqual(c, "Aaa", "BaA");
- NotEqual(c, "Aaa", "Bab");
- }
-
- [Fact]
- public static void LeftHandSingleChar()
- {
- var c = new LeftJustifiedSingleCharComparer
- {
- Index = 0,
- Count = 1
- };
-
- Equal(c, "a", "a", true);
- Equal(c, "a", "aa", false);
- Equal(c, "a", "ab", false);
- NotEqual(c, "a", "A");
- NotEqual(c, "a", "b");
-
- c.Index = 1;
- c.Count = 1;
- Equal(c, "Aa", "Ba", false);
- Equal(c, "Aa", "Baa", false);
- Equal(c, "aa", "Bab", false);
- Equal(c, "Aa", "Aa", true);
- Equal(c, "Aab", "Aab", true);
- NotEqual(c, "Aa", "BA");
- NotEqual(c, "Aa", "Bb");
- }
-
- [Fact]
- public static void LeftHandCaseInsensitive()
- {
- var c = new LeftJustifiedCaseInsensitiveSubstringComparer
- {
- Index = 0,
- Count = 1
- };
-
- Equal(c, "a", "a", true);
- Equal(c, "a", "A", true);
- Equal(c, "a", "aa", false);
- Equal(c, "a", "AA", false);
- Equal(c, "a", "ab", false);
- Equal(c, "a", "AB", false);
- NotEqual(c, "a", "b");
-
- c.Index = 1;
- c.Count = 1;
- Equal(c, "Xa", "Ya", false);
- Equal(c, "Xa", "YA", false);
- Equal(c, "Xa", "Xa", true);
- Equal(c, "Xa", "XA", true);
- Equal(c, "Xa", "Yaa", false);
- Equal(c, "Xa", "YAA", false);
- Equal(c, "Xa", "Yab", false);
- Equal(c, "Xa", "YAB", false);
- NotEqual(c, "Xa", "Yb");
- }
-
- [Fact]
- public static void LeftHandCaseInsensitiveAscii()
- {
- var c = new LeftJustifiedCaseInsensitiveAsciiSubstringComparer
- {
- Index = 0,
- Count = 1
- };
-
- Equal(c, "a", "a", true);
- Equal(c, "a", "A", true);
- Equal(c, "a", "aa", false);
- Equal(c, "a", "AA", false);
- Equal(c, "a", "ab", false);
- Equal(c, "a", "AB", false);
- NotEqual(c, "a", "b");
-
- c.Index = 1;
- c.Count = 1;
- Equal(c, "Xa", "Ya", false);
- Equal(c, "Xa", "YA", false);
- Equal(c, "Xa", "Xa", true);
- Equal(c, "Xa", "XA", true);
- Equal(c, "Xa", "Yaa", false);
- Equal(c, "Xa", "YAA", false);
- Equal(c, "Xa", "Yab", false);
- Equal(c, "Xa", "YAB", false);
- NotEqual(c, "Xa", "Yb");
- }
-
- [Fact]
- public static void RightHand()
- {
- var c = new RightJustifiedSubstringComparer
- {
- Index = -1,
- Count = 1
- };
-
- Equal(c, "a", "a", true);
- Equal(c, "a", "aa", false);
- Equal(c, "a", "ba", false);
- NotEqual(c, "a", "A");
- NotEqual(c, "a", "b");
-
- c.Index = -2;
- c.Count = 1;
- Equal(c, "aX", "aY", false);
- Equal(c, "XaX", "YaY", false);
- Equal(c, "XaX", "YYaY", false);
- Equal(c, "XXaX", "YaY", false);
- NotEqual(c, "XXaX", "YYa");
-
- c.Index = -2;
- c.Count = 2;
- Equal(c, "aa", "aa", true);
- Equal(c, "aa", "aaa", false);
- Equal(c, "aa", "baa", false);
- NotEqual(c, "aa", "AA");
- NotEqual(c, "aa", "bb");
- }
-
- [Fact]
- public static void RightHandSingleChar()
- {
- var c = new RightJustifiedSingleCharComparer
- {
- Index = -1,
- Count = 1
- };
-
- Equal(c, "a", "a", true);
- Equal(c, "a", "aa", false);
- Equal(c, "a", "ba", false);
- NotEqual(c, "a", "A");
- NotEqual(c, "a", "b");
-
- c.Index = -2;
- c.Count = 1;
- Equal(c, "aX", "aY", false);
- Equal(c, "XaX", "YaY", false);
- Equal(c, "XaX", "YYaY", false);
- Equal(c, "XXaX", "YaY", false);
- NotEqual(c, "XXaX", "YYa");
- }
-
- [Fact]
- public static void RightHandCaseInsensitive()
- {
- var c = new RightJustifiedCaseInsensitiveSubstringComparer
- {
- Index = -1,
- Count = 1
- };
-
- Equal(c, "a", "a", true);
- Equal(c, "a", "aa", false);
- Equal(c, "a", "ba", false);
- Equal(c, "a", "A", true);
- Equal(c, "a", "AA", false);
- Equal(c, "a", "BA", false);
- NotEqual(c, "a", "b");
-
- c.Index = -2;
- c.Count = 1;
- Equal(c, "aX", "aY", false);
- Equal(c, "XaX", "YaY", false);
- Equal(c, "XaX", "YYaY", false);
- Equal(c, "XXaX", "YaY", false);
- Equal(c, "aX", "AY", false);
- Equal(c, "XaX", "YAY", false);
- Equal(c, "XaX", "YYAY", false);
- Equal(c, "XXaX", "YAY", false);
- NotEqual(c, "XXaX", "YYa");
-
- c.Index = -2;
- c.Count = 2;
- Equal(c, "aa", "aa", true);
- Equal(c, "aa", "aaa", false);
- Equal(c, "aa", "baa", false);
- Equal(c, "aa", "AA", true);
- Equal(c, "aa", "AAA", false);
- Equal(c, "aa", "bAA", false);
- NotEqual(c, "aa", "bb");
- }
-
- [Fact]
- public static void RightHandCaseInsensitiveAscii()
- {
- var c = new RightJustifiedCaseInsensitiveAsciiSubstringComparer
- {
- Index = -1,
- Count = 1
- };
-
- Equal(c, "a", "a", true);
- Equal(c, "a", "aa", false);
- Equal(c, "a", "ba", false);
- Equal(c, "a", "A", true);
- Equal(c, "a", "AA", false);
- Equal(c, "a", "BA", false);
- NotEqual(c, "a", "b");
-
- c.Index = -2;
- c.Count = 1;
- Equal(c, "aX", "aY", false);
- Equal(c, "XaX", "YaY", false);
- Equal(c, "XaX", "YYaY", false);
- Equal(c, "XXaX", "YaY", false);
- Equal(c, "aX", "AY", false);
- Equal(c, "XaX", "YAY", false);
- Equal(c, "XaX", "YYAY", false);
- Equal(c, "XXaX", "YAY", false);
- NotEqual(c, "XXaX", "YYa");
-
- c.Index = -2;
- c.Count = 2;
- Equal(c, "aa", "aa", true);
- Equal(c, "aa", "aaa", false);
- Equal(c, "aa", "baa", false);
- Equal(c, "aa", "AA", true);
- Equal(c, "aa", "AAA", false);
- Equal(c, "aa", "bAA", false);
- NotEqual(c, "aa", "bb");
- }
-
- [Fact]
- public static void Full()
- {
- var c = new FullStringComparer();
-
- Equal(c, "", "", true);
- Equal(c, "A", "A", true);
- Equal(c, "AA", "AA", true);
-
- NotEqual(c, "A", "AA");
- NotEqual(c, "AA", "A");
- }
-
- [Fact]
- public static void FullCaseInsensitive()
- {
- var c = new FullCaseInsensitiveStringComparer();
-
- Equal(c, "", "", true);
- Equal(c, "A", "A", true);
- Equal(c, "A", "a", true);
- Equal(c, "a", "A", true);
- Equal(c, "AA", "aa", true);
- Equal(c, "aa", "AA", true);
-
- NotEqual(c, "A", "AA");
- NotEqual(c, "AA", "A");
- }
-
- [Fact]
- public static void FullCaseInsensitiveAscii()
- {
- var c = new FullCaseInsensitiveAsciiStringComparer();
-
- Equal(c, "", "", true);
- Equal(c, "A", "A", true);
- Equal(c, "A", "a", true);
- Equal(c, "a", "A", true);
- Equal(c, "AA", "aa", true);
- Equal(c, "aa", "AA", true);
-
- NotEqual(c, "A", "AA");
- NotEqual(c, "AA", "A");
- }
- }
-}
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Xunit;
+
+namespace System.Collections.Frozen.Tests
+{
+ public static class KeyAnalyzerTests
+ {
+ private static KeyAnalyzer.AnalysisResults RunAnalysis(string[] values, bool ignoreCase)
+ {
+ KeyAnalyzer.Analyze(values, ignoreCase, out KeyAnalyzer.AnalysisResults r);
+
+ foreach (string s in values)
+ {
+ Assert.True(s.Length >= r.MinimumLength);
+ Assert.True(s.Length <= r.MinimumLength + r.MaximumLengthDiff);
+ }
+
+ return r;
+ }
+
+ [Fact]
+ public static void LeftHand()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "K0", "K20", "K300" }, false);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.False(r.IgnoreCase);
+ Assert.Equal(1, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "S1" }, false);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.False(r.IgnoreCase);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "S1", "T1" }, false);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.False(r.IgnoreCase);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "SA1", "TA1", "SB1" }, false);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.False(r.IgnoreCase);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(2, r.HashCount);
+ }
+
+ [Fact]
+ public static void LeftHandCaseInsensitive()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "É1" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "É1", "T1" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "ÉA1", "TA1", "ÉB1" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(2, r.HashCount);
+
+ r = RunAnalysis(new[] { "ABCDEÉ1ABCDEF", "ABCDETA1ABCDEF", "ABCDESB1ABCDEF" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ Assert.Equal(5, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "ABCDEFÉ1ABCDEF", "ABCDEFTA1ABCDEF", "ABCDEFSB1ABCDEF" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ Assert.Equal(6, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "ABCÉDEFÉ1ABCDEF", "ABCÉDEFTA1ABCDEF", "ABCÉDEFSB1ABCDEF" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ Assert.Equal(7, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+ }
+
+ [Fact]
+ public static void LeftHandCaseInsensitiveAscii()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "S1" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "S1", "T1" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "SA1", "TA1", "SB1" }, true);
+ Assert.False(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ Assert.Equal(0, r.HashIndex);
+ Assert.Equal(2, r.HashCount);
+ }
+
+ [Fact]
+ public static void RightHand()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "1S", "1T" }, false);
+ Assert.True(r.RightJustifiedSubstring);
+ Assert.False(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ Assert.Equal(-1, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "1AS", "1AT", "1BS" }, false);
+ Assert.True(r.RightJustifiedSubstring);
+ Assert.False(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ Assert.Equal(-2, r.HashIndex);
+ Assert.Equal(2, r.HashCount);
+ }
+
+ [Fact]
+ public static void RightHandCaseInsensitive()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "1É", "1T" }, true);
+ Assert.True(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ Assert.Equal(-1, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "1AÉ", "1AT", "1BÉ" }, true);
+ Assert.True(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ Assert.Equal(-2, r.HashIndex);
+ Assert.Equal(2, r.HashCount);
+ }
+
+ [Fact]
+ public static void RightHandCaseInsensitiveAscii()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "1S", "1T" }, true);
+ Assert.True(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ Assert.Equal(-1, r.HashIndex);
+ Assert.Equal(1, r.HashCount);
+
+ r = RunAnalysis(new[] { "1AS", "1AT", "1BS" }, true);
+ Assert.True(r.RightJustifiedSubstring);
+ Assert.True(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ Assert.Equal(-2, r.HashIndex);
+ Assert.Equal(2, r.HashCount);
+ }
+
+ [Fact]
+ public static void Full()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "ABC", "DBC", "ADC", "ABD", "ABDABD" }, false);
+ Assert.False(r.SubstringHashing);
+ Assert.False(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ }
+
+ [Fact]
+ public static void FullCaseInsensitive()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "æbc", "DBC", "æDC", "æbd", "æbdæbd" }, true);
+ Assert.False(r.SubstringHashing);
+ Assert.True(r.IgnoreCase);
+ Assert.False(r.AllAscii);
+ }
+
+ [Fact]
+ public static void FullCaseInsensitiveAscii()
+ {
+ KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "abc", "DBC", "aDC", "abd", "abdabd" }, true);
+ Assert.False(r.SubstringHashing);
+ Assert.True(r.IgnoreCase);
+ Assert.True(r.AllAscii);
+ }
+
+ [Fact]
+ public static void IsAllAscii()
+ {
+ Assert.True(KeyAnalyzer.IsAllAscii("abc".AsSpan()));
+ Assert.True(KeyAnalyzer.IsAllAscii("abcdefghij".AsSpan()));
+ Assert.False(KeyAnalyzer.IsAllAscii("abcdéfghij".AsSpan()));
+ }
+ }
+}
<Compile Include="$(CommonTestPath)System\Collections\DictionaryExtensions.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" Link="Common\System\Collections\DictionaryExtensions.cs" />
<Compile Include="BadHasher.cs" />
<Compile Include="EverythingEqual.cs" />
+ <Compile Include="Frozen\KeyAnalyzerTests.cs" />
<Compile Include="ImmutableArrayExtensionsTest.cs" />
<Compile Include="ImmutableArrayTest.cs" />
<Compile Include="ImmutableArrayTest.netcoreapp.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
<!-- Frozen Collections Tests -->
<Compile Include="Frozen\FrozenSetTests.cs" />
<Compile Include="Frozen\FrozenDictionaryTests.cs" />
- <Compile Include="Frozen\ComparerPickerTests.cs" />
- <Compile Include="Frozen\ComparerTests.cs" />
<!-- Common Collections tests -->
<Compile Include="$(CommonTestPath)System\Collections\CollectionAsserts.cs" Link="Common\System\Collections\CollectionAsserts.cs" />
<Compile Include="$(CommonTestPath)System\Collections\DelegateEqualityComparer.cs" Link="Common\System\Collections\DelegateEqualityComparer.cs" />