< Summary

Information
Class: SwiftCollections.SwiftBiDictionary<T1, T2>
Assembly: SwiftCollections
File(s): /home/runner/work/SwiftCollections/SwiftCollections/src/SwiftCollections/Collection/SwiftBiDictionary.cs
Line coverage
100%
Covered lines: 112
Uncovered lines: 0
Coverable lines: 112
Total lines: 421
Line coverage: 100%
Branch coverage
100%
Covered branches: 38
Total branches: 38
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
.ctor(...)100%11100%
.ctor(...)100%11100%
.ctor(...)100%22100%
.ctor(...)100%11100%
get_ReverseSyncRoot()100%22100%
get_Item(...)100%11100%
set_Item(...)100%66100%
get_State()100%11100%
set_State(...)100%22100%
Remove(...)100%66100%
Add(...)100%66100%
InsertIfNotExist(...)100%44100%
Remove(...)100%44100%
Clear()100%11100%
SetComparer(...)100%66100%
TryGetKey(...)100%11100%
GetKey(...)100%11100%
ContainsValue(...)100%11100%

File(s)

/home/runner/work/SwiftCollections/SwiftCollections/src/SwiftCollections/Collection/SwiftBiDictionary.cs

#LineLine coverage
 1//=======================================================================
 2// SwiftBiDictionary.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 Chronicler;
 9using MemoryPack;
 10using SwiftCollections.Diagnostics;
 11using SwiftCollections.Utility;
 12using System;
 13using System.Collections.Generic;
 14using System.Text.Json.Serialization;
 15
 16namespace SwiftCollections;
 17
 18/// <summary>
 19/// Represents a bidirectional dictionary that allows for efficient lookups in both directions,
 20/// mapping keys to values and values back to keys. Both keys and values must be unique to maintain
 21/// the integrity of the bidirectional relationship.
 22/// Inherits from <see cref="SwiftDictionary{TKey, TValue}"/> and maintains a reverse map for reverse lookups.
 23/// </summary>
 24/// <typeparam name="T1">The type of the keys in the forward dictionary.</typeparam>
 25/// <typeparam name="T2">The type of the values in the forward dictionary.</typeparam>
 26/// <remarks>
 27/// The comparer is not serialized. After deserialization the dictionary reverts
 28/// to the same default comparer selection used by a new instance for both the
 29/// forward and reverse maps. String keys use SwiftCollections' deterministic
 30/// default comparer. Object keys use a SwiftCollections comparer that hashes
 31/// strings deterministically, while other object-key determinism still depends
 32/// on the underlying key type's <see cref="object.GetHashCode()"/>
 33/// implementation. Other key types use <see cref="EqualityComparer{T1}.Default"/>
 34/// and <see cref="EqualityComparer{T2}.Default"/>.
 35///
 36/// If a custom comparer is required it can be reapplied using
 37/// <see cref="SetComparer(IEqualityComparer{T1}, IEqualityComparer{T2})"/>.
 38/// </remarks>
 39[Serializable]
 40[JsonConverter(typeof(StateJsonConverterFactory))]
 41[MemoryPackable]
 42public partial class SwiftBiDictionary<T1, T2> : SwiftDictionary<T1, T2>, IStateBacked<SwiftDictionaryState<T1, T2>>
 43    where T1 : notnull
 44    where T2 : notnull
 45{
 46    #region Fields
 47
 48    /// <summary>
 49    /// The reverse map for bidirectional lookup, mapping from <typeparamref name="T2"/> to <typeparamref name="T1"/>.
 50    /// </summary>
 51    private SwiftDictionary<T2, T1> _reverseMap;
 52
 53    /// <summary>
 54    /// The comparer used to determine equality of keys and to generate hash codes.
 55    /// </summary>
 56    [NonSerialized]
 57    private IEqualityComparer<T2> _reverseComparer;
 58
 59    /// <summary>
 60    /// An object used to synchronize access to the reverse map during serialization and deserialization.
 61    /// </summary>
 62    [NonSerialized]
 63    private object? _reverseSyncRoot;
 64
 65    #endregion
 66
 67    #region Constructors
 68
 69    /// <summary>
 70    /// Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class that is empty and uses the defau
 71    /// </summary>
 3472    public SwiftBiDictionary() : this(null, null) { }
 73
 74    /// <summary>
 75    /// Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class that is empty and uses the speci
 76    /// </summary>
 77    /// <param name="comparer1">The comparer to use for the keys.</param>
 78    /// <param name="comparer2">The comparer to use for the values.</param>
 2279    public SwiftBiDictionary(IEqualityComparer<T1>? comparer1, IEqualityComparer<T2>? comparer2) : base(DefaultCapacity,
 80    {
 2281        _reverseComparer = SwiftHashTools.GetDefaultEqualityComparer(comparer2);
 2282        _reverseMap = new SwiftDictionary<T2, T1>(DefaultCapacity, _reverseComparer);
 2283    }
 84
 85    /// <summary>
 86    /// Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class that contains elements copied fr
 87    /// </summary>
 88    /// <param name="dictionary">The dictionary whose elements are copied to the new <see cref="SwiftBiDictionary{T1, T2
 289    public SwiftBiDictionary(IDictionary<T1, T2> dictionary) : this(dictionary, null, null) { }
 90
 91    /// <summary>
 92    /// Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class that contains elements copied fr
 93    /// </summary>
 94    /// <param name="dictionary">The dictionary whose elements are copied to the new <see cref="SwiftBiDictionary{T1, T2
 95    /// <param name="comparer1">The comparer to use for the keys.</param>
 96    /// <param name="comparer2">The comparer to use for the values.</param>
 97    public SwiftBiDictionary(IDictionary<T1, T2> dictionary, IEqualityComparer<T1>? comparer1, IEqualityComparer<T2>? co
 298        : this(comparer1, comparer2)
 99    {
 2100        SwiftThrowHelper.ThrowIfNull(dictionary, nameof(dictionary));
 101
 12102        foreach (var kvp in dictionary)
 4103            Add(kvp.Key, kvp.Value);
 2104    }
 105
 106    ///  <summary>
 107    ///  Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class with the specified <see cref="S
 108    ///  </summary>
 109    ///  <param name="state">The state containing the internal array, count, offset, and version for initialization.</pa
 110    [MemoryPackConstructor]
 6111    public SwiftBiDictionary(SwiftDictionaryState<T1, T2> state)
 112    {
 6113        State = state;
 114
 6115        SwiftThrowHelper.ThrowIfNull(_reverseMap, nameof(_reverseMap));
 6116        SwiftThrowHelper.ThrowIfNull(_reverseComparer, nameof(_reverseComparer));
 6117    }
 118
 119    #endregion
 120
 121    #region Properties
 122
 123    /// <summary>
 124    /// Gets an object that can be used to synchronize access to the reverse collection.
 125    /// </summary>
 126    /// <remarks>
 127    /// Use this object to lock the reverse collection during multithreaded operations to ensure thread safety.
 128    /// This property is intended for advanced scenarios where manual synchronization is required.
 129    /// </remarks>
 130    [JsonIgnore]
 131    [MemoryPackIgnore]
 145132    public object ReverseSyncRoot => _reverseSyncRoot ??= new object();
 133
 134    /// <summary>
 135    /// Gets or sets the value associated with the specified key. Ensures that each value is unique within the collectio
 136    /// </summary>
 137    /// <remarks>
 138    /// Setting a value that already exists in the collection will result in an exception, as each value must be unique.
 139    /// If the key does not exist, a new key-value pair is added.
 140    /// If the key exists and the value is unchanged, no operation is performed.
 141    /// </remarks>
 142    /// <param name="key">The key whose value to get or set. Cannot be null.</param>
 143    /// <returns>The value associated with the specified key.</returns>
 144    /// <exception cref="ArgumentException">Thrown when the specified value already exists in the collection.</exception
 145    [JsonIgnore]
 146    [MemoryPackIgnore]
 147    public new T2 this[T1 key]
 148    {
 13149        get => base[key];
 150        set
 151        {
 6152            SwiftThrowHelper.ThrowIfNullGeneric(key, nameof(key));
 153
 6154            lock (ReverseSyncRoot)
 155            {
 6156                if (TryGetValue(key, out T2 oldValue))
 157                {
 158                    // No change
 4159                    if (_reverseComparer.Equals(oldValue, value))
 1160                        return;
 161
 162                    // Prevent duplicate values
 3163                    SwiftThrowHelper.ThrowIfArgument(_reverseMap.ContainsKey(value), nameof(value), "Value already exist
 164
 165                    // Remove old reverse mapping
 2166                    _reverseMap.Remove(oldValue);
 167
 168                    // Update forward value
 2169                    int index = FindEntry(key);
 2170                    _entries[index].Value = value;
 171
 172                    // Insert new reverse mapping
 2173                    _reverseMap.Add(value, key);
 174
 2175                    _version++;
 176                }
 177                else
 178                {
 179                    // New insert
 2180                    SwiftThrowHelper.ThrowIfArgument(_reverseMap.ContainsKey(value), nameof(value), "Value already exist
 181
 2182                    CheckLoadThreshold();
 183
 2184                    bool added = InsertIfNotExist(key, value);
 185
 2186                    if (added)
 2187                        _reverseMap.Add(value, key);
 188                }
 2189            }
 5190        }
 191    }
 192
 193    /// <summary>
 194    /// Gets or sets the current state of the dictionary, including its items and configuration.
 195    /// </summary>
 196    /// <remarks>
 197    /// Setting this property updates the internal reverse mapping and comparer to reflect the new state.
 198    /// The reverse mapping is rebuilt to ensure consistency with the updated state.
 199    /// This property is intended for serialization and deserialization scenarios.
 200    /// </remarks>
 201    [JsonInclude]
 202    [MemoryPackInclude]
 203    public new SwiftDictionaryState<T1, T2> State
 204    {
 5205        get => base.State;
 206        internal set
 207        {
 6208            _reverseComparer = SwiftHashTools.GetDefaultEqualityComparer<T2>();
 6209            _reverseMap = new SwiftDictionary<T2, T1>(value.Items.Length, _reverseComparer);
 210
 6211            base.State = value;
 212
 6213            lock (ReverseSyncRoot)
 214            {
 6215                _reverseMap.Clear();
 216
 46217                foreach (var kv in this)
 17218                    _reverseMap.Add(kv.Value, kv.Key);
 219            }
 6220        }
 221    }
 222
 223    #endregion
 224
 225    #region Collection Manipulation
 226
 227    /// <summary>
 228    /// Removes the key-value pair from the <see cref="SwiftBiDictionary{T1, T2}"/>.
 229    /// Also removes the corresponding value-key pair from the reverse map.
 230    /// </summary>
 231    /// <param name="key">The key of the element to remove.</param>
 232    /// <param name="value">The value of the element to remove.</param>
 233    /// <returns><c>true</c> if the element is successfully found and removed; otherwise, <c>false</c>.</returns>
 234    public bool Remove(T1 key, T2 value)
 235    {
 2236        SwiftThrowHelper.ThrowIfNullGeneric(key, nameof(key));
 2237        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 238
 2239        if (TryGetValue(key, out T2 existingValue))
 240        {
 2241            if (_reverseComparer.Equals(existingValue, value))
 242            {
 1243                bool removed = base.Remove(key);
 1244                if (removed)
 245                {
 1246                    lock (ReverseSyncRoot)
 1247                        _reverseMap.Remove(value);
 248                }
 1249                return removed;
 250            }
 251        }
 1252        return false;
 253    }
 254
 255    #region Overrides
 256
 257    /// <summary>
 258    /// Adds the specified key and value to the <see cref="SwiftBiDictionary{T1, T2}"/>.
 259    /// Also adds the value-key pair to the reverse map.
 260    /// </summary>
 261    /// <param name="key">The key of the element to add.</param>
 262    /// <param name="value">The value of the element to add.</param>
 263    /// <exception cref="ArgumentException">An element with the same key or value already exists.</exception>
 264    public override bool Add(T1 key, T2 value)
 265    {
 42266        SwiftThrowHelper.ThrowIfNullGeneric(key, nameof(key));
 42267        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 268
 42269        if (ContainsKey(key))
 1270            return false;
 271
 41272        lock (ReverseSyncRoot)
 273        {
 41274            if (_reverseMap.ContainsKey(value))
 1275                return false;
 276
 40277            bool added = base.Add(key, value);
 278
 40279            if (added)
 40280                _reverseMap.Add(value, key);
 281
 40282            return added;
 283        }
 41284    }
 285
 286    /// <summary>
 287    /// Overrides the base class's <see cref="SwiftDictionary{TKey, TValue}.InsertIfNotExist"/> method to ensure synchro
 288    /// </summary>
 289    /// <param name="key">The key to insert.</param>
 290    /// <param name="value">The value to insert.</param>
 291    /// <returns><c>true</c> if a new entry was added; otherwise, <c>false</c>.</returns>
 292    internal override bool InsertIfNotExist(T1 key, T2 value)
 293    {
 60294        lock (ReverseSyncRoot)
 295        {
 60296            if (_reverseMap.ContainsKey(value))
 1297                return false;
 298
 59299            bool result = base.InsertIfNotExist(key, value);
 300
 59301            if (result)
 59302                _reverseMap.Add(value, key);
 303
 59304            return result;
 305        }
 60306    }
 307
 308    /// <summary>
 309    /// Removes the value with the specified key from the <see cref="SwiftBiDictionary{T1, T2}"/>.
 310    /// Also removes the corresponding key from the reverse map.
 311    /// </summary>
 312    /// <param name="key">The key of the element to remove.</param>
 313    /// <returns><c>true</c> if the element is successfully found and removed; otherwise, <c>false</c>.</returns>
 314    public override bool Remove(T1 key)
 315    {
 2316        SwiftThrowHelper.ThrowIfNullGeneric(key, nameof(key));
 317
 2318        if (TryGetValue(key, out T2 value))
 319        {
 1320            bool removed = base.Remove(key);
 321
 1322            if (removed)
 323            {
 1324                lock (ReverseSyncRoot)
 1325                    _reverseMap.Remove(value);
 326            }
 327
 1328            return removed;
 329        }
 330
 1331        return false;
 332    }
 333
 334    /// <summary>
 335    /// Removes all keys and values from the <see cref="SwiftBiDictionary{T1, T2}"/>.
 336    /// Also clears the reverse map.
 337    /// </summary>
 338    public override void Clear()
 339    {
 1340        base.Clear();
 1341        lock (ReverseSyncRoot)
 1342            _reverseMap.Clear();
 1343    }
 344
 345    #endregion
 346
 347    #endregion
 348
 349    #region Utility Methods
 350
 351    /// <summary>
 352    /// Sets the comparers used to determine equality for keys and values in the dictionary.
 353    /// </summary>
 354    /// <remarks>This method updates the internal comparers and rebuilds the reverse mapping using the
 355    /// specified value comparer. The operation is thread-safe and locks the internal state during the update. Changing
 356    /// comparers may affect key and value lookup behavior.</remarks>
 357    /// <param name="comparer1">The equality comparer to use for keys of type T1. Cannot be null.</param>
 358    /// <param name="comparer2">The equality comparer to use for values of type T2. Cannot be null.</param>
 359    public void SetComparer(IEqualityComparer<T1> comparer1, IEqualityComparer<T2> comparer2)
 360    {
 3361        SwiftThrowHelper.ThrowIfNull(comparer1, nameof(comparer1));
 3362        SwiftThrowHelper.ThrowIfNull(comparer2, nameof(comparer2));
 3363        if (ReferenceEquals(comparer1, _comparer) && ReferenceEquals(comparer2, _reverseComparer))
 1364            return;
 365
 2366        lock (ReverseSyncRoot)
 367        {
 2368            var newReverseMap = new SwiftDictionary<T2, T1>(Count, comparer2);
 24369            foreach (var kv in this)
 10370                newReverseMap.Add(kv.Value, kv.Key);
 371
 2372            _reverseMap = newReverseMap;
 2373            _reverseComparer = comparer2;
 374
 2375            base.SetComparer(comparer1);
 2376        }
 2377    }
 378
 379    /// <summary>
 380    /// Attempts to get the key associated with the specified value.
 381    /// </summary>
 382    /// <param name="value">The value whose associated key is to be retrieved.</param>
 383    /// <param name="key">When this method returns, contains the key associated with the specified value, if the key is 
 384    /// <returns><c>true</c> if the key was found; otherwise, <c>false</c>.</returns>
 385    public bool TryGetKey(T2 value, out T1 key)
 386    {
 19387        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 388
 19389        lock (ReverseSyncRoot)
 19390            return _reverseMap.TryGetValue(value, out key);
 19391    }
 392
 393    /// <summary>
 394    /// Gets the key associated with the specified value.
 395    /// </summary>
 396    /// <param name="value">The value whose associated key is to be retrieved.</param>
 397    /// <returns>The key associated with the specified value.</returns>
 398    /// <exception cref="KeyNotFoundException">The property is retrieved and <paramref name="value"/> does not exist in 
 399    public T1 GetKey(T2 value)
 400    {
 2401        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 402
 2403        lock (ReverseSyncRoot)
 2404            return _reverseMap[value];
 2405    }
 406
 407    /// <summary>
 408    /// Determines whether the <see cref="SwiftBiDictionary{T1, T2}"/> contains the specified value.
 409    /// </summary>
 410    /// <param name="value">The value to locate in the dictionary.</param>
 411    /// <returns><c>true</c> if the dictionary contains an element with the specified value; otherwise, <c>false</c>.</r
 412    public bool ContainsValue(T2 value)
 413    {
 6414        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 415
 6416        lock (ReverseSyncRoot)
 6417            return _reverseMap.ContainsKey(value);
 6418    }
 419
 420    #endregion
 421}