< Summary

Information
Class: SwiftCollections.Utility.SwiftHashTools
Assembly: SwiftCollections
File(s): /home/runner/work/SwiftCollections/SwiftCollections/src/SwiftCollections/Utility/SwiftHashTools.cs
Line coverage
100%
Covered lines: 120
Uncovered lines: 0
Coverable lines: 120
Total lines: 456
Line coverage: 100%
Branch coverage
100%
Covered branches: 58
Total branches: 58
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/SwiftCollections/SwiftCollections/src/SwiftCollections/Utility/SwiftHashTools.cs

#LineLine coverage
 1//=======================================================================
 2// SwiftHashTools.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024–present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using SwiftCollections.Diagnostics;
 9using System;
 10using System.Collections;
 11using System.Collections.Generic;
 12using System.Runtime.CompilerServices;
 13using System.Runtime.Serialization;
 14using System.Security.Cryptography;
 15using System.Threading;
 16
 17namespace SwiftCollections.Utility;
 18
 19/// <summary>
 20/// Provides utility functions for generating, combining, and working with hash codes.
 21/// </summary>
 22public static class SwiftHashTools
 23{
 24    internal const int DefaultDeterministicStringHashSeed = 0x51C4B5D;
 25    internal const int DefaultDeterministicObjectHashSeed = 0x1F6E4D75;
 26
 127    private static readonly IEqualityComparer<string> s_deterministicStringComparer =
 128        new SwiftDeterministicStringEqualityComparer(DefaultDeterministicStringHashSeed);
 29
 130    private static readonly IEqualityComparer<object> s_deterministicObjectComparer =
 131        new SwiftDeterministicObjectEqualityComparer(DefaultDeterministicObjectHashSeed);
 32
 33    /// <summary>
 34    /// Provides a cryptographic random number generator for collision-hardening entropy.
 35    /// </summary>
 36    private static RandomNumberGenerator? rng;
 37
 38    /// <summary>
 39    /// Stores random bytes used for entropy generation.
 40    /// </summary>
 41    private static byte[]? data;
 42
 43    /// <summary>
 44    /// Tracks the current index in the entropy data buffer.
 45    /// </summary>
 146    private static int currentIndex = 1024;
 47
 48    /// <summary>
 49    /// Synchronization object for thread-safe operations.
 50    /// </summary>
 151    private static readonly object lockObj = new();
 52
 53    /// <summary>
 54    /// Holds serialization information for objects during the serialization process.
 55    /// </summary>
 56    private static ConditionalWeakTable<object, SerializationInfo>? s_SerializationInfoTable;
 57
 58    /// <summary>
 59    /// Gets the table that stores serialization information for objects.
 60    /// </summary>
 61    internal static ConditionalWeakTable<object, SerializationInfo> SerializationInfoTable
 62    {
 63        get
 64        {
 265            if (s_SerializationInfoTable == null)
 66            {
 167                ConditionalWeakTable<object, SerializationInfo> value = new();
 168                Interlocked.CompareExchange(ref s_SerializationInfoTable, value, null);
 69            }
 70
 271            return s_SerializationInfoTable;
 72        }
 73    }
 74
 75    /// <summary>
 76    /// Determines whether the specified integer is a power of two.
 77    /// </summary>
 78    /// <remarks>Zero and negative values are not considered powers of two.</remarks>
 79    /// <param name="x">The integer value to test.</param>
 80    /// <returns>true if the value of x is a power of two; otherwise, false.</returns>
 81    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 482    public static bool IsPowerOfTwo(int x) => (x != 0) && ((x & (x - 1)) == 0);
 83
 84    /// <summary>
 85    /// Calculates the smallest power of two that is greater than or equal to the specified integer.
 86    /// This method is used to ensure that capacities are powers of two, enabling optimizations
 87    /// in indexing operations through bitwise arithmetic.
 88    /// </summary>
 89    /// <param name="value">The integer value for which to find the next power of two.</param>
 90    /// <returns>The smallest power of two greater than or equal to <paramref name="value"/>.</returns>
 91    /// <remarks>
 92    /// If <paramref name="value"/> is less than or equal to zero, the method returns 1.
 93    /// If <paramref name="value"/> is too large to represent as a power of two within an <c>int</c>,
 94    /// the methods returns `int.Max`.
 95    /// </remarks>
 96    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 97    public static int NextPowerOfTwo(int value)
 98    {
 1247099        if (value <= 0) return 1;
 12467100        if (value >= (1 << 30)) return int.MaxValue;
 101
 12465102        value--;
 12465103        value |= value >> 1;
 12465104        value |= value >> 2;
 12465105        value |= value >> 4;
 12465106        value |= value >> 8;
 12465107        value |= value >> 16;
 12465108        value++;
 12465109        return value;
 110    }
 111
 112    /// <summary>
 113    /// Determines whether the specified comparer is a recognized, well-known equality comparer supported by the system.
 114    /// </summary>
 115    /// <remarks>
 116    /// A well-known equality comparer is one that is commonly used and recognized by the system,
 117    /// such as the default equality comparers for string and object, or specific deterministic comparers.
 118    /// Use this method to check if a comparer is supported for optimized or special handling.
 119    /// </remarks>
 120    /// <param name="comparer">
 121    /// The object to test as an equality comparer.
 122    /// This can be null or an instance of a supported equality comparer type.
 123    /// </param>
 124    /// <returns>true if the comparer is a well-known equality comparer; otherwise, false.</returns>
 125    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 126    public static bool IsWellKnownEqualityComparer(object comparer)
 127    {
 52128        return comparer == null
 52129            || comparer == EqualityComparer<string>.Default
 52130            || comparer == EqualityComparer<object>.Default
 52131            || comparer is SwiftDeterministicStringEqualityComparer
 52132            || comparer is SwiftDeterministicObjectEqualityComparer;
 133    }
 134
 135    /// <summary>
 136    /// Gets the default comparer used by SwiftCollections for the specified type when no comparer is supplied.
 137    /// String keys are hashed deterministically. Object keys hash strings deterministically, while other object-key
 138    /// determinism still depends on the underlying key type's <see cref="object.GetHashCode()"/> implementation.
 139    /// </summary>
 140    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 141    public static IEqualityComparer<T> GetDeterministicEqualityComparer<T>()
 142    {
 474143        if (typeof(T) == typeof(string))
 91144            return (IEqualityComparer<T>)GetDeterministicStringEqualityComparer();
 145
 383146        if (typeof(T) == typeof(object))
 6147            return (IEqualityComparer<T>)GetDeterministicObjectEqualityComparer();
 148
 377149        return EqualityComparer<T>.Default;
 150    }
 151
 152    /// <summary>
 153    /// Gets the deterministic default comparer used by SwiftCollections for strings.
 154    /// </summary>
 155    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 156    public static IEqualityComparer<string> GetDeterministicStringEqualityComparer()
 92157        => s_deterministicStringComparer;
 158
 159    /// <summary>
 160    /// Gets a deterministic string comparer using the supplied seed.
 161    /// </summary>
 162    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 163    public static IEqualityComparer<string> GetDeterministicStringEqualityComparer(int seed)
 2164        => seed == DefaultDeterministicStringHashSeed
 2165            ? s_deterministicStringComparer
 2166            : new SwiftDeterministicStringEqualityComparer(seed);
 167
 168    /// <summary>
 169    /// Gets the default comparer used by SwiftCollections for object keys.
 170    /// Strings are hashed deterministically even when stored as objects. Other object keys still depend on their
 171    /// runtime <see cref="object.GetHashCode()"/> implementations.
 172    /// </summary>
 173    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 174    public static IEqualityComparer<object> GetDeterministicObjectEqualityComparer()
 7175        => s_deterministicObjectComparer;
 176
 177    /// <summary>
 178    /// Gets an object comparer using the supplied seed.
 179    /// Strings are hashed deterministically even when stored as objects. Other object keys still depend on their
 180    /// runtime <see cref="object.GetHashCode()"/> implementations.
 181    /// </summary>
 182    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 183    public static IEqualityComparer<object> GetDeterministicObjectEqualityComparer(int seed)
 2184        => seed == DefaultDeterministicObjectHashSeed
 2185            ? s_deterministicObjectComparer
 2186            : new SwiftDeterministicObjectEqualityComparer(seed);
 187
 188    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 189    internal static IEqualityComparer<T> GetDefaultEqualityComparer<T>(IEqualityComparer<T>? comparer = null)
 447190        => comparer ?? GetDeterministicEqualityComparer<T>();
 191
 192    /// <summary>
 193    /// Returns an IEqualityComparer instance optimized for use with Swift serialization, based on the specified compare
 194    /// </summary>
 195    /// <remarks>
 196    /// Use this method to obtain an equality comparer that ensures deterministic behavior when serializing with Swift.
 197    /// If the provided comparer is not recognized, a default Swift object equality comparer is returned.
 198    /// </remarks>
 199    /// <param name="comparer">
 200    /// The comparer object to evaluate.
 201    /// Determines which specialized Swift equality comparer to return.
 202    /// Can be an EqualityComparer for string or object, or a custom Swift deterministic comparer.
 203    /// </param>
 204    /// <returns>
 205    /// An IEqualityComparer instance suitable for Swift serialization.
 206    /// Returns a specialized comparer for strings or objects, depending on the input.
 207    /// </returns>
 208    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 209    public static IEqualityComparer GetSwiftEqualityComparer(object comparer)
 210    {
 7211        if (comparer == EqualityComparer<string>.Default || comparer is SwiftDeterministicStringEqualityComparer)
 4212            return new SwiftStringEqualityComparer();
 213
 3214        if (comparer == EqualityComparer<object>.Default || comparer is SwiftDeterministicObjectEqualityComparer)
 3215            return new SwiftObjectEqualityComparer();
 216
 217        return new SwiftObjectEqualityComparer();
 218    }
 219
 220    /// <summary>
 221    /// Generates a cryptographically strong random 64-bit integer for collision-hardening entropy.
 222    /// </summary>
 223    /// <returns>A 64-bit integer filled with cryptographically strong random bytes.</returns>
 224    internal static long GetEntropy()
 225    {
 139226        lock (lockObj)
 227        {
 139228            if (currentIndex == 1024 || rng == null || data == null)
 229            {
 2230                rng ??= RandomNumberGenerator.Create();
 2231                data ??= new byte[1024];
 232
 2233                rng.GetBytes(data);
 2234                currentIndex = 0;
 235            }
 236
 139237            long result = BitConverter.ToInt64(data, currentIndex);
 139238            currentIndex += 8;
 139239            return result;
 240        }
 139241    }
 242
 243    #region Hash Code Combination
 244
 245    /// <summary>
 246    /// Combines the hash codes of the elements in a tuple using a DJB2-inspired mixing strategy.
 247    /// </summary>
 248    public static int CombineHashCodes(
 249        this ITuple tupled,
 250        int seed = 5381,
 251        int shift1 = 16,
 252        int shift2 = 5,
 253        int shift3 = 27,
 254        int factor3 = 1566083941)
 255    {
 1256        int hash1 = (seed << shift1) + seed;
 1257        int hash2 = hash1;
 258
 10259        for (int i = 0; i < tupled.Length; i++)
 260        {
 4261            int itemHash = tupled[i]?.GetHashCode() ?? 0;
 262            unchecked
 263            {
 4264                if (i % 2 == 0)
 2265                    hash1 = ((hash1 << shift2) + hash1 + (hash1 >> shift3)) ^ itemHash;
 266                else
 2267                    hash2 = ((hash2 << shift2) + hash2 + (hash2 >> shift3)) ^ itemHash;
 268            }
 269        }
 270
 1271        return hash1 ^ (hash2 * factor3);
 272    }
 273
 274    /// <summary>
 275    /// Combines the hash codes of a set of objects using a DJB2-inspired mixing strategy.
 276    /// </summary>
 277    public static int CombineHashCodes(
 278        this object[] values,
 279        int seed = 5381,
 280        int shift1 = 16,
 281        int shift2 = 5,
 282        int shift3 = 27,
 283        int factor3 = 1566083941)
 284    {
 7285        int hash1 = (seed << shift1) + seed;
 7286        int hash2 = hash1;
 287
 62288        for (int i = 0; i < values.Length; i++)
 289        {
 24290            var itemHash = values[i]?.GetHashCode() ?? 0;
 291            unchecked
 292            {
 24293                if ((i & 1) == 0)
 13294                    hash1 = ((hash1 << shift2) + hash1 + (hash1 >> shift3)) ^ itemHash;
 295                else
 11296                    hash2 = ((hash2 << shift2) + hash2 + (hash2 >> shift3)) ^ itemHash;
 297            }
 298        }
 299
 7300        return hash1 + (hash2 * factor3);
 301    }
 302
 303    /// <summary>
 304    /// Combines the hash codes of the provided objects using a DJB2-inspired mixing strategy.
 305    /// </summary>
 6306    public static int CombineHashCodes(params object[] values) => values.CombineHashCodes();
 307
 308    /// <summary>
 309    /// Combines two integer hash codes without allocating the object-array params path.
 310    /// </summary>
 311    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 312    public static int CombineHashCodes(
 313        int value1,
 314        int value2)
 315    {
 316        const int seed = 5381;
 317        const int shift1 = 16;
 318        const int shift2 = 5;
 319        const int shift3 = 27;
 320        const int factor3 = 1566083941;
 1321        int hash1 = (seed << shift1) + seed;
 1322        int hash2 = hash1;
 323
 1324        hash1 = MixHashCode(hash1, value1, shift2, shift3);
 1325        hash2 = MixHashCode(hash2, value2, shift2, shift3);
 326
 1327        return hash1 + (hash2 * factor3);
 328    }
 329
 330    /// <summary>
 331    /// Combines three integer hash codes without allocating the object-array params path.
 332    /// </summary>
 333    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 334    public static int CombineHashCodes(
 335        int value1,
 336        int value2,
 337        int value3)
 338    {
 339        const int seed = 5381;
 340        const int shift1 = 16;
 341        const int shift2 = 5;
 342        const int shift3 = 27;
 343        const int factor3 = 1566083941;
 1042344        int hash1 = (seed << shift1) + seed;
 1042345        int hash2 = hash1;
 346
 1042347        hash1 = MixHashCode(hash1, value1, shift2, shift3);
 1042348        hash2 = MixHashCode(hash2, value2, shift2, shift3);
 1042349        hash1 = MixHashCode(hash1, value3, shift2, shift3);
 350
 1042351        return hash1 + (hash2 * factor3);
 352    }
 353
 354    /// <summary>
 355    /// Combines four integer hash codes without allocating the object-array params path.
 356    /// </summary>
 357    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 358    public static int CombineHashCodes(
 359        int value1,
 360        int value2,
 361        int value3,
 362        int value4)
 363    {
 364        const int seed = 5381;
 365        const int shift1 = 16;
 366        const int shift2 = 5;
 367        const int shift3 = 27;
 368        const int factor3 = 1566083941;
 1369        int hash1 = (seed << shift1) + seed;
 1370        int hash2 = hash1;
 371
 1372        hash1 = MixHashCode(hash1, value1, shift2, shift3);
 1373        hash2 = MixHashCode(hash2, value2, shift2, shift3);
 1374        hash1 = MixHashCode(hash1, value3, shift2, shift3);
 1375        hash2 = MixHashCode(hash2, value4, shift2, shift3);
 376
 1377        return hash1 + (hash2 * factor3);
 378    }
 379
 380    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 381    private static int MixHashCode(int hash, int itemHash, int shift2, int shift3)
 382    {
 383        unchecked
 384        {
 3132385            return ((hash << shift2) + hash + (hash >> shift3)) ^ itemHash;
 386        }
 387    }
 388
 389    /// <summary>
 390    /// Computes a hash code for a string using the MurmurHash3 algorithm, incorporating entropy for randomization.
 391    /// </summary>
 392    /// <param name="key">The string to hash.</param>
 393    /// <param name="seed">The seed value for the hash function.</param>
 394    /// <returns>An integer hash code for the string.</returns>
 395    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 396    public static unsafe int MurmurHash3(string key, int seed)
 397    {
 212461398        SwiftThrowHelper.ThrowIfNull(key, nameof(key));
 399
 400        const uint c1 = 0xcc9e2d51;
 401        const uint c2 = 0x1b873593;
 212460402        uint h1 = (uint)seed;
 212460403        ReadOnlySpan<char> chars = key.AsSpan();
 404
 212460405        fixed (char* ptr = chars)
 406        {
 212460407            int length = chars.Length;
 212460408            int blockCount = length / 2;
 409
 410            unchecked
 411            {
 412                // Process 32-bit blocks
 6690504413                for (int i = 0; i < blockCount; i++)
 414                {
 3132792415                    uint k1 = *(uint*)(ptr + (i * 2));
 3132792416                    k1 *= c1;
 3132792417                    k1 = RotateLeft(k1, 15);
 3132792418                    k1 *= c2;
 3132792419                    h1 ^= k1;
 3132792420                    h1 = RotateLeft(h1, 13);
 3132792421                    h1 = h1 * 5 + 0xe6546b64;
 422                }
 423
 424                // Handle remaining characters if odd length
 212460425                if (length % 2 != 0)
 426                {
 25812427                    uint k1 = ptr[length - 1];
 25812428                    k1 *= c1;
 25812429                    k1 = RotateLeft(k1, 15);
 25812430                    k1 *= c2;
 25812431                    h1 ^= k1;
 432                }
 433            }
 434
 212460435            h1 ^= (uint)length;
 212460436            h1 = FMix(h1);
 212460437            return (int)(h1 & 0x7FFFFFFF);
 438        }
 439    }
 440
 441    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 6291396442    private static uint RotateLeft(uint x, int r) => (x << r) | (x >> (32 - r));
 443
 444    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 445    private static uint FMix(uint h)
 446    {
 212460447        h ^= h >> 16;
 212460448        h *= 0x85ebca6b;
 212460449        h ^= h >> 13;
 212460450        h *= 0xc2b2ae35;
 212460451        h ^= h >> 16;
 212460452        return h;
 453    }
 454
 455    #endregion
 456}