< 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: 110
Uncovered lines: 0
Coverable lines: 110
Total lines: 413
Line coverage: 100%
Branch coverage
100%
Covered branches: 34
Total branches: 34
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%44100%
Add(...)100%66100%
InsertIfNotExist(...)100%44100%
Remove(...)100%22100%
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 System;
 9using System.Collections.Generic;
 10using System.Text.Json.Serialization;
 11using Chronicler;
 12using MemoryPack;
 13using SwiftCollections.Diagnostics;
 14using SwiftCollections.Utility;
 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    {
 3236        SwiftThrowHelper.ThrowIfNullGeneric(key, nameof(key));
 3237        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 238
 3239        if (TryGetValue(key, out T2 existingValue))
 240        {
 2241            if (_reverseComparer.Equals(existingValue, value))
 242            {
 1243                base.Remove(key);
 1244                lock (ReverseSyncRoot)
 1245                    _reverseMap.Remove(value);
 1246                return true;
 247            }
 248        }
 2249        return false;
 250    }
 251
 252    #region Overrides
 253
 254    /// <summary>
 255    /// Adds the specified key and value to the <see cref="SwiftBiDictionary{T1, T2}"/>.
 256    /// Also adds the value-key pair to the reverse map.
 257    /// </summary>
 258    /// <param name="key">The key of the element to add.</param>
 259    /// <param name="value">The value of the element to add.</param>
 260    /// <exception cref="ArgumentException">An element with the same key or value already exists.</exception>
 261    public override bool Add(T1 key, T2 value)
 262    {
 42263        SwiftThrowHelper.ThrowIfNullGeneric(key, nameof(key));
 42264        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 265
 42266        if (ContainsKey(key))
 1267            return false;
 268
 41269        lock (ReverseSyncRoot)
 270        {
 41271            if (_reverseMap.ContainsKey(value))
 1272                return false;
 273
 40274            bool added = base.Add(key, value);
 275
 40276            if (added)
 40277                _reverseMap.Add(value, key);
 278
 40279            return added;
 280        }
 41281    }
 282
 283    /// <summary>
 284    /// Overrides the base class's <see cref="SwiftDictionary{TKey, TValue}.InsertIfNotExist"/> method to ensure synchro
 285    /// </summary>
 286    /// <param name="key">The key to insert.</param>
 287    /// <param name="value">The value to insert.</param>
 288    /// <returns><c>true</c> if a new entry was added; otherwise, <c>false</c>.</returns>
 289    internal override bool InsertIfNotExist(T1 key, T2 value)
 290    {
 60291        lock (ReverseSyncRoot)
 292        {
 60293            if (_reverseMap.ContainsKey(value))
 1294                return false;
 295
 59296            bool result = base.InsertIfNotExist(key, value);
 297
 59298            if (result)
 59299                _reverseMap.Add(value, key);
 300
 59301            return result;
 302        }
 60303    }
 304
 305    /// <summary>
 306    /// Removes the value with the specified key from the <see cref="SwiftBiDictionary{T1, T2}"/>.
 307    /// Also removes the corresponding key from the reverse map.
 308    /// </summary>
 309    /// <param name="key">The key of the element to remove.</param>
 310    /// <returns><c>true</c> if the element is successfully found and removed; otherwise, <c>false</c>.</returns>
 311    public override bool Remove(T1 key)
 312    {
 2313        SwiftThrowHelper.ThrowIfNullGeneric(key, nameof(key));
 314
 2315        if (TryGetValue(key, out T2 value))
 316        {
 1317            base.Remove(key);
 1318            lock (ReverseSyncRoot)
 1319                _reverseMap.Remove(value);
 1320            return true;
 321        }
 322
 1323        return false;
 324    }
 325
 326    /// <summary>
 327    /// Removes all keys and values from the <see cref="SwiftBiDictionary{T1, T2}"/>.
 328    /// Also clears the reverse map.
 329    /// </summary>
 330    public override void Clear()
 331    {
 1332        base.Clear();
 1333        lock (ReverseSyncRoot)
 1334            _reverseMap.Clear();
 1335    }
 336
 337    #endregion
 338
 339    #endregion
 340
 341    #region Utility Methods
 342
 343    /// <summary>
 344    /// Sets the comparers used to determine equality for keys and values in the dictionary.
 345    /// </summary>
 346    /// <remarks>This method updates the internal comparers and rebuilds the reverse mapping using the
 347    /// specified value comparer. The operation is thread-safe and locks the internal state during the update. Changing
 348    /// comparers may affect key and value lookup behavior.</remarks>
 349    /// <param name="comparer1">The equality comparer to use for keys of type T1. Cannot be null.</param>
 350    /// <param name="comparer2">The equality comparer to use for values of type T2. Cannot be null.</param>
 351    public void SetComparer(IEqualityComparer<T1> comparer1, IEqualityComparer<T2> comparer2)
 352    {
 3353        SwiftThrowHelper.ThrowIfNull(comparer1, nameof(comparer1));
 3354        SwiftThrowHelper.ThrowIfNull(comparer2, nameof(comparer2));
 3355        if (ReferenceEquals(comparer1, _comparer) && ReferenceEquals(comparer2, _reverseComparer))
 1356            return;
 357
 2358        lock (ReverseSyncRoot)
 359        {
 2360            var newReverseMap = new SwiftDictionary<T2, T1>(Count, comparer2);
 24361            foreach (var kv in this)
 10362                newReverseMap.Add(kv.Value, kv.Key);
 363
 2364            _reverseMap = newReverseMap;
 2365            _reverseComparer = comparer2;
 366
 2367            base.SetComparer(comparer1);
 2368        }
 2369    }
 370
 371    /// <summary>
 372    /// Attempts to get the key associated with the specified value.
 373    /// </summary>
 374    /// <param name="value">The value whose associated key is to be retrieved.</param>
 375    /// <param name="key">When this method returns, contains the key associated with the specified value, if the key is 
 376    /// <returns><c>true</c> if the key was found; otherwise, <c>false</c>.</returns>
 377    public bool TryGetKey(T2 value, out T1 key)
 378    {
 19379        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 380
 19381        lock (ReverseSyncRoot)
 19382            return _reverseMap.TryGetValue(value, out key);
 19383    }
 384
 385    /// <summary>
 386    /// Gets the key associated with the specified value.
 387    /// </summary>
 388    /// <param name="value">The value whose associated key is to be retrieved.</param>
 389    /// <returns>The key associated with the specified value.</returns>
 390    /// <exception cref="KeyNotFoundException">The property is retrieved and <paramref name="value"/> does not exist in 
 391    public T1 GetKey(T2 value)
 392    {
 2393        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 394
 2395        lock (ReverseSyncRoot)
 2396            return _reverseMap[value];
 2397    }
 398
 399    /// <summary>
 400    /// Determines whether the <see cref="SwiftBiDictionary{T1, T2}"/> contains the specified value.
 401    /// </summary>
 402    /// <param name="value">The value to locate in the dictionary.</param>
 403    /// <returns><c>true</c> if the dictionary contains an element with the specified value; otherwise, <c>false</c>.</r
 404    public bool ContainsValue(T2 value)
 405    {
 6406        SwiftThrowHelper.ThrowIfNullGeneric(value, nameof(value));
 407
 6408        lock (ReverseSyncRoot)
 6409            return _reverseMap.ContainsKey(value);
 6410    }
 411
 412    #endregion
 413}