From 58fb690101af173a799bf10bd67a78a12a38d9db Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Tue, 8 Oct 2024 08:41:35 +0200 Subject: [PATCH 1/2] add new primitive types --- .../Collections/ValueListDictionary.cs | 468 ++++++++++++++++++ PG.Commons/PG.Commons/Numerics/Vector2Int.cs | 73 +++ PG.Commons/PG.Commons/Numerics/Vector3Int.cs | 82 +++ PG.Commons/PG.Commons/Numerics/Vector4Int.cs | 91 ++++ 4 files changed, 714 insertions(+) create mode 100644 PG.Commons/PG.Commons/Collections/ValueListDictionary.cs create mode 100644 PG.Commons/PG.Commons/Numerics/Vector2Int.cs create mode 100644 PG.Commons/PG.Commons/Numerics/Vector3Int.cs create mode 100644 PG.Commons/PG.Commons/Numerics/Vector4Int.cs diff --git a/PG.Commons/PG.Commons/Collections/ValueListDictionary.cs b/PG.Commons/PG.Commons/Collections/ValueListDictionary.cs new file mode 100644 index 00000000..f09a535b --- /dev/null +++ b/PG.Commons/PG.Commons/Collections/ValueListDictionary.cs @@ -0,0 +1,468 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using AnakinRaW.CommonUtilities.Collections; + +namespace PG.Commons.Collections; + +/// +/// Represents a generic read-only collection of key/value-list pairs. +/// +/// The type of the keys in the read-only dictionary. +/// The type of the value-list in the read-only dictionary. +public interface IReadOnlyValueListDictionary : IEnumerable> where TKey : notnull +{ + /// + /// Gets an enumerable collection that contains the values in the dictionary. + /// + /// + /// Modifications to the collection are not reflected in the dictionary. + ///
+ /// The order of the keys in the returned is unspecified, + /// but it is guaranteed that values of the same key get returned as a sequence in the order they were added. + ///
+ ICollection Values { get; } + + /// + /// Gets an containing the keys in the dictionary. + /// + /// + /// Modifications to the collection are not reflected in the dictionary. + ///
+ /// The order of the keys in the returned is unspecified. + ///
+ ICollection Keys { get; } + + /// + /// Gets the number of values contained in the dictionary. + /// + int Count { get; } + + /// + /// Gets the key at the specified index. + /// + /// The zero-based index of the key to get. + /// The key at the specified index. + /// is not a valid index in the dictionary. + TKey this[int index] { get; } + + /// + /// Gets the value at the specified key index. + /// + /// The zero-based index of the key to get the value for. + /// The value at the specified key index. + /// is not a valid index in the dictionary. + TValue GetValueAtKeyIndex(int index); + + /// + /// Determines whether the dictionary contains the specified key. + /// + /// The key to locate in the dictionary. + /// if the dictionary contains the key; otherwise, . + bool ContainsKey(TKey key); + + /// + /// Get the list of values stored at the specified key. + /// + /// The key to get the list of values for. + /// The list of values of the specified . + ReadOnlyFrugalList GetValues(TKey key); + + /// + /// Gets the last element with the specified key. + /// + /// The key of the element to get. + /// The last element with the specified key. + TValue GetLastValue(TKey key); + + /// + /// Gets the first element with the specified key. + /// + /// The key of the element to get. + /// The first element with the specified key. + TValue GetFirstValue(TKey key); + + /// + /// Gets the first value associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, the first value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. + /// if the dictionary contains a value with the specified key; otherwise, . + bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value); + + /// + /// Gets the last value associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, the last value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. + /// if the dictionary contains a value with the specified key; otherwise, . + bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value); + + /// + /// Gets the list of values associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, a list of values associated with the specified key, if the key is found; + /// otherwise, an empty list. This parameter is passed uninitialized. + /// if the dictionary contains at least one value with the specified key; otherwise, . + bool TryGetValues(TKey key, out ReadOnlyFrugalList values); +} + +/// +/// Represents a generic collection of key/value-list pairs. +/// +/// The type of the keys in the dictionary. +/// The type of the value-list in the dictionary. +public interface IValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull +{ + /// + /// Adds an element with the provided key to the value-list of the dictionary. + /// + /// The object to use as the key of the element to add. + /// The object to use as the value of the element to add. + /// if values of the same already existed; otherwise, . + bool Add(TKey key, TValue value); +} + +// NOT THREAD-SAFE! +public class ValueListDictionary : IValueListDictionary where TKey : notnull +{ + private readonly List _insertionTrackingList = new(); + private readonly Dictionary _singleValueDictionary; + private readonly Dictionary> _multiValueDictionary; + + private readonly IEqualityComparer _equalityComparer; + + public int Count => _insertionTrackingList.Count; + + public TKey this[int index] => _insertionTrackingList[index]; + + public ICollection Keys => _singleValueDictionary.Keys.Concat(_multiValueDictionary.Keys).ToList(); + + public ICollection Values => this.Select(x => x.Value).ToList(); + + public ValueListDictionary() : this(null) + { + } + + public ValueListDictionary(IEqualityComparer? comparer) + { + _equalityComparer = comparer ?? EqualityComparer.Default; + _singleValueDictionary = new Dictionary(_equalityComparer); + _multiValueDictionary = new Dictionary>(_equalityComparer); + } + + public TValue GetValueAtKeyIndex(int index) + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + var key = this[index]; + if (_singleValueDictionary.TryGetValue(key, out var value)) + return value; + + if (index == 0) + return _multiValueDictionary[key].First(); + + if (index == Count - 1) + return _multiValueDictionary[key].Last(); + + var keyCount = 0; + foreach (var k in _insertionTrackingList.Take(index + 1)) + { + if (_equalityComparer.Equals(key, k)) + keyCount++; + } + + return _multiValueDictionary[key][keyCount - 1]; + } + + public bool ContainsKey(TKey key) + { + return _singleValueDictionary.ContainsKey(key) || _multiValueDictionary.ContainsKey(key); + } + + public bool Add(TKey key, TValue value) + { + if (key is null) + throw new ArgumentNullException(nameof(key)); + + _insertionTrackingList.Add(key); + + if (!_singleValueDictionary.ContainsKey(key)) + { + if (!_multiValueDictionary.TryGetValue(key, out var list)) + { + _singleValueDictionary.Add(key, value); + return false; + } + + list.Add(value); + return true; + } + + Debug.Assert(_multiValueDictionary.ContainsKey(key) == false); + + var firstValue = _singleValueDictionary[key]; + _singleValueDictionary.Remove(key); + + _multiValueDictionary.Add(key, [ + firstValue, + value + ]); + + return true; + } + + public TValue GetLastValue(TKey key) + { + if (_singleValueDictionary.TryGetValue(key, out var value)) + return value; + + if (_multiValueDictionary.TryGetValue(key, out var valueList)) + return valueList.Last(); + + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + public TValue GetFirstValue(TKey key) + { + if (_singleValueDictionary.TryGetValue(key, out var value)) + return value; + + if (_multiValueDictionary.TryGetValue(key, out var valueList)) + return valueList.First(); + + throw new KeyNotFoundException($"The key '{key}' was not found."); + } + + public ReadOnlyFrugalList GetValues(TKey key) + { + if (TryGetValues(key, out var values)) + return values; + + throw new KeyNotFoundException($"The key '{key}' was not found."); + + } + + public bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value) + { + if (_singleValueDictionary.TryGetValue(key, out value!)) + return true; + + if (_multiValueDictionary.TryGetValue(key, out var valueList)) + { + value = valueList.First()!; + return true; + } + + return false; + } + + public bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value) + { + if (_singleValueDictionary.TryGetValue(key, out value!)) + return true; + + if (_multiValueDictionary.TryGetValue(key, out var valueList)) + { + value = valueList.Last()!; + return true; + } + + return false; + } + + public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) + { + if (_singleValueDictionary.TryGetValue(key, out var value)) + { + values = new ReadOnlyFrugalList(value); + return true; + } + + if (_multiValueDictionary.TryGetValue(key, out var valueList)) + { + values = new ReadOnlyFrugalList(valueList); + return true; + } + + values = ReadOnlyFrugalList.Empty; + return false; + } + + public IEnumerator> GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator> + { + private Dictionary.Enumerator _singleEnumerator; + private Dictionary>.Enumerator _multiEnumerator; + private List.Enumerator _currentListEnumerator = default; + private bool _isMultiEnumeratorActive = false; + + internal Enumerator(ValueListDictionary valueListDictionary) + { + _singleEnumerator = valueListDictionary._singleValueDictionary.GetEnumerator(); + _multiEnumerator = valueListDictionary._multiValueDictionary.GetEnumerator(); + } + + public KeyValuePair Current => + _isMultiEnumeratorActive + ? new KeyValuePair(_multiEnumerator.Current.Key, _currentListEnumerator.Current) + : _singleEnumerator.Current; + + object IEnumerator.Current => Current; + + public bool MoveNext() + { + if (_singleEnumerator.MoveNext()) + return true; + + if (_isMultiEnumeratorActive) + { + if (_currentListEnumerator.MoveNext()) + return true; + _isMultiEnumeratorActive = false; + } + + if (_multiEnumerator.MoveNext()) + { + _currentListEnumerator = _multiEnumerator.Current.Value.GetEnumerator(); + _isMultiEnumeratorActive = true; + return _currentListEnumerator.MoveNext(); + } + + return false; + } + + public void Reset() + { + throw new NotSupportedException(); + } + + public void Dispose() + { + _singleEnumerator.Dispose(); + _multiEnumerator.Dispose(); + } + } +} + +public static class ValueListDictionaryExtensions +{ + public static IEnumerable + AggregateValues( + this IReadOnlyValueListDictionary valueListDictionary, + AggregateStrategy aggregateStrategy, + Predicate? filter = null) + where TKey : notnull + where T : TValue + { + return valueListDictionary.AggregateValues(valueListDictionary.Keys, aggregateStrategy, filter); + } + + public static IEnumerable AggregateValues( + this IReadOnlyValueListDictionary valueListDictionary, + ICollection keys, + AggregateStrategy aggregateStrategy, + Predicate? filter = null) + where TKey : notnull + where T : TValue + { + foreach (var key in keys) + { + if (!valueListDictionary.ContainsKey(key)) + continue; + if (aggregateStrategy == AggregateStrategy.MultipleValuesPerKey) + { + foreach (var value in valueListDictionary.GetValues(key)) + { + if (value is not null) + { + var typedValue = (T)value; + if (filter is not null && filter(typedValue)) + yield return typedValue; + } + + } + } + else + { + var value = aggregateStrategy == AggregateStrategy.FirstValuePerKey + ? valueListDictionary.GetFirstValue(key) + : valueListDictionary.GetLastValue(key); + if (value is not null) + { + var typedValue = (T)value; + if (filter is not null && filter(typedValue)) + yield return typedValue; + } + } + } + } + + public static IEnumerable AggregateValues( + this IReadOnlyValueListDictionary valueListDictionary, + Func, T> selector, + AggregateStrategy aggregateStrategy) + where TKey : notnull + { + return valueListDictionary.AggregateValues(valueListDictionary.Keys, selector, aggregateStrategy); + } + + public static IEnumerable AggregateValues( + this IReadOnlyValueListDictionary valueListDictionary, + ICollection keys, + Func, T> selector, + AggregateStrategy aggregateStrategy) + where TKey : notnull + { + foreach (var key in keys) + { + if (!valueListDictionary.ContainsKey(key)) + continue; + if (aggregateStrategy == AggregateStrategy.MultipleValuesPerKey) + { + foreach (var value in valueListDictionary.GetValues(key)) + { + if (value is not null) + yield return selector(new KeyValuePair(key, value)); + + } + } + else + { + var value = aggregateStrategy == AggregateStrategy.FirstValuePerKey + ? valueListDictionary.GetFirstValue(key) + : valueListDictionary.GetLastValue(key); + if (value is not null) + { + yield return selector(new KeyValuePair(key, value)); + } + } + } + } + + public enum AggregateStrategy + { + FirstValuePerKey, + LastValuePerKey, + MultipleValuesPerKey, + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Numerics/Vector2Int.cs b/PG.Commons/PG.Commons/Numerics/Vector2Int.cs new file mode 100644 index 00000000..91bb60f9 --- /dev/null +++ b/PG.Commons/PG.Commons/Numerics/Vector2Int.cs @@ -0,0 +1,73 @@ +using System; + +namespace PG.Commons.Numerics; + +/// +/// Represents a vector with two integer values. +/// +public readonly struct Vector2Int : IEquatable +{ + /// + /// The first component of the vector. + /// + public int First { get; } + + /// + /// The second component of the vector. + /// + public int Second { get; } + + /// + /// Constructs a vector from the given . If the span does not contain enough elements, + /// the default integer value 0 is used to initialize the respecting component. + /// + /// The span of elements to assign to the vector. + public Vector2Int(ReadOnlySpan values) + { + var length = values.Length; + if (length >= 1) + First = values[0]; + if (length >= 2) + Second = values[1]; + } + + /// + /// Creates a vector whose elements have the specified values. + /// + /// The value to assign to the field. + /// The value to assign to the field. + public Vector2Int(int first, int second) + { + First = first; + Second = second; + } + + /// + /// Returns a value that indicates whether this instance and another vector are equal. + /// + /// The other vector. + /// if the two vectors are equal; otherwise, . + public bool Equals(Vector2Int other) + { + return First == other.First && Second == other.Second; + } + + /// + /// Returns a value that indicates whether this instance and a specified object are equal. + /// + /// The object to compare with the current instance. + /// if the current instance and are equal; otherwise, . If obj is , the method returns . + public override bool Equals(object? obj) + { + return obj is Vector2Int other && Equals(other); + } + + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + return HashCode.Combine(First, Second); + } +} diff --git a/PG.Commons/PG.Commons/Numerics/Vector3Int.cs b/PG.Commons/PG.Commons/Numerics/Vector3Int.cs new file mode 100644 index 00000000..be7ed130 --- /dev/null +++ b/PG.Commons/PG.Commons/Numerics/Vector3Int.cs @@ -0,0 +1,82 @@ +using System; + +namespace PG.Commons.Numerics; + +/// +/// Represents a vector with three integer values. +/// +public readonly struct Vector3Int : IEquatable +{ + /// + /// The first component of the vector. + /// + public int First { get; } + + /// + /// The second component of the vector. + /// + public int Second { get; } + + /// + /// The third component of the vector. + /// + public int Third { get; } + + /// + /// Constructs a vector from the given . If the span does not contain enough elements, + /// the default integer value 0 is used to initialize the respecting component. + /// + /// The span of elements to assign to the vector. + public Vector3Int(ReadOnlySpan values) + { + var length = values.Length; + if (length >= 1) + First = values[0]; + if (length >= 2) + Second = values[1]; + if (length >= 3) + Third = values[2]; + } + + /// + /// Creates a vector whose elements have the specified values. + /// + /// The value to assign to the field. + /// The value to assign to the field. + /// The value to assign to the field. + public Vector3Int(int first, int second, int third) + { + First = first; + Second = second; + Third = third; + } + + /// + /// Returns a value that indicates whether this instance and another vector are equal. + /// + /// The other vector. + /// if the two vectors are equal; otherwise, . + public bool Equals(Vector3Int other) + { + return First == other.First && Second == other.Second && Third == other.Third; + } + + /// + /// Returns a value that indicates whether this instance and a specified object are equal. + /// + /// The object to compare with the current instance. + /// if the current instance and are equal; otherwise, . If obj is , the method returns . + public override bool Equals(object? obj) + { + return obj is Vector3Int other && Equals(other); + } + + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + return HashCode.Combine(First, Second, Third); + } +} diff --git a/PG.Commons/PG.Commons/Numerics/Vector4Int.cs b/PG.Commons/PG.Commons/Numerics/Vector4Int.cs new file mode 100644 index 00000000..10c783f8 --- /dev/null +++ b/PG.Commons/PG.Commons/Numerics/Vector4Int.cs @@ -0,0 +1,91 @@ +using System; + +namespace PG.Commons.Numerics; + +/// +/// Represents a vector with four integer values. +/// +public readonly struct Vector4Int : IEquatable +{ + /// + /// The first component of the vector. + /// + public int First { get; } + + /// + /// The second component of the vector. + /// + public int Second { get; } + + /// + /// The third component of the vector. + /// + public int Third { get; } + + /// + /// The fourth component of the vector. + /// + public int Fourth { get; } + + /// + /// Constructs a vector from the given . If the span does not contain enough elements, + /// the default integer value 0 is used to initialize the respecting component. + /// + /// The span of elements to assign to the vector. + public Vector4Int(ReadOnlySpan values) + { + var length = values.Length; + if (length >= 1) + First = values[0]; + if (length >= 2) + Second = values[1]; + if (length >= 3) + Third = values[2]; + if (length >= 4) + Fourth = values[3]; + } + + /// + /// Creates a vector whose elements have the specified values. + /// + /// The value to assign to the field. + /// The value to assign to the field. + /// The value to assign to the field. + /// The value to assign to the field. + public Vector4Int(int first, int second, int third, int fourth) + { + First = first; + Second = second; + Third = third; + Fourth = fourth; + } + + /// + /// Returns a value that indicates whether this instance and another vector are equal. + /// + /// The other vector. + /// if the two vectors are equal; otherwise, . + public bool Equals(Vector4Int other) + { + return First == other.First && Second == other.Second && Third == other.Third && Fourth == other.Fourth; + } + + /// + /// Returns a value that indicates whether this instance and a specified object are equal. + /// + /// The object to compare with the current instance. + /// if the current instance and are equal; otherwise, . If obj is , the method returns . + public override bool Equals(object? obj) + { + return obj is Vector4Int other && Equals(other); + } + + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + return HashCode.Combine(First, Second, Third, Fourth); + } +} From 052f3ab2d672bb4bfbea0356ed3e6e5dfaf2f89e Mon Sep 17 00:00:00 2001 From: AnakinRaW Date: Tue, 8 Oct 2024 16:20:05 +0200 Subject: [PATCH 2/2] add tests --- .../Collections/ValueListDictionaryTests.cs | 436 ++++++++++++++++++ .../Numerics/Vector2IntTests.cs | 139 ++++++ .../Numerics/Vector3IntTests.cs | 134 ++++++ .../Numerics/Vector4IntTests.cs | 141 ++++++ .../IReadOnlyValueListDictionary.cs | 114 +++++ .../Collections/IValueListDictionary.cs | 17 + .../Collections/ValueListDictionary.cs | 268 ++--------- 7 files changed, 1018 insertions(+), 231 deletions(-) create mode 100644 PG.Commons/PG.Commons.Test/Collections/ValueListDictionaryTests.cs create mode 100644 PG.Commons/PG.Commons.Test/Numerics/Vector2IntTests.cs create mode 100644 PG.Commons/PG.Commons.Test/Numerics/Vector3IntTests.cs create mode 100644 PG.Commons/PG.Commons.Test/Numerics/Vector4IntTests.cs create mode 100644 PG.Commons/PG.Commons/Collections/IReadOnlyValueListDictionary.cs create mode 100644 PG.Commons/PG.Commons/Collections/IValueListDictionary.cs diff --git a/PG.Commons/PG.Commons.Test/Collections/ValueListDictionaryTests.cs b/PG.Commons/PG.Commons.Test/Collections/ValueListDictionaryTests.cs new file mode 100644 index 00000000..7fe073a1 --- /dev/null +++ b/PG.Commons/PG.Commons.Test/Collections/ValueListDictionaryTests.cs @@ -0,0 +1,436 @@ +using PG.Commons.Collections; +using System.Collections.Generic; +using System; +using System.Collections; +using Xunit; + +namespace PG.Commons.Test.Collections; + +public class ValueListDictionaryTests +{ + [Fact] + public void Constructor_Should_Initialize_EmptyDictionary() + { + var dictionary = new ValueListDictionary(); + + Assert.NotNull(dictionary); + Assert.Empty(dictionary.Keys); + Assert.Empty(dictionary.Values); + Assert.Equal(0, dictionary.Count); + } + + [Fact] + public void Constructor_WithComparer_Should_InitializeWithCustomComparer() + { + var comparer = StringComparer.OrdinalIgnoreCase; + + var dictionary = new ValueListDictionary(comparer); + + Assert.NotNull(dictionary); + Assert.Empty(dictionary.Keys); + Assert.Empty(dictionary.Values); + Assert.Equal(0, dictionary.Count); + + dictionary.Add("KEY1", 1); + Assert.True(dictionary.ContainsKey("key1")); + Assert.True(dictionary.ContainsKey("KEY1")); + + dictionary.Add("key1", 2); + Assert.True(dictionary.ContainsKey("key1")); + Assert.True(dictionary.ContainsKey("KEY1")); + + Assert.Equal(2, dictionary.GetValues("kEy1").Count); + Assert.Equal(2, dictionary.Count); + } + + [Fact] + public void Add_Should_AddSingleValue_WhenKeyDoesNotExist() + { + var dictionary = new ValueListDictionary(); + + var added = dictionary.Add("Key1", 10); + + Assert.False(added); // No prior values + Assert.Single(dictionary.Keys); + Assert.Single(dictionary.Values); + Assert.Equal(1, dictionary.Count); + } + + [Fact] + public void Add_Should_AddValueToExistingKey_WhenKeyAlreadyExists() + { + var dictionary = new ValueListDictionary { { "Key1", 10 } }; + + var added = dictionary.Add("Key1", 20); + + Assert.True(added); // Prior values exist + Assert.Single(dictionary.Keys); + Assert.Equal(2, dictionary.Count); + } + + [Fact] + public void Keys_Should_ReturnAllKeysInDictionary() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key2", 20 } + }; + + var keys = dictionary.Keys; + + Assert.Equal(2, keys.Count); + Assert.Contains("Key1", keys); + Assert.Contains("Key2", keys); + } + + [Fact] + public void Values_Should_ReturnAllValuesInDictionary() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key2", 20 } + }; + + var values = dictionary.Values; + + Assert.Equal(2, values.Count); + Assert.Contains(10, values); + Assert.Contains(20, values); + } + + [Fact] + public void Indexer_Should_ReturnKeyAtIndex() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key2", 20 }, + { "Key1", 100} + }; + + var keyAtIndex0 = dictionary[0]; + var keyAtIndex1 = dictionary[1]; + var keyAtIndex2 = dictionary[2]; + + Assert.Equal("Key1", keyAtIndex0); + Assert.Equal("Key2", keyAtIndex1); + Assert.Equal("Key1", keyAtIndex2); + } + + [Fact] + public void Indexer_Should_ThrowException_WhenIndexIsOutOfRange() + { + var dictionary = new ValueListDictionary { { "Key1", 10 } }; + + Assert.Throws(() => dictionary[1]); + } + + [Fact] + public void GetValueAtKeyIndex_Should_ReturnValueAtIndex() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key1", 100 }, + { "Key2", 20}, + { "Key1", 1000}, + }; + + var valueAtIndex0 = dictionary.GetValueAtKeyIndex(0); + var valueAtIndex1 = dictionary.GetValueAtKeyIndex(1); + var valueAtIndex2 = dictionary.GetValueAtKeyIndex(2); + var valueAtIndex3 = dictionary.GetValueAtKeyIndex(3); + + Assert.Equal(10, valueAtIndex0); + Assert.Equal(100, valueAtIndex1); + Assert.Equal(20, valueAtIndex2); + Assert.Equal(1000, valueAtIndex3); + } + + [Fact] + public void GetValueAtKeyIndex_Should_ThrowException_WhenIndexIsOutOfRange() + { + var dictionary = new ValueListDictionary { { "Key1", 10 } }; + + Assert.Throws(() => dictionary.GetValueAtKeyIndex(1)); + } + + [Fact] + public void ContainsKey_Should_ReturnTrue_WhenKeyExists() + { + var dictionary = new ValueListDictionary { { "Key1", 10 } }; + + var result = dictionary.ContainsKey("Key1"); + + Assert.True(result); + } + + [Fact] + public void ContainsKey_Should_ReturnFalse_WhenKeyDoesNotExist() + { + var dictionary = new ValueListDictionary(); + + var result = dictionary.ContainsKey("Key1"); + + Assert.False(result); + } + + [Fact] + public void GetValues_Should_ReturnListOfValues_ForExistingKey() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key1", 20 } + }; + + var values = dictionary.GetValues("Key1"); + + Assert.Equal(2, values.Count); + Assert.Equal([10, 20], values); + } + + [Fact] + public void GetValues_Should_ThrowException_WhenKeyDoesNotExist() + { + var dictionary = new ValueListDictionary(); + + Assert.Throws(() => dictionary.GetValues("Key1")); + } + + [Fact] + public void GetFirstValue_Should_ReturnFirstValue_ForExistingKey() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key1", 100 }, + { "Key2", 20 }, + }; + + var firstValueKey1 = dictionary.GetFirstValue("Key1"); + var firstValueKey2 = dictionary.GetFirstValue("Key2"); + + Assert.Equal(10, firstValueKey1); + Assert.Equal(20, firstValueKey2); + } + + [Fact] + public void GetFirstValue_Should_ThrowException_WhenKeyDoesNotExist() + { + var dictionary = new ValueListDictionary(); + + Assert.Throws(() => dictionary.GetFirstValue("Key1")); + } + + [Fact] + public void GetLastValue_Should_ReturnLastValue_ForExistingKey() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key1", 100 }, + { "Key2", 20 }, + }; + + var lastValueKey1 = dictionary.GetLastValue("Key1"); + var lastValueKey2 = dictionary.GetLastValue("Key2"); + + Assert.Equal(100, lastValueKey1); + Assert.Equal(20, lastValueKey2); + } + + [Fact] + public void GetLastValue_Should_ThrowException_WhenKeyDoesNotExist() + { + var dictionary = new ValueListDictionary(); + + Assert.Throws(() => dictionary.GetLastValue("Key1")); + } + + [Fact] + public void TryGetFirstValue_Should_ReturnTrueAndFirstValue_WhenKeyExists() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key1", 100 }, + { "Key2", 20 }, + }; + + var resultKey1 = dictionary.TryGetFirstValue("Key1", out var firstValueKey1); + var resultKey2 = dictionary.TryGetFirstValue("Key2", out var firstValueKey2); + + Assert.True(resultKey1); + Assert.Equal(10, firstValueKey1); + + Assert.True(resultKey2); + Assert.Equal(20, firstValueKey2); + } + + [Fact] + public void TryGetFirstValue_Should_ReturnFalse_WhenKeyDoesNotExist() + { + var dictionary = new ValueListDictionary(); + + var result = dictionary.TryGetFirstValue("Key1", out var firstValue); + + Assert.False(result); + Assert.Equal(default, firstValue); + } + + [Fact] + public void TryGetLastValue_Should_ReturnTrueAndLastValue_WhenKeyExists() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key1", 100 }, + { "Key2", 20 }, + }; + + var resultKey1 = dictionary.TryGetLastValue("Key1", out var lastValueKey1); + var resultKey2 = dictionary.TryGetLastValue("Key2", out var lastValueKey2); + + Assert.True(resultKey1); + Assert.Equal(100, lastValueKey1); + + Assert.True(resultKey2); + Assert.Equal(20, lastValueKey2); + } + + [Fact] + public void TryGetLastValue_Should_ReturnFalse_WhenKeyDoesNotExist() + { + var dictionary = new ValueListDictionary(); + + var result = dictionary.TryGetLastValue("Key1", out var lastValue); + + Assert.False(result); + Assert.Equal(default, lastValue); + } + + [Fact] + public void TryGetValues_Should_ReturnTrueAndListOfValues_WhenKeyExists() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key1", 100 }, + { "Key2", 20 }, + }; + + var resultKey1 = dictionary.TryGetValues("Key1", out var valuesKey1); + var resultKey2 = dictionary.TryGetValues("Key2", out var valuesKey2); + + Assert.True(resultKey1); + Assert.Equal(2, valuesKey1.Count); + Assert.Equal([10, 100], valuesKey1); + + Assert.True(resultKey2); + Assert.Single(valuesKey2); + Assert.Equal([20], valuesKey2); + } + + [Fact] + public void TryGetValues_Should_ReturnFalse_WhenKeyDoesNotExist() + { + var dictionary = new ValueListDictionary(); + + var result = dictionary.TryGetValues("Key1", out var values); + + Assert.False(result); + Assert.Empty(values); // Expect empty list + } + + [Fact] + public void Count_Should_ReturnTotalNumberOfValuesInDictionary() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key1", 20 }, + { "Key2", 30 } + }; + + var count = dictionary.Count; + + Assert.Equal(3, count); // 2 values for Key1, 1 value for Key2 + } + + [Fact] + public void Enumerator_Should_IterateThroughAllKeyValuePairs() + { + var dictionary = new ValueListDictionary + { + { "Key1", 10 }, + { "Key2", 20 }, + { "Key2", 30 } + }; + + var keyValuePairs = new List>(); + + var objEnumerator = ((IEnumerable)dictionary).GetEnumerator(); + foreach (var pair in dictionary) + { + Assert.True(objEnumerator.MoveNext()); + Assert.Equal(pair, objEnumerator.Current); + keyValuePairs.Add(pair); + } + + Assert.Equal(3, keyValuePairs.Count); + Assert.Contains(new KeyValuePair("Key1", 10), keyValuePairs); + Assert.Contains(new KeyValuePair("Key2", 20), keyValuePairs); + Assert.Contains(new KeyValuePair("Key2", 30), keyValuePairs); + } + + [Fact] + public void Add_Should_ThrowException_WhenNullKeyIsProvided() + { + var dictionary = new ValueListDictionary(); + Assert.Throws(() => dictionary.Add(null!, 10)); + } + + [Fact] + public void GetValues_Should_ThrowException_WhenNullKeyIsProvided() + { + var dictionary = new ValueListDictionary(); + Assert.Throws(() => dictionary.GetValues(null!)); + } + + [Fact] + public void ContainsKey_Should_ThrowException_WhenNullKeyIsProvided() + { + var dictionary = new ValueListDictionary(); + Assert.Throws(() => dictionary.ContainsKey(null!)); + } + + [Fact] + public void Add_Should_HandleLargeNumberOfEntries() + { + var dictionary = new ValueListDictionary(); + const int largeCount = 1000000; + + for (var i = 0; i < largeCount; i++) + dictionary.Add(i, i); + + Assert.Equal(largeCount, dictionary.Count); + } + + [Fact] + public void GetValues_Should_HandleMultipleValuesForSameKey() + { + var dictionary = new ValueListDictionary(); + const int largeCount = 1000; + + for (var i = 0; i < largeCount; i++) + dictionary.Add("Key1", i); + + var values = dictionary.GetValues("Key1"); + Assert.Equal(largeCount, values.Count); + Assert.Equal(0, values[0]); + Assert.Equal(largeCount - 1, values[values.Count - 1]); // Last value check + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons.Test/Numerics/Vector2IntTests.cs b/PG.Commons/PG.Commons.Test/Numerics/Vector2IntTests.cs new file mode 100644 index 00000000..3983ed8f --- /dev/null +++ b/PG.Commons/PG.Commons.Test/Numerics/Vector2IntTests.cs @@ -0,0 +1,139 @@ +using System; +using PG.Commons.Numerics; +using Xunit; + +namespace PG.Commons.Test.Numerics; + +public class Vector2IntTests +{ + [Fact] + public void Constructor_WithTwoIntegers_ShouldInitializeCorrectly() + { + const int first = 5; + const int second = 10; + + var vector = new Vector2Int(first, second); + + Assert.Equal(first, vector.First); + Assert.Equal(second, vector.Second); + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenBothValuesProvided() + { + ReadOnlySpan values = [1, 2]; + + var vector = new Vector2Int(values); + + Assert.Equal(1, vector.First); + Assert.Equal(2, vector.Second); + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenOneValueProvided() + { + ReadOnlySpan values = [3]; + + var vector = new Vector2Int(values); + + Assert.Equal(3, vector.First); + Assert.Equal(0, vector.Second); // Default value for the second component + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenMoreValuesProvided() + { + ReadOnlySpan values = [1, 2, 3]; + + var vector = new Vector2Int(values); + + Assert.Equal(1, vector.First); + Assert.Equal(2, vector.Second); + } + + + [Fact] + public void Constructor_WithSpan_ShouldInitializeToZero_WhenNoValuesProvided() + { + var values = ReadOnlySpan.Empty; + + var vector = new Vector2Int(values); + + Assert.Equal(0, vector.First); // Default value for the first component + Assert.Equal(0, vector.Second); // Default value for the second component + } + + [Fact] + public void Equals_ShouldReturnTrue_WhenVectorsAreEqual() + { + var vector1 = new Vector2Int(1, 2); + var vector2 = new Vector2Int(1, 2); + + var result = vector1.Equals(vector2); + + Assert.True(result); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenVectorsAreNotEqual() + { + var vector1 = new Vector2Int(1, 2); + var vector2 = new Vector2Int(3, 4); + + var result = vector1.Equals(vector2); + + Assert.False(result); + } + + [Fact] + public void Equals_ShouldReturnTrue_WhenComparingWithEqualObject() + { + var vector1 = new Vector2Int(1, 2); + object vector2 = new Vector2Int(1, 2); + + var result = vector1.Equals(vector2); + + Assert.True(result); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenComparingWithDifferentObject() + { + var vector1 = new Vector2Int(1, 2); + object vector2 = new Vector2Int(3, 4); + + Assert.False(vector1.Equals(vector2)); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenComparingWithNullObject() + { + var vector = new Vector2Int(1, 2); + + Assert.False(vector.Equals(null)); + } + + [Fact] + public void GetHashCode_ShouldReturnSameHashCode_ForEqualVectors() + { + var vector1 = new Vector2Int(1, 2); + var vector2 = new Vector2Int(1, 2); + + var hashCode1 = vector1.GetHashCode(); + var hashCode2 = vector2.GetHashCode(); + + Assert.Equal(hashCode1, hashCode2); + } + + [Fact] + public void GetHashCode_ShouldReturnDifferentHashCode_ForDifferentVectors() + { + var vector1 = new Vector2Int(1, 2); + var vector2 = new Vector2Int(3, 4); + + var hashCode1 = vector1.GetHashCode(); + var hashCode2 = vector2.GetHashCode(); + + Assert.NotEqual(hashCode1, hashCode2); + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons.Test/Numerics/Vector3IntTests.cs b/PG.Commons/PG.Commons.Test/Numerics/Vector3IntTests.cs new file mode 100644 index 00000000..20726a30 --- /dev/null +++ b/PG.Commons/PG.Commons.Test/Numerics/Vector3IntTests.cs @@ -0,0 +1,134 @@ +using System; +using PG.Commons.Numerics; +using Xunit; + +namespace PG.Commons.Test.Numerics; + +public class Vector3IntTests +{ + [Fact] + public void Constructor_WithThreeIntegers_ShouldInitializeCorrectly() + { + const int first = 5; + const int second = 10; + const int third = 15; + + var vector = new Vector3Int(first, second, third); + + Assert.Equal(first, vector.First); + Assert.Equal(second, vector.Second); + Assert.Equal(third, vector.Third); + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenAllValuesProvided() + { + ReadOnlySpan values = [1, 2, 3]; + + var vector = new Vector3Int(values); + + Assert.Equal(1, vector.First); + Assert.Equal(2, vector.Second); + Assert.Equal(3, vector.Third); + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenTwoValuesProvided() + { + ReadOnlySpan values = [3, 4]; + + var vector = new Vector3Int(values); + + Assert.Equal(3, vector.First); + Assert.Equal(4, vector.Second); + Assert.Equal(0, vector.Third); // Default value for the third component + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenOneValueProvided() + { + ReadOnlySpan values = [5]; + + var vector = new Vector3Int(values); + + Assert.Equal(5, vector.First); + Assert.Equal(0, vector.Second); // Default value for the second component + Assert.Equal(0, vector.Third); // Default value for the third component + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenMoreValuesProvided() + { + ReadOnlySpan values = [1, 2, 3, 4]; + + var vector = new Vector3Int(values); + + Assert.Equal(1, vector.First); + Assert.Equal(2, vector.Second); + Assert.Equal(3, vector.Third); + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeToZero_WhenNoValuesProvided() + { + var values = ReadOnlySpan.Empty; + + var vector = new Vector3Int(values); + + Assert.Equal(0, vector.First); // Default value for the first component + Assert.Equal(0, vector.Second); // Default value for the second component + Assert.Equal(0, vector.Third); // Default value for the third component + } + + [Fact] + public void Equals_ShouldReturnTrue_WhenVectorsAreEqual() + { + var vector1 = new Vector3Int(1, 2, 3); + var vector2 = new Vector3Int(1, 2, 3); + + Assert.True(vector1.Equals(vector2)); + Assert.True(vector1.Equals((object)vector2)); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenComparingWithNullObject() + { + var vector = new Vector3Int(1, 2, 3); + + Assert.False(vector.Equals(null)); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenVectorsAreNotEqual() + { + var vector1 = new Vector3Int(1, 2, 3); + var vector2 = new Vector3Int(4, 5, 6); + + Assert.False(vector1.Equals(vector2)); + Assert.False(vector1.Equals((object)vector2)); + } + + [Fact] + public void GetHashCode_ShouldReturnSameHashCode_ForEqualVectors() + { + var vector1 = new Vector3Int(1, 2, 3); + var vector2 = new Vector3Int(1, 2, 3); + + var hashCode1 = vector1.GetHashCode(); + var hashCode2 = vector2.GetHashCode(); + + Assert.Equal(hashCode1, hashCode2); + } + + [Fact] + public void GetHashCode_ShouldReturnDifferentHashCode_ForDifferentVectors() + { + var vector1 = new Vector3Int(1, 2, 3); + var vector2 = new Vector3Int(4, 5, 6); + + var hashCode1 = vector1.GetHashCode(); + var hashCode2 = vector2.GetHashCode(); + + Assert.NotEqual(hashCode1, hashCode2); + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons.Test/Numerics/Vector4IntTests.cs b/PG.Commons/PG.Commons.Test/Numerics/Vector4IntTests.cs new file mode 100644 index 00000000..6b139d11 --- /dev/null +++ b/PG.Commons/PG.Commons.Test/Numerics/Vector4IntTests.cs @@ -0,0 +1,141 @@ +using System; +using PG.Commons.Numerics; +using Xunit; + +namespace PG.Commons.Test.Numerics; + +public class Vector4IntTests +{ + [Fact] + public void Constructor_WithFourIntegers_ShouldInitializeCorrectly() + { + const int first = 5; + const int second = 10; + const int third = 15; + const int fourth = 20; + + var vector = new Vector4Int(first, second, third, fourth); + + Assert.Equal(first, vector.First); + Assert.Equal(second, vector.Second); + Assert.Equal(third, vector.Third); + Assert.Equal(fourth, vector.Fourth); + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenAllValuesProvided() + { + ReadOnlySpan values = [1, 2, 3, 4]; + + var vector = new Vector4Int(values); + + Assert.Equal(1, vector.First); + Assert.Equal(2, vector.Second); + Assert.Equal(3, vector.Third); + Assert.Equal(4, vector.Fourth); + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenThreeValuesProvided() + { + ReadOnlySpan values = [3, 4, 5]; + + var vector = new Vector4Int(values); + + Assert.Equal(3, vector.First); + Assert.Equal(4, vector.Second); + Assert.Equal(5, vector.Third); + Assert.Equal(0, vector.Fourth); + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenTwoValuesProvided() + { + ReadOnlySpan values = [1, 2]; + + var vector = new Vector4Int(values); + + Assert.Equal(1, vector.First); + Assert.Equal(2, vector.Second); + Assert.Equal(0, vector.Third); // Default value for the third component + Assert.Equal(0, vector.Fourth); // Default value for the fourth component + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeToZero_WhenNoValuesProvided() + { + var values = ReadOnlySpan.Empty; + + var vector = new Vector4Int(values); + + Assert.Equal(0, vector.First); // Default value for the first component + Assert.Equal(0, vector.Second); // Default value for the second component + Assert.Equal(0, vector.Third); // Default value for the third component + Assert.Equal(0, vector.Fourth); // Default value for the fourth component + } + + [Fact] + public void Constructor_WithSpan_ShouldInitializeCorrectly_WhenMoreValuesProvided() + { + ReadOnlySpan values = [1, 2, 3, 4, 5]; + + var vector = new Vector4Int(values); + + Assert.Equal(1, vector.First); + Assert.Equal(2, vector.Second); + Assert.Equal(3, vector.Third); + Assert.Equal(4, vector.Fourth); + } + + [Fact] + public void Equals_ShouldReturnTrue_WhenVectorsAreEqual() + { + var vector1 = new Vector4Int(1, 2, 3, 4); + var vector2 = new Vector4Int(1, 2, 3, 4); + + Assert.True(vector1.Equals(vector2)); + Assert.True(vector1.Equals((object)vector2)); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenVectorsAreNotEqual() + { + var vector1 = new Vector4Int(1, 2, 3, 4); + var vector2 = new Vector4Int(5, 6, 7, 8); + + Assert.False(vector1.Equals(vector2)); + Assert.False(vector1.Equals((object)vector2)); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenComparingWithNullObject() + { + var vector = new Vector4Int(1, 2, 3, 4); + + Assert.False(vector.Equals(null)); + } + + [Fact] + public void GetHashCode_ShouldReturnSameHashCode_ForEqualVectors() + { + var vector1 = new Vector4Int(1, 2, 3, 4); + var vector2 = new Vector4Int(1, 2, 3, 4); + + var hashCode1 = vector1.GetHashCode(); + var hashCode2 = vector2.GetHashCode(); + + Assert.Equal(hashCode1, hashCode2); + } + + [Fact] + public void GetHashCode_ShouldReturnDifferentHashCode_ForDifferentVectors() + { + var vector1 = new Vector4Int(1, 2, 3, 4); + var vector2 = new Vector4Int(5, 6, 7, 8); + + var hashCode1 = vector1.GetHashCode(); + var hashCode2 = vector2.GetHashCode(); + + Assert.NotEqual(hashCode1, hashCode2); + } +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Collections/IReadOnlyValueListDictionary.cs b/PG.Commons/PG.Commons/Collections/IReadOnlyValueListDictionary.cs new file mode 100644 index 00000000..541c7fc9 --- /dev/null +++ b/PG.Commons/PG.Commons/Collections/IReadOnlyValueListDictionary.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using AnakinRaW.CommonUtilities.Collections; + +namespace PG.Commons.Collections; + +/// +/// Represents a generic read-only collection of key/value-list pairs. +/// +/// The type of the keys in the read-only dictionary. +/// The type of the value-list in the read-only dictionary. +public interface IReadOnlyValueListDictionary : IEnumerable> where TKey : notnull +{ + /// + /// Gets an enumerable collection that contains the values in the dictionary. + /// + /// + /// Modifications to the collection are not reflected in the dictionary. + ///
+ /// The order of the keys in the returned is unspecified, + /// but it is guaranteed that values of the same key get returned as a sequence in the order they were added. + ///
+ ICollection Values { get; } + + /// + /// Gets an containing the keys in the dictionary. + /// + /// + /// Modifications to the collection are not reflected in the dictionary. + ///
+ /// The order of the keys in the returned is unspecified. + ///
+ ICollection Keys { get; } + + /// + /// Gets the number of values contained in the dictionary. + /// + int Count { get; } + + /// + /// Gets the key at the specified index. + /// + /// The zero-based index of the key to get. + /// The key at the specified index. + /// is not a valid index in the dictionary. + TKey this[int index] { get; } + + /// + /// Gets the value at the specified key index. + /// + /// The zero-based index of the key to get the value for. + /// The value at the specified key index. + /// is not a valid index in the dictionary. + TValue GetValueAtKeyIndex(int index); + + /// + /// Determines whether the dictionary contains the specified key. + /// + /// The key to locate in the dictionary. + /// if the dictionary contains the key; otherwise, . + bool ContainsKey(TKey key); + + /// + /// Get the list of values stored at the specified key. + /// + /// The key to get the list of values for. + /// The list of values of the specified . + ReadOnlyFrugalList GetValues(TKey key); + + /// + /// Gets the last element with the specified key. + /// + /// The key of the element to get. + /// The last element with the specified key. + TValue GetLastValue(TKey key); + + /// + /// Gets the first element with the specified key. + /// + /// The key of the element to get. + /// The first element with the specified key. + TValue GetFirstValue(TKey key); + + /// + /// Gets the first value associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, the first value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. + /// if the dictionary contains a value with the specified key; otherwise, . + bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value); + + /// + /// Gets the last value associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, the last value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. + /// if the dictionary contains a value with the specified key; otherwise, . + bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value); + + /// + /// Gets the list of values associated with the specified key. + /// + /// The key whose value to get. + /// + /// When this method returns, a list of values associated with the specified key, if the key is found; + /// otherwise, an empty list. This parameter is passed uninitialized. + /// if the dictionary contains at least one value with the specified key; otherwise, . + bool TryGetValues(TKey key, out ReadOnlyFrugalList values); +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Collections/IValueListDictionary.cs b/PG.Commons/PG.Commons/Collections/IValueListDictionary.cs new file mode 100644 index 00000000..06da91e7 --- /dev/null +++ b/PG.Commons/PG.Commons/Collections/IValueListDictionary.cs @@ -0,0 +1,17 @@ +namespace PG.Commons.Collections; + +/// +/// Represents a generic collection of key/value-list pairs. +/// +/// The type of the keys in the dictionary. +/// The type of the value-list in the dictionary. +public interface IValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull +{ + /// + /// Adds an element with the provided key to the value-list of the dictionary. + /// + /// The object to use as the key of the element to add. + /// The object to use as the value of the element to add. + /// if values of the same already existed; otherwise, . + bool Add(TKey key, TValue value); +} \ No newline at end of file diff --git a/PG.Commons/PG.Commons/Collections/ValueListDictionary.cs b/PG.Commons/PG.Commons/Collections/ValueListDictionary.cs index f09a535b..32276172 100644 --- a/PG.Commons/PG.Commons/Collections/ValueListDictionary.cs +++ b/PG.Commons/PG.Commons/Collections/ValueListDictionary.cs @@ -8,131 +8,12 @@ namespace PG.Commons.Collections; -/// -/// Represents a generic read-only collection of key/value-list pairs. -/// -/// The type of the keys in the read-only dictionary. -/// The type of the value-list in the read-only dictionary. -public interface IReadOnlyValueListDictionary : IEnumerable> where TKey : notnull -{ - /// - /// Gets an enumerable collection that contains the values in the dictionary. - /// - /// - /// Modifications to the collection are not reflected in the dictionary. - ///
- /// The order of the keys in the returned is unspecified, - /// but it is guaranteed that values of the same key get returned as a sequence in the order they were added. - ///
- ICollection Values { get; } - - /// - /// Gets an containing the keys in the dictionary. - /// - /// - /// Modifications to the collection are not reflected in the dictionary. - ///
- /// The order of the keys in the returned is unspecified. - ///
- ICollection Keys { get; } - - /// - /// Gets the number of values contained in the dictionary. - /// - int Count { get; } - - /// - /// Gets the key at the specified index. - /// - /// The zero-based index of the key to get. - /// The key at the specified index. - /// is not a valid index in the dictionary. - TKey this[int index] { get; } - - /// - /// Gets the value at the specified key index. - /// - /// The zero-based index of the key to get the value for. - /// The value at the specified key index. - /// is not a valid index in the dictionary. - TValue GetValueAtKeyIndex(int index); - - /// - /// Determines whether the dictionary contains the specified key. - /// - /// The key to locate in the dictionary. - /// if the dictionary contains the key; otherwise, . - bool ContainsKey(TKey key); - - /// - /// Get the list of values stored at the specified key. - /// - /// The key to get the list of values for. - /// The list of values of the specified . - ReadOnlyFrugalList GetValues(TKey key); - - /// - /// Gets the last element with the specified key. - /// - /// The key of the element to get. - /// The last element with the specified key. - TValue GetLastValue(TKey key); - - /// - /// Gets the first element with the specified key. - /// - /// The key of the element to get. - /// The first element with the specified key. - TValue GetFirstValue(TKey key); - - /// - /// Gets the first value associated with the specified key. - /// - /// The key whose value to get. - /// - /// When this method returns, the first value associated with the specified key, if the key is found; - /// otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. - /// if the dictionary contains a value with the specified key; otherwise, . - bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value); - - /// - /// Gets the last value associated with the specified key. - /// - /// The key whose value to get. - /// - /// When this method returns, the last value associated with the specified key, if the key is found; - /// otherwise, the default value for the type of the parameter. This parameter is passed uninitialized. - /// if the dictionary contains a value with the specified key; otherwise, . - bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value); - - /// - /// Gets the list of values associated with the specified key. - /// - /// The key whose value to get. - /// - /// When this method returns, a list of values associated with the specified key, if the key is found; - /// otherwise, an empty list. This parameter is passed uninitialized. - /// if the dictionary contains at least one value with the specified key; otherwise, . - bool TryGetValues(TKey key, out ReadOnlyFrugalList values); -} - -/// -/// Represents a generic collection of key/value-list pairs. -/// -/// The type of the keys in the dictionary. -/// The type of the value-list in the dictionary. -public interface IValueListDictionary : IReadOnlyValueListDictionary where TKey : notnull -{ - /// - /// Adds an element with the provided key to the value-list of the dictionary. - /// - /// The object to use as the key of the element to add. - /// The object to use as the value of the element to add. - /// if values of the same already existed; otherwise, . - bool Add(TKey key, TValue value); -} - -// NOT THREAD-SAFE! +/// +/// +/// A can support multiple readers concurrently, as long as the collection is not modified. +/// Even so, enumerating through a collection is intrinsically not a thread-safe procedure. +/// In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. +/// public class ValueListDictionary : IValueListDictionary where TKey : notnull { private readonly List _insertionTrackingList = new(); @@ -141,18 +22,29 @@ public class ValueListDictionary : IValueListDictionary _equalityComparer; + /// public int Count => _insertionTrackingList.Count; + /// public TKey this[int index] => _insertionTrackingList[index]; - public ICollection Keys => _singleValueDictionary.Keys.Concat(_multiValueDictionary.Keys).ToList(); + /// + public virtual ICollection Keys => _singleValueDictionary.Keys.Concat(_multiValueDictionary.Keys).ToList(); + /// public ICollection Values => this.Select(x => x.Value).ToList(); + /// + /// Initializes a new instance of the class that is empty and uses the default equality comparer for the key type. + /// public ValueListDictionary() : this(null) { } + /// + /// Initializes a new instance of the class that is empty and uses the specified . + /// + /// The implementation to use when comparing keys, or null to use the default for the type of the key. public ValueListDictionary(IEqualityComparer? comparer) { _equalityComparer = comparer ?? EqualityComparer.Default; @@ -160,6 +52,7 @@ public ValueListDictionary(IEqualityComparer? comparer) _multiValueDictionary = new Dictionary>(_equalityComparer); } + /// public TValue GetValueAtKeyIndex(int index) { if (index < 0 || index >= Count) @@ -185,11 +78,13 @@ public TValue GetValueAtKeyIndex(int index) return _multiValueDictionary[key][keyCount - 1]; } + /// public bool ContainsKey(TKey key) { return _singleValueDictionary.ContainsKey(key) || _multiValueDictionary.ContainsKey(key); } - + + /// public bool Add(TKey key, TValue value) { if (key is null) @@ -222,6 +117,7 @@ public bool Add(TKey key, TValue value) return true; } + /// public TValue GetLastValue(TKey key) { if (_singleValueDictionary.TryGetValue(key, out var value)) @@ -233,6 +129,7 @@ public TValue GetLastValue(TKey key) throw new KeyNotFoundException($"The key '{key}' was not found."); } + /// public TValue GetFirstValue(TKey key) { if (_singleValueDictionary.TryGetValue(key, out var value)) @@ -243,7 +140,8 @@ public TValue GetFirstValue(TKey key) throw new KeyNotFoundException($"The key '{key}' was not found."); } - + + /// public ReadOnlyFrugalList GetValues(TKey key) { if (TryGetValues(key, out var values)) @@ -253,6 +151,7 @@ public ReadOnlyFrugalList GetValues(TKey key) } + /// public bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value) { if (_singleValueDictionary.TryGetValue(key, out value!)) @@ -267,6 +166,7 @@ public bool TryGetFirstValue(TKey key, [NotNullWhen(true)] out TValue value) return false; } + /// public bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value) { if (_singleValueDictionary.TryGetValue(key, out value!)) @@ -281,6 +181,7 @@ public bool TryGetLastValue(TKey key, [NotNullWhen(true)] out TValue value) return false; } + /// public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) { if (_singleValueDictionary.TryGetValue(key, out var value)) @@ -299,6 +200,7 @@ public bool TryGetValues(TKey key, out ReadOnlyFrugalList values) return false; } + /// public IEnumerator> GetEnumerator() { return new Enumerator(this); @@ -309,6 +211,9 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } + /// + /// Enumerates the values of a . + /// public struct Enumerator : IEnumerator> { private Dictionary.Enumerator _singleEnumerator; @@ -322,6 +227,7 @@ internal Enumerator(ValueListDictionary valueListDictionary) _multiEnumerator = valueListDictionary._multiValueDictionary.GetEnumerator(); } + /// public KeyValuePair Current => _isMultiEnumeratorActive ? new KeyValuePair(_multiEnumerator.Current.Key, _currentListEnumerator.Current) @@ -329,6 +235,7 @@ internal Enumerator(ValueListDictionary valueListDictionary) object IEnumerator.Current => Current; + /// public bool MoveNext() { if (_singleEnumerator.MoveNext()) @@ -351,118 +258,17 @@ public bool MoveNext() return false; } + /// public void Reset() { throw new NotSupportedException(); } + /// public void Dispose() { _singleEnumerator.Dispose(); _multiEnumerator.Dispose(); } } -} - -public static class ValueListDictionaryExtensions -{ - public static IEnumerable - AggregateValues( - this IReadOnlyValueListDictionary valueListDictionary, - AggregateStrategy aggregateStrategy, - Predicate? filter = null) - where TKey : notnull - where T : TValue - { - return valueListDictionary.AggregateValues(valueListDictionary.Keys, aggregateStrategy, filter); - } - - public static IEnumerable AggregateValues( - this IReadOnlyValueListDictionary valueListDictionary, - ICollection keys, - AggregateStrategy aggregateStrategy, - Predicate? filter = null) - where TKey : notnull - where T : TValue - { - foreach (var key in keys) - { - if (!valueListDictionary.ContainsKey(key)) - continue; - if (aggregateStrategy == AggregateStrategy.MultipleValuesPerKey) - { - foreach (var value in valueListDictionary.GetValues(key)) - { - if (value is not null) - { - var typedValue = (T)value; - if (filter is not null && filter(typedValue)) - yield return typedValue; - } - - } - } - else - { - var value = aggregateStrategy == AggregateStrategy.FirstValuePerKey - ? valueListDictionary.GetFirstValue(key) - : valueListDictionary.GetLastValue(key); - if (value is not null) - { - var typedValue = (T)value; - if (filter is not null && filter(typedValue)) - yield return typedValue; - } - } - } - } - - public static IEnumerable AggregateValues( - this IReadOnlyValueListDictionary valueListDictionary, - Func, T> selector, - AggregateStrategy aggregateStrategy) - where TKey : notnull - { - return valueListDictionary.AggregateValues(valueListDictionary.Keys, selector, aggregateStrategy); - } - - public static IEnumerable AggregateValues( - this IReadOnlyValueListDictionary valueListDictionary, - ICollection keys, - Func, T> selector, - AggregateStrategy aggregateStrategy) - where TKey : notnull - { - foreach (var key in keys) - { - if (!valueListDictionary.ContainsKey(key)) - continue; - if (aggregateStrategy == AggregateStrategy.MultipleValuesPerKey) - { - foreach (var value in valueListDictionary.GetValues(key)) - { - if (value is not null) - yield return selector(new KeyValuePair(key, value)); - - } - } - else - { - var value = aggregateStrategy == AggregateStrategy.FirstValuePerKey - ? valueListDictionary.GetFirstValue(key) - : valueListDictionary.GetLastValue(key); - if (value is not null) - { - yield return selector(new KeyValuePair(key, value)); - } - } - } - } - - public enum AggregateStrategy - { - FirstValuePerKey, - LastValuePerKey, - MultipleValuesPerKey, - } } \ No newline at end of file