// MIT License - Copyright (c) 2023 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.Extension { using System; using System.Collections.Concurrent; using System.Collections.Generic; /// /// Extension methods for dictionary types providing additional functionality for retrieving, adding, and manipulating dictionary entries. /// public static class DictionaryExtensions { /// /// Gets an existing value from the dictionary or adds a new value if the key doesn't exist. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The dictionary to query or modify. /// The key to look up or add. /// A function that produces a new value if the key is not found. /// The existing value if the key exists, otherwise the newly added value. /// /// Optimized for ConcurrentDictionary using thread-safe operations. /// For non-concurrent dictionaries, uses TryGetValue followed by direct assignment. /// Null handling: Throws if dictionary or valueProducer is null. /// Thread-safe: Yes, if using ConcurrentDictionary. No, for other dictionary types. /// Performance: O(1) average case for hash-based dictionaries. /// /// Thrown if dictionary or valueProducer is null. public static V GetOrAdd( this IDictionary dictionary, K key, Func valueProducer ) { if (dictionary is ConcurrentDictionary concurrentDictionary) { return concurrentDictionary.GetOrAdd( key, static (_, existing) => existing(), valueProducer ); } if (dictionary.TryGetValue(key, out V result)) { return result; } return dictionary[key] = valueProducer(); } /// /// Gets an existing value from the dictionary or adds a new value if the key doesn't exist. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The dictionary to query or modify. /// The key to look up or add. /// A function that takes the key and produces a new value if the key is not found. /// The existing value if the key exists, otherwise the newly added value. /// /// This overload allows the value producer to use the key when creating a new value. /// Optimized for ConcurrentDictionary using thread-safe operations. /// Null handling: Throws if dictionary or valueProducer is null. /// Thread-safe: Yes, if using ConcurrentDictionary. No, for other dictionary types. /// Performance: O(1) average case for hash-based dictionaries. /// /// Thrown if dictionary or valueProducer is null. public static V GetOrAdd( this IDictionary dictionary, K key, Func valueProducer ) { if (dictionary is ConcurrentDictionary concurrentDictionary) { return concurrentDictionary.GetOrAdd(key, valueProducer); } if (dictionary.TryGetValue(key, out V result)) { return result; } return dictionary[key] = valueProducer(key); } /// /// Gets an existing value from a read-only dictionary or returns a default value if the key doesn't exist. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The read-only dictionary to query. /// The key to look up. /// A function that produces a default value if the key is not found. /// The existing value if the key exists, otherwise the value produced by valueProducer. /// /// Does not modify the dictionary. /// Null handling: Throws if dictionary or valueProducer is null. /// Thread-safe: Yes, as it only reads from the dictionary. /// Performance: O(1) average case for hash-based dictionaries. /// /// Thrown if dictionary or valueProducer is null. public static V GetOrElse( this IReadOnlyDictionary dictionary, K key, Func valueProducer ) { if (dictionary.TryGetValue(key, out V value)) { return value; } return valueProducer.Invoke(); } /// /// Gets an existing value from a read-only dictionary or returns a default value if the key doesn't exist. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The read-only dictionary to query. /// The key to look up. /// A function that takes the key and produces a default value if the key is not found. /// The existing value if the key exists, otherwise the value produced by valueProducer. /// /// This overload allows the value producer to use the key when creating a default value. /// Does not modify the dictionary. /// Null handling: Throws if dictionary or valueProducer is null. /// Thread-safe: Yes, as it only reads from the dictionary. /// Performance: O(1) average case for hash-based dictionaries. /// /// Thrown if dictionary or valueProducer is null. public static V GetOrElse( this IReadOnlyDictionary dictionary, K key, Func valueProducer ) { if (dictionary.TryGetValue(key, out V value)) { return value; } return valueProducer.Invoke(key); } /// /// Gets an existing value from the dictionary or adds a new instance if the key doesn't exist. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. Must have a parameterless constructor. /// The dictionary to query or modify. /// The key to look up or add. /// The existing value if the key exists, otherwise a newly created instance of V. /// /// Requires that V has a parameterless constructor. /// Optimized for ConcurrentDictionary using thread-safe operations. /// Null handling: Throws if dictionary is null. /// Thread-safe: Yes, if using ConcurrentDictionary. No, for other dictionary types. /// Performance: O(1) average case for hash-based dictionaries. Adds object allocation overhead for new instances. /// /// Thrown if dictionary is null. public static V GetOrAdd(this IDictionary dictionary, K key) where V : new() { if (dictionary is ConcurrentDictionary concurrentDictionary) { return concurrentDictionary.AddOrUpdate( key, _ => new V(), (_, existing) => existing ); } if (dictionary.TryGetValue(key, out V result)) { return result; } return dictionary[key] = new V(); } /// /// Gets an existing value from a read-only dictionary or returns the specified default value if the key doesn't exist. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The read-only dictionary to query. /// The key to look up. /// The default value to return if the key is not found. /// The existing value if the key exists, otherwise the specified default value. /// /// Does not modify the dictionary. /// Null handling: Returns the default value if dictionary is null or key doesn't exist. /// Thread-safe: Yes, as it only reads from the dictionary. /// Performance: O(1) average case for hash-based dictionaries. /// public static V GetOrElse(this IReadOnlyDictionary dictionary, K key, V value) { return dictionary.GetValueOrDefault(key, value); } /// /// Adds a new key-value pair or updates an existing value in the dictionary. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The dictionary to modify. /// The key to add or update. /// A function that creates a value if the key doesn't exist. /// A function that updates the value if the key exists. /// The final value that was added or updated. /// /// Optimized for ConcurrentDictionary using thread-safe AddOrUpdate. /// For non-concurrent dictionaries, uses TryGetValue followed by direct assignment. /// Null handling: Throws if dictionary, creator, or updater is null. /// Thread-safe: Yes, if using ConcurrentDictionary. No, for other dictionary types. /// Performance: O(1) average case for hash-based dictionaries. /// /// Thrown if dictionary, creator, or updater is null. public static V AddOrUpdate( this IDictionary dictionary, K key, Func creator, Func updater ) { if (dictionary is ConcurrentDictionary concurrentDictionary) { return concurrentDictionary.AddOrUpdate(key, creator, updater); } V latest = dictionary.TryGetValue(key, out V value) ? updater(key, value) : creator(key); dictionary[key] = latest; return latest; } /// /// Tries to add a new key-value pair to the dictionary, or returns the existing value if the key already exists. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The dictionary to modify. /// The key to add. /// A function that creates a value if the key doesn't exist. /// The existing value if the key exists, otherwise the newly created value. /// /// Unlike GetOrAdd, this always returns the value that was in the dictionary after the operation. /// Optimized for ConcurrentDictionary using thread-safe AddOrUpdate. /// Null handling: Throws if dictionary or creator is null. /// Thread-safe: Yes, if using ConcurrentDictionary. No, for other dictionary types. /// Performance: O(1) average case for hash-based dictionaries. /// /// Thrown if dictionary or creator is null. public static V TryAdd(this IDictionary dictionary, K key, Func creator) { if (dictionary is ConcurrentDictionary concurrentDictionary) { return concurrentDictionary.AddOrUpdate( key, creator, (_, existingValue) => existingValue ); } if (dictionary.TryGetValue(key, out V existing)) { return existing; } V value = creator(key); dictionary[key] = value; return value; } /// /// Merges two read-only dictionaries into a new dictionary, with values from the right-hand side overwriting values from the left-hand side for duplicate keys. /// /// The type of keys in the dictionaries. /// The type of values in the dictionaries. /// The left-hand side dictionary (lower priority). /// The right-hand side dictionary (higher priority, overwrites lhs). /// Optional function to create the result dictionary. If null, a new Dictionary is created. /// A new dictionary containing all entries from both dictionaries, with rhs values taking precedence. /// /// Values from rhs overwrite values from lhs for any duplicate keys. /// Does not modify the input dictionaries. /// Null handling: Handles null or empty dictionaries gracefully. /// Thread-safe: No. The returned dictionary is not thread-safe unless created with a concurrent type. /// Performance: O(n+m) where n and m are the sizes of the input dictionaries. /// Allocations: Creates a new dictionary. Use the creator parameter for custom capacity or implementation. /// public static Dictionary Merge( this IReadOnlyDictionary lhs, IReadOnlyDictionary rhs, Func> creator = null ) { Dictionary result = creator?.Invoke() ?? new Dictionary(); if (0 < lhs.Count) { foreach (KeyValuePair kvp in lhs) { result[kvp.Key] = kvp.Value; } } if (0 < rhs.Count) { foreach (KeyValuePair kvp in rhs) { result[kvp.Key] = kvp.Value; } } return result; } /// /// Attempts to remove a key-value pair from the dictionary and returns the removed value. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The dictionary to modify. /// The key to remove. /// The value that was removed, or default if the key wasn't found. /// True if the key was found and removed, false otherwise. /// /// Optimized for ConcurrentDictionary using thread-safe TryRemove. /// Null handling: Returns false if dictionary is null. /// Thread-safe: Yes, if using ConcurrentDictionary. No, for other dictionary types. /// Performance: O(1) average case for hash-based dictionaries. /// public static bool TryRemove(this IDictionary dictionary, K key, out V value) { if (dictionary is ConcurrentDictionary concurrentDictionary) { return concurrentDictionary.TryRemove(key, out value); } return dictionary.Remove(key, out value); } /// /// Computes the difference between two dictionaries, returning entries from rhs that differ from or don't exist in lhs. /// /// The type of keys in the dictionaries. /// The type of values in the dictionaries. /// The basis dictionary for comparison. /// The changed dictionary to compare against. /// Optional function to create the result dictionary. If null, a new Dictionary is created with capacity of rhs.Count. /// A dictionary containing all entries from rhs that either don't exist in lhs or have different values. /// /// Uses Equals to compare values. /// Does not modify the input dictionaries. /// Null handling: Handles null or empty dictionaries gracefully. /// Thread-safe: No. /// Performance: O(m) where m is the size of rhs. /// Allocations: Creates a new dictionary. Use the creator parameter for custom capacity. /// public static Dictionary Difference( this IReadOnlyDictionary lhs, IReadOnlyDictionary rhs, Func> creator = null ) { Dictionary result = creator?.Invoke() ?? new Dictionary(rhs.Count); foreach (KeyValuePair kvp in rhs) { K key = kvp.Key; if (lhs.TryGetValue(key, out V existing) && Equals(existing, kvp.Value)) { continue; } result[key] = kvp.Value; } return result; } /// /// Creates a reversed dictionary where values become keys and keys become values. /// /// The type of keys in the input dictionary. /// The type of values in the input dictionary. /// The dictionary to reverse. /// Optional function to create the result dictionary. If null, a new Dictionary is created with capacity of dictionary.Count. /// A new dictionary where values from the input become keys and keys become values. /// /// If multiple keys map to the same value in the input, only the last encountered key will be retained. /// Does not modify the input dictionary. /// Null handling: Handles null or empty dictionaries gracefully. /// Thread-safe: No. /// Performance: O(n) where n is the size of the input dictionary. /// Allocations: Creates a new dictionary. Duplicate values will cause overwriting. /// public static Dictionary Reverse( this IReadOnlyDictionary dictionary, Func> creator = null ) { Dictionary output = creator?.Invoke() ?? new Dictionary(dictionary.Count); foreach (KeyValuePair entry in dictionary) { output[entry.Value] = entry.Key; } return output; } /// /// Converts a read-only dictionary to a mutable Dictionary. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The read-only dictionary to convert. /// A new mutable Dictionary containing all entries from the input. /// /// Creates a new Dictionary instance; modifications won't affect the original. /// Null handling: Throws ArgumentNullException if dictionary is null. /// Thread-safe: No. /// Performance: O(n) where n is the size of the dictionary. /// Allocations: Creates a new dictionary with the same size as the input. /// /// Thrown if dictionary is null. public static Dictionary ToDictionary(this IReadOnlyDictionary dictionary) { return new Dictionary(dictionary); } /// /// Converts a read-only dictionary to a mutable Dictionary with a custom equality comparer. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// The read-only dictionary to convert. /// The equality comparer to use for keys. /// A new mutable Dictionary containing all entries from the input, using the specified comparer. /// /// Creates a new Dictionary instance; modifications won't affect the original. /// Null handling: Throws ArgumentNullException if dictionary is null. /// Thread-safe: No. /// Performance: O(n) where n is the size of the dictionary. /// Allocations: Creates a new dictionary with the same size as the input. /// /// Thrown if dictionary is null. public static Dictionary ToDictionary( this IReadOnlyDictionary dictionary, IEqualityComparer comparer ) { return new Dictionary(dictionary, comparer); } /// /// Converts an enumerable of key-value pairs to a Dictionary. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// An enumerable of key-value pairs. /// A new Dictionary containing all entries from the input enumerable. /// /// Null handling: Throws ArgumentNullException if prettyMuchADictionary is null or contains null keys. /// Thread-safe: No. /// Performance: O(n) where n is the number of key-value pairs. /// Allocations: Creates a new dictionary. /// /// Thrown if prettyMuchADictionary is null or contains null keys. public static Dictionary ToDictionary( this IEnumerable> prettyMuchADictionary ) { Dictionary result = new(); foreach (KeyValuePair kvp in prettyMuchADictionary) { result[kvp.Key] = kvp.Value; } return result; } /// /// Converts an enumerable of key-value pairs to a Dictionary with a custom equality comparer. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// An enumerable of key-value pairs. /// The equality comparer to use for keys. /// A new Dictionary containing all entries from the input enumerable, using the specified comparer. /// /// Null handling: Throws ArgumentNullException if prettyMuchADictionary is null or contains null keys. /// Thread-safe: No. /// Performance: O(n) where n is the number of key-value pairs. /// Allocations: Creates a new dictionary. /// /// Thrown if prettyMuchADictionary is null or contains null keys. public static Dictionary ToDictionary( this IEnumerable> prettyMuchADictionary, IEqualityComparer comparer ) { Dictionary result = new(comparer); foreach (KeyValuePair kvp in prettyMuchADictionary) { result[kvp.Key] = kvp.Value; } return result; } /// /// Converts an enumerable of tuples to a Dictionary. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// An enumerable of (key, value) tuples. /// A new Dictionary containing all entries from the input enumerable. /// /// Null handling: Throws ArgumentNullException if prettyMuchADictionary is null or contains null keys. /// Thread-safe: No. /// Performance: O(n) where n is the number of tuples. /// Allocations: Creates a new dictionary. /// /// Thrown if prettyMuchADictionary is null or contains null keys. public static Dictionary ToDictionary( this IEnumerable<(K, V)> prettyMuchADictionary ) { Dictionary result = new(); foreach ((K key, V value) in prettyMuchADictionary) { result[key] = value; } return result; } /// /// Converts an enumerable of tuples to a Dictionary with a custom equality comparer. /// /// The type of keys in the dictionary. /// The type of values in the dictionary. /// An enumerable of (key, value) tuples. /// The equality comparer to use for keys. /// A new Dictionary containing all entries from the input enumerable, using the specified comparer. /// /// Null handling: Throws ArgumentNullException if prettyMuchADictionary is null or contains null keys. /// Thread-safe: No. /// Performance: O(n) where n is the number of tuples. /// Allocations: Creates a new dictionary. /// /// Thrown if prettyMuchADictionary is null or contains null keys. public static Dictionary ToDictionary( this IEnumerable<(K, V)> prettyMuchADictionary, IEqualityComparer comparer ) { Dictionary result = new(comparer); foreach ((K key, V value) in prettyMuchADictionary) { result[key] = value; } return result; } /// /// Compares two read-only dictionaries for content equality using IEquatable comparison for values. /// /// The type of keys in the dictionaries. /// The type of values in the dictionaries, must implement IEquatable. /// The first dictionary to compare. /// The second dictionary to compare. /// True if both dictionaries have the same keys with equal values, false otherwise. /// /// Returns true if both dictionaries are the same reference or both are null. /// Returns false if only one is null or if they have different counts. /// Returns true for empty dictionaries with matching counts. /// Null handling: Handles null dictionaries gracefully, returning true only if both are null. /// Thread-safe: Yes, as it only reads from the dictionaries. /// Performance: O(n) where n is the size of the dictionaries. /// public static bool ContentEquals( this IReadOnlyDictionary dictionary, IReadOnlyDictionary other ) where V : IEquatable { if (ReferenceEquals(dictionary, other)) { return true; } if (ReferenceEquals(dictionary, null) || ReferenceEquals(other, null)) { return false; } if (dictionary.Count != other.Count) { return false; } if (dictionary.Count == 0) { return true; } foreach (KeyValuePair entry in dictionary) { if (!other.TryGetValue(entry.Key, out V value) || !entry.Value.Equals(value)) { return false; } } return true; } } }