// 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;
}
}
}