< 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: 414
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 Chronicler.Diagnostics;
 13using Chronicler.Serialization;
 14using MemoryPack;
 15using SwiftCollections.Utility;
 16
 17namespace SwiftCollections;
 18
 19/// <summary>
 20/// Represents a bidirectional dictionary that allows for efficient lookups in both directions,
 21/// mapping keys to values and values back to keys. Both keys and values must be unique to maintain
 22/// the integrity of the bidirectional relationship.
 23/// Inherits from <see cref="SwiftDictionary{TKey, TValue}"/> and maintains a reverse map for reverse lookups.
 24/// </summary>
 25/// <typeparam name="T1">The type of the keys in the forward dictionary.</typeparam>
 26/// <typeparam name="T2">The type of the values in the forward dictionary.</typeparam>
 27/// <remarks>
 28/// The comparer is not serialized. After deserialization the dictionary reverts
 29/// to the same default comparer selection used by a new instance for both the
 30/// forward and reverse maps. String keys use SwiftCollections' deterministic
 31/// default comparer. Object keys use a SwiftCollections comparer that hashes
 32/// strings deterministically, while other object-key determinism still depends
 33/// on the underlying key type's <see cref="object.GetHashCode()"/>
 34/// implementation. Other key types use <see cref="EqualityComparer{T1}.Default"/>
 35/// and <see cref="EqualityComparer{T2}.Default"/>.
 36///
 37/// If a custom comparer is required it can be reapplied using
 38/// <see cref="SetComparer(IEqualityComparer{T1}, IEqualityComparer{T2})"/>.
 39/// </remarks>
 40[Serializable]
 41[JsonConverter(typeof(StateJsonConverterFactory))]
 42[MemoryPackable]
 43public partial class SwiftBiDictionary<T1, T2> : SwiftDictionary<T1, T2>, IStateBacked<SwiftDictionaryState<T1, T2>>
 44    where T1 : notnull
 45    where T2 : notnull
 46{
 47    #region Fields
 48
 49    /// <summary>
 50    /// The reverse map for bidirectional lookup, mapping from <typeparamref name="T2"/> to <typeparamref name="T1"/>.
 51    /// </summary>
 52    private SwiftDictionary<T2, T1> _reverseMap;
 53
 54    /// <summary>
 55    /// The comparer used to determine equality of keys and to generate hash codes.
 56    /// </summary>
 57    [NonSerialized]
 58    private IEqualityComparer<T2> _reverseComparer;
 59
 60    /// <summary>
 61    /// An object used to synchronize access to the reverse map during serialization and deserialization.
 62    /// </summary>
 63    [NonSerialized]
 64    private object? _reverseSyncRoot;
 65
 66    #endregion
 67
 68    #region Constructors
 69
 70    /// <summary>
 71    /// Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class that is empty and uses the defau
 72    /// </summary>
 7473    public SwiftBiDictionary() : this(null, null) { }
 74
 75    /// <summary>
 76    /// Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class that is empty and uses the speci
 77    /// </summary>
 78    /// <param name="comparer1">The comparer to use for the keys.</param>
 79    /// <param name="comparer2">The comparer to use for the values.</param>
 4280    public SwiftBiDictionary(IEqualityComparer<T1>? comparer1, IEqualityComparer<T2>? comparer2) : base(DefaultCapacity,
 81    {
 4282        _reverseComparer = SwiftHashTools.GetDefaultEqualityComparer(comparer2);
 4283        _reverseMap = new SwiftDictionary<T2, T1>(DefaultCapacity, _reverseComparer);
 4284    }
 85
 86    /// <summary>
 87    /// Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class that contains elements copied fr
 88    /// </summary>
 89    /// <param name="dictionary">The dictionary whose elements are copied to the new <see cref="SwiftBiDictionary{T1, T2
 290    public SwiftBiDictionary(IDictionary<T1, T2> dictionary) : this(dictionary, null, null) { }
 91
 92    /// <summary>
 93    /// Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class that contains elements copied fr
 94    /// </summary>
 95    /// <param name="dictionary">The dictionary whose elements are copied to the new <see cref="SwiftBiDictionary{T1, T2
 96    /// <param name="comparer1">The comparer to use for the keys.</param>
 97    /// <param name="comparer2">The comparer to use for the values.</param>
 98    public SwiftBiDictionary(IDictionary<T1, T2> dictionary, IEqualityComparer<T1>? comparer1, IEqualityComparer<T2>? co
 299        : this(comparer1, comparer2)
 100    {
 2101        ChronicleGuard.ThrowIfNull(dictionary, nameof(dictionary));
 102
 12103        foreach (var kvp in dictionary)
 4104            Add(kvp.Key, kvp.Value);
 2105    }
 106
 107    ///  <summary>
 108    ///  Initializes a new instance of the <see cref="SwiftBiDictionary{T1, T2}"/> class with the specified <see cref="S
 109    ///  </summary>
 110    ///  <param name="state">The state containing the internal array, count, offset, and version for initialization.</pa
 111    [MemoryPackConstructor]
 6112    public SwiftBiDictionary(SwiftDictionaryState<T1, T2> state)
 113    {
 6114        State = state;
 115
 6116        ChronicleGuard.ThrowIfNull(_reverseMap, nameof(_reverseMap));
 6117        ChronicleGuard.ThrowIfNull(_reverseComparer, nameof(_reverseComparer));
 6118    }
 119
 120    #endregion
 121
 122    #region Properties
 123
 124    /// <summary>
 125    /// Gets an object that can be used to synchronize access to the reverse collection.
 126    /// </summary>
 127    /// <remarks>
 128    /// Use this object to lock the reverse collection during multithreaded operations to ensure thread safety.
 129    /// This property is intended for advanced scenarios where manual synchronization is required.
 130    /// </remarks>
 131    [JsonIgnore]
 132    [MemoryPackIgnore]
 215133    public object ReverseSyncRoot => _reverseSyncRoot ??= new object();
 134
 135    /// <summary>
 136    /// Gets or sets the value associated with the specified key. Ensures that each value is unique within the collectio
 137    /// </summary>
 138    /// <remarks>
 139    /// Setting a value that already exists in the collection will result in an exception, as each value must be unique.
 140    /// If the key does not exist, a new key-value pair is added.
 141    /// If the key exists and the value is unchanged, no operation is performed.
 142    /// </remarks>
 143    /// <param name="key">The key whose value to get or set. Cannot be null.</param>
 144    /// <returns>The value associated with the specified key.</returns>
 145    /// <exception cref="ArgumentException">Thrown when the specified value already exists in the collection.</exception
 146    [JsonIgnore]
 147    [MemoryPackIgnore]
 148    public new T2 this[T1 key]
 149    {
 13150        get => base[key];
 151        set
 152        {
 6153            ChronicleGuard.ThrowIfNullGeneric(key, nameof(key));
 154
 6155            lock (ReverseSyncRoot)
 156            {
 6157                if (TryGetValue(key, out T2 oldValue))
 158                {
 159                    // No change
 4160                    if (_reverseComparer.Equals(oldValue, value))
 1161                        return;
 162
 163                    // Prevent duplicate values
 3164                    ChronicleGuard.ThrowIfArgument(_reverseMap.ContainsKey(value), nameof(value), "Value already exists"
 165
 166                    // Remove old reverse mapping
 2167                    _reverseMap.Remove(oldValue);
 168
 169                    // Update forward value
 2170                    int index = FindEntry(key);
 2171                    _entries[index].Value = value;
 172
 173                    // Insert new reverse mapping
 2174                    _reverseMap.Add(value, key);
 175
 2176                    _version++;
 177                }
 178                else
 179                {
 180                    // New insert
 2181                    ChronicleGuard.ThrowIfArgument(_reverseMap.ContainsKey(value), nameof(value), "Value already exists"
 182
 2183                    CheckLoadThreshold();
 184
 2185                    bool added = InsertIfNotExist(key, value);
 186
 2187                    if (added)
 2188                        _reverseMap.Add(value, key);
 189                }
 2190            }
 5191        }
 192    }
 193
 194    /// <summary>
 195    /// Gets or sets the current state of the dictionary, including its items and configuration.
 196    /// </summary>
 197    /// <remarks>
 198    /// Setting this property updates the internal reverse mapping and comparer to reflect the new state.
 199    /// The reverse mapping is rebuilt to ensure consistency with the updated state.
 200    /// This property is intended for serialization and deserialization scenarios.
 201    /// </remarks>
 202    [JsonInclude]
 203    [MemoryPackInclude]
 204    public new SwiftDictionaryState<T1, T2> State
 205    {
 5206        get => base.State;
 207        internal set
 208        {
 6209            _reverseComparer = SwiftHashTools.GetDefaultEqualityComparer<T2>();
 6210            _reverseMap = new SwiftDictionary<T2, T1>(value.Items.Length, _reverseComparer);
 211
 6212            base.State = value;
 213
 6214            lock (ReverseSyncRoot)
 215            {
 6216                _reverseMap.Clear();
 217
 46218                foreach (var kv in this)
 17219                    _reverseMap.Add(kv.Value, kv.Key);
 220            }
 6221        }
 222    }
 223
 224    #endregion
 225
 226    #region Collection Manipulation
 227
 228    /// <summary>
 229    /// Removes the key-value pair from the <see cref="SwiftBiDictionary{T1, T2}"/>.
 230    /// Also removes the corresponding value-key pair from the reverse map.
 231    /// </summary>
 232    /// <param name="key">The key of the element to remove.</param>
 233    /// <param name="value">The value of the element to remove.</param>
 234    /// <returns><c>true</c> if the element is successfully found and removed; otherwise, <c>false</c>.</returns>
 235    public bool Remove(T1 key, T2 value)
 236    {
 3237        ChronicleGuard.ThrowIfNullGeneric(key, nameof(key));
 3238        ChronicleGuard.ThrowIfNullGeneric(value, nameof(value));
 239
 3240        if (TryGetValue(key, out T2 existingValue))
 241        {
 2242            if (_reverseComparer.Equals(existingValue, value))
 243            {
 1244                base.Remove(key);
 1245                lock (ReverseSyncRoot)
 1246                    _reverseMap.Remove(value);
 1247                return true;
 248            }
 249        }
 2250        return false;
 251    }
 252
 253    #region Overrides
 254
 255    /// <summary>
 256    /// Adds the specified key and value to the <see cref="SwiftBiDictionary{T1, T2}"/>.
 257    /// Also adds the value-key pair to the reverse map.
 258    /// </summary>
 259    /// <param name="key">The key of the element to add.</param>
 260    /// <param name="value">The value of the element to add.</param>
 261    /// <exception cref="ArgumentException">An element with the same key or value already exists.</exception>
 262    public override bool Add(T1 key, T2 value)
 263    {
 73264        ChronicleGuard.ThrowIfNullGeneric(key, nameof(key));
 73265        ChronicleGuard.ThrowIfNullGeneric(value, nameof(value));
 266
 73267        if (ContainsKey(key))
 1268            return false;
 269
 72270        lock (ReverseSyncRoot)
 271        {
 72272            if (_reverseMap.ContainsKey(value))
 1273                return false;
 274
 71275            bool added = base.Add(key, value);
 276
 71277            if (added)
 71278                _reverseMap.Add(value, key);
 279
 71280            return added;
 281        }
 72282    }
 283
 284    /// <summary>
 285    /// Overrides the base class's <see cref="SwiftDictionary{TKey, TValue}.InsertIfNotExist"/> method to ensure synchro
 286    /// </summary>
 287    /// <param name="key">The key to insert.</param>
 288    /// <param name="value">The value to insert.</param>
 289    /// <returns><c>true</c> if a new entry was added; otherwise, <c>false</c>.</returns>
 290    internal override bool InsertIfNotExist(T1 key, T2 value)
 291    {
 91292        lock (ReverseSyncRoot)
 293        {
 91294            if (_reverseMap.ContainsKey(value))
 1295                return false;
 296
 90297            bool result = base.InsertIfNotExist(key, value);
 298
 90299            if (result)
 90300                _reverseMap.Add(value, key);
 301
 90302            return result;
 303        }
 91304    }
 305
 306    /// <summary>
 307    /// Removes the value with the specified key from the <see cref="SwiftBiDictionary{T1, T2}"/>.
 308    /// Also removes the corresponding key from the reverse map.
 309    /// </summary>
 310    /// <param name="key">The key of the element to remove.</param>
 311    /// <returns><c>true</c> if the element is successfully found and removed; otherwise, <c>false</c>.</returns>
 312    public override bool Remove(T1 key)
 313    {
 2314        ChronicleGuard.ThrowIfNullGeneric(key, nameof(key));
 315
 2316        if (TryGetValue(key, out T2 value))
 317        {
 1318            base.Remove(key);
 1319            lock (ReverseSyncRoot)
 1320                _reverseMap.Remove(value);
 1321            return true;
 322        }
 323
 1324        return false;
 325    }
 326
 327    /// <summary>
 328    /// Removes all keys and values from the <see cref="SwiftBiDictionary{T1, T2}"/>.
 329    /// Also clears the reverse map.
 330    /// </summary>
 331    public override void Clear()
 332    {
 5333        base.Clear();
 5334        lock (ReverseSyncRoot)
 5335            _reverseMap.Clear();
 5336    }
 337
 338    #endregion
 339
 340    #endregion
 341
 342    #region Utility Methods
 343
 344    /// <summary>
 345    /// Sets the comparers used to determine equality for keys and values in the dictionary.
 346    /// </summary>
 347    /// <remarks>This method updates the internal comparers and rebuilds the reverse mapping using the
 348    /// specified value comparer. The operation is thread-safe and locks the internal state during the update. Changing
 349    /// comparers may affect key and value lookup behavior.</remarks>
 350    /// <param name="comparer1">The equality comparer to use for keys of type T1. Cannot be null.</param>
 351    /// <param name="comparer2">The equality comparer to use for values of type T2. Cannot be null.</param>
 352    public void SetComparer(IEqualityComparer<T1> comparer1, IEqualityComparer<T2> comparer2)
 353    {
 3354        ChronicleGuard.ThrowIfNull(comparer1, nameof(comparer1));
 3355        ChronicleGuard.ThrowIfNull(comparer2, nameof(comparer2));
 3356        if (ReferenceEquals(comparer1, _comparer) && ReferenceEquals(comparer2, _reverseComparer))
 1357            return;
 358
 2359        lock (ReverseSyncRoot)
 360        {
 2361            var newReverseMap = new SwiftDictionary<T2, T1>(Count, comparer2);
 24362            foreach (var kv in this)
 10363                newReverseMap.Add(kv.Value, kv.Key);
 364
 2365            _reverseMap = newReverseMap;
 2366            _reverseComparer = comparer2;
 367
 2368            base.SetComparer(comparer1);
 2369        }
 2370    }
 371
 372    /// <summary>
 373    /// Attempts to get the key associated with the specified value.
 374    /// </summary>
 375    /// <param name="value">The value whose associated key is to be retrieved.</param>
 376    /// <param name="key">When this method returns, contains the key associated with the specified value, if the key is 
 377    /// <returns><c>true</c> if the key was found; otherwise, <c>false</c>.</returns>
 378    public bool TryGetKey(T2 value, out T1 key)
 379    {
 21380        ChronicleGuard.ThrowIfNullGeneric(value, nameof(value));
 381
 21382        lock (ReverseSyncRoot)
 21383            return _reverseMap.TryGetValue(value, out key);
 21384    }
 385
 386    /// <summary>
 387    /// Gets the key associated with the specified value.
 388    /// </summary>
 389    /// <param name="value">The value whose associated key is to be retrieved.</param>
 390    /// <returns>The key associated with the specified value.</returns>
 391    /// <exception cref="KeyNotFoundException">The property is retrieved and <paramref name="value"/> does not exist in 
 392    public T1 GetKey(T2 value)
 393    {
 4394        ChronicleGuard.ThrowIfNullGeneric(value, nameof(value));
 395
 4396        lock (ReverseSyncRoot)
 4397            return _reverseMap[value];
 4398    }
 399
 400    /// <summary>
 401    /// Determines whether the <see cref="SwiftBiDictionary{T1, T2}"/> contains the specified value.
 402    /// </summary>
 403    /// <param name="value">The value to locate in the dictionary.</param>
 404    /// <returns><c>true</c> if the dictionary contains an element with the specified value; otherwise, <c>false</c>.</r
 405    public bool ContainsValue(T2 value)
 406    {
 6407        ChronicleGuard.ThrowIfNullGeneric(value, nameof(value));
 408
 6409        lock (ReverseSyncRoot)
 6410            return _reverseMap.ContainsKey(value);
 6411    }
 412
 413    #endregion
 414}