From df169e376acd25bea6372667ceb7fdf6fea6ef96 Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Fri, 2 Mar 2012 23:00:02 +0000 Subject: [PATCH] Removed overridable change notifications from MultiDictionary - accurately sending these would involve considerable overhead; added unit tests for all main interface methods of the MultiDictionary git-svn-id: file:///srv/devel/repo-conversion/nusu@261 d2e56fa2-650e-0410-a79f-9358c0239efd --- Documents/CommandLine.txt | 489 +++++++++++++++++- .../Collections/MultiDictionary.Interfaces.cs | 2 - Source/Collections/MultiDictionary.Test.cs | 178 +++++++ Source/Collections/MultiDictionary.cs | 73 +-- 4 files changed, 663 insertions(+), 79 deletions(-) diff --git a/Documents/CommandLine.txt b/Documents/CommandLine.txt index ef8da36..c3c6589 100644 --- a/Documents/CommandLine.txt +++ b/Documents/CommandLine.txt @@ -1,13 +1,478 @@ - /* - struct CommandLine { - [Option] - bool? Option; - [Option] - int? Width; - [Option] - TypeCode Code; - [Values] - string[] Values; - } -*/ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2012 Nuclex Development Labs +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Nuclex.Support.Collections { + + /// Dictionary that can contain multiple values under the same key + /// Type of keys used within the dictionary + /// Type of values used within the dictionary + public partial class MultiDictionary : IMultiDictionary { + + #region class Enumerator + + /// Enumerates the values stored in a multi dictionary + private class Enumerator : + IEnumerator>, IDictionaryEnumerator { + + /// Initializes a new multi dictionary enumerator + /// Dictionary that will be enumerated + public Enumerator(MultiDictionary dictionary) { + this.dictionary = dictionary; + + Reset(); + } + + /// The current entry the enumerator is pointing at + public KeyValuePair Current { + get { + if(this.currentValue == null) { + throw new InvalidOperationException("Enumerator is not on a valid position"); + } + + return new KeyValuePair( + this.currentCollection.Current.Key, this.currentValue.Current + ); + } + } + + /// Immediately releases all resources owned by the instance + public void Dispose() { + if(this.currentValue != null) { + this.currentValue.Dispose(); + this.currentValue = null; + } + if(this.currentCollection != null) { + this.currentCollection.Dispose(); + this.currentCollection = null; + } + } + + /// Advances the enumerator to the entry + /// + /// True if there was a next entry, false if the end of the set has been reached + /// + public bool MoveNext() { + if(this.currentCollection == null) { + return false; + } + + for(; ; ) { + + // Try to move the enumerator in the current key's list to the next item + if(this.currentValue != null) { + if(this.currentValue.MoveNext()) { + return true; // We found the next item + } else { + this.currentValue.Dispose(); + } + } + + // Enumerator for the current key's list reached the end, go to the next key + if(this.currentCollection.MoveNext()) { + this.currentValue = this.currentCollection.Current.Value.GetEnumerator(); + } else { + this.currentValue = null; // Guaranteed to be disposed already + this.currentCollection.Dispose(); + this.currentCollection = null; + return false; + } + + } + } + + /// Resets the enumerator to its initial position + public void Reset() { + if(this.currentValue != null) { + this.currentValue.Dispose(); + this.currentValue = null; + } + if(this.currentCollection != null) { + this.currentCollection.Dispose(); + } + this.currentCollection = this.dictionary.GetEnumerator(); + } + + #region IEnumerator implementation + + /// The item the enumerator is currently pointing at + object IEnumerator.Current { + get { return Current; } + } + + #endregion // IEnumerator implementation + + #region IDictionaryEnumerator implementation + + /// The current entry the enumerator is pointing to + DictionaryEntry IDictionaryEnumerator.Entry { + get { + enforceEnumeratorOnValidPosition(); + + return new DictionaryEntry( + this.currentCollection.Current.Key, this.currentValue.Current + ); + } + } + + /// + /// Throws an exception if the enumerator is not on a valid position + /// + private void enforceEnumeratorOnValidPosition() { + if(this.currentValue == null) { + throw new InvalidOperationException("Enumerator is not on a valid position"); + } + } + + /// The current dictionary key + object IDictionaryEnumerator.Key { + get { + enforceEnumeratorOnValidPosition(); + return this.currentCollection.Current.Key; + } + } + + /// The current dictionary value + object IDictionaryEnumerator.Value { + get { + enforceEnumeratorOnValidPosition(); + return this.currentValue.Current; + } + } + + #endregion // IDictionaryEnumerator implementation + + /// Dictionary over whose entries the enumerator is enumerating + private IDictionary> dictionary; + /// Current key the enumerator is at + private IEnumerator>> currentCollection; + /// Current value in the current key the enumerator is at + private IEnumerator currentValue; + + } + + #endregion // class Enumerator + + #region class ValueList + + /// Stores the list of values for a dictionary key + private class ValueList : Collection { + + /// Initializes a new value list + /// Dictionary the value list belongs to + public ValueList(MultiDictionary dictionary) { + this.dictionary = dictionary; + } + + /// Called when the value list is being cleared + protected override void ClearItems() { + this.dictionary.count -= Count; + base.ClearItems(); + } + + /// Called when an item is inserted into the value list + /// Index at which the item is being inserted + /// Item that is being inserted + protected override void InsertItem(int index, TValue item) { + base.InsertItem(index, item); + ++this.dictionary.count; + } + + /// Called when an item is removed from the value list + /// Index at which the item is being removed + protected override void RemoveItem(int index) { + base.RemoveItem(index); + --this.dictionary.count; + } + + /// The dictionary the value list belongs to + private MultiDictionary dictionary; + + } + + #endregion // class ValueList + + /// Initializes a new multi dictionary + public MultiDictionary() : this(new Dictionary>()) { } + + /// Initializes a new multi dictionary + /// Dictionary the multi dictionary will be based on + internal MultiDictionary(IDictionary> dictionary) { + this.typedDictionary = dictionary; + this.objectDictionary = (this.typedDictionary as IDictionary); + + foreach(ICollection values in dictionary.Values) { + this.count += values.Count; + } + } + + /// Whether the dictionary is write-protected + public bool IsReadOnly { + get { return this.typedDictionary.IsReadOnly; } + } + + /// Determines the number of values stored under the specified key + /// Key whose values will be counted + /// The number of values stored under the specified key + public int CountValues(TKey key) { + ICollection values; + if(this.typedDictionary.TryGetValue(key, out values)) { + return values.Count; + } else { + return 0; + } + } + + /// + /// Determines whether the specified KeyValuePair is contained in the dictionary + /// + /// KeyValuePair that will be checked for + /// True if the provided KeyValuePair was contained in the dictionary + public bool Contains(KeyValuePair item) { + ICollection values; + if(this.typedDictionary.TryGetValue(item.Key, out values)) { + return values.Contains(item.Value); + } else { + return false; + } + } + + /// Determines whether the Dictionary contains the specified key + /// Key that will be checked for + /// + /// True if an entry with the specified key was contained in the Dictionary + /// + public bool ContainsKey(TKey key) { + return this.typedDictionary.ContainsKey(key); + } + + /// Copies the contents of the Dictionary into an array + /// Array the Dictionary will be copied into + /// + /// Starting index at which to begin filling the destination array + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) { + foreach(KeyValuePair> item in this.typedDictionary) { + foreach(TValue value in item.Value) { + array[arrayIndex] = new KeyValuePair(item.Key, value); + ++arrayIndex; + } + } + } + + /// Number of elements contained in the Dictionary + public int Count { + get { return this.count; } + } + + /// Creates a new enumerator for the dictionary + /// The new dictionary enumerator + public IEnumerator> GetEnumerator() { + return new Enumerator(this); + } + + /// Collection of all keys contained in the dictionary + public ICollection Keys { + get { return this.typedDictionary.Keys; } + } + + /// Collection of all values contained in the dictionary + public ICollection Values { + get { + if(this.valueCollection == null) { + this.valueCollection = new ValueCollection(this); + } + + return this.valueCollection; + } + } + + /// + /// Attempts to retrieve the item with the specified key from the dictionary + /// + /// Key of the item to attempt to retrieve + /// + /// Output parameter that will receive the values upon successful completion + /// + /// + /// True if the item was found and has been placed in the output parameter + /// + public bool TryGetValue(TKey key, out ICollection values) { + return this.typedDictionary.TryGetValue(key, out values); + } + + /// Accesses an item in the dictionary by its key + /// Key of the item that will be accessed + public ICollection this[TKey key] { + get { return this.typedDictionary[key]; } + set { + if(value == null) { + ICollection values; + if(this.typedDictionary.TryGetValue(key, out values)) { + foreach(TValue removedValue in values) { + + } + } + this.typedDictionary.Remove(key); + } + + ICollection currentValues; + if(this.typedDictionary.TryGetValue(key, out currentValues)) { + ValueList currentValueList = (ValueList)currentValues; + + int index = 0; + foreach(TValue addedValue in value) { + if(index < currentValueList.Count) { + TValue original = currentValueList[index]; + currentValueList[index] = addedValue; + OnReplaced( + new KeyValuePair(key, original), + new KeyValuePair(key, addedValue) + ); + } else { + currentValueList.Add(addedValue); + OnAdded(new KeyValuePair(key, addedValue)); + } + ++index; + } + + int count = currentValueList.Count; + while(count > index) { + --count; + TValue removedValue = currentValueList[count]; + currentValueList.RemoveAt(count); + OnRemoved(new KeyValuePair(key, removedValue)); + } + } else { + currentValues = new ValueList(this); + this.typedDictionary.Add(key, currentValues); + + foreach(TValue addedValue in value) { + currentValues.Add(addedValue); + OnAdded(new KeyValuePair(key, addedValue)); + ++this.count; + } + } + } + } + + /// Inserts an item into the dictionary + /// Key under which to add the new item + /// Item that will be added to the dictionary + public void Add(TKey key, TValue value) { + ICollection values; + if(!this.typedDictionary.TryGetValue(key, out values)) { + values = new ValueList(this); + this.typedDictionary.Add(key, values); + } + + values.Add(value); + OnAdded(new KeyValuePair(key, value)); + } + + /// + /// Removes the item with the specified key and value from the dictionary + /// + /// Key of the item that will be removed + /// Value of the item that will be removed + /// + /// True if the specified item was contained in the dictionary and was removed + /// + /// If the dictionary is read-only + public bool Remove(TKey key, TValue value) { + ICollection values; + if(this.typedDictionary.TryGetValue(key, out values)) { + values.Remove(value); + + if(values.Count == 0) { + this.typedDictionary.Remove(key); + } + + OnRemoved(new KeyValuePair(key, value)); + return true; + } else { + return false; + } + } + + /// Removes all items with the specified key from the dictionary + /// Key of the item that will be removed + /// The number of items that have been removed from the dictionary + /// If the dictionary is read-only + public int RemoveKey(TKey key) { + ICollection values; + if(this.typedDictionary.TryGetValue(key, out values)) { + this.count -= values.Count; + this.typedDictionary.Remove(key); + + foreach(TValue value in values) { + OnRemoved(new KeyValuePair(key, value)); + } + return values.Count; + } else { + return 0; + } + } + + /// Removes all items from the Dictionary + public void Clear() { + OnClearing(); + this.typedDictionary.Clear(); + this.count = 0; + OnCleared(); + } + + /// Fires the 'ItemAdded' event + /// Item that has been added to the collection + protected virtual void OnAdded(KeyValuePair item) { } + + /// Fires the 'ItemRemoved' event + /// Item that has been removed from the collection + protected virtual void OnRemoved(KeyValuePair item) { } + + /// Fires the 'ItemReplaced' event + /// Item that was replaced in the collection + /// Item that the original was replaced by + protected virtual void OnReplaced( + KeyValuePair oldItem, KeyValuePair newItem + ) { } + + /// Fires the 'Clearing' event + protected virtual void OnClearing() { } + + /// Fires the 'Cleared' event + protected virtual void OnCleared() { } + + /// The wrapped Dictionary under its type-safe interface + private IDictionary> typedDictionary; + /// The wrapped Dictionary under its object interface + private IDictionary objectDictionary; + /// The number of items currently in the multi dictionary + private int count; + /// Provides the values stores in the dictionary in sequence + private ValueCollection valueCollection; + + } + +} // namespace Nuclex.Support.Collections diff --git a/Source/Collections/MultiDictionary.Interfaces.cs b/Source/Collections/MultiDictionary.Interfaces.cs index 86c1a1e..040ee01 100644 --- a/Source/Collections/MultiDictionary.Interfaces.cs +++ b/Source/Collections/MultiDictionary.Interfaces.cs @@ -119,8 +119,6 @@ namespace Nuclex.Support.Collections { if(values.Count == 0) { this.typedDictionary.Remove(itemToRemove.Key); } - - OnRemoved(itemToRemove); return true; } else { return false; diff --git a/Source/Collections/MultiDictionary.Test.cs b/Source/Collections/MultiDictionary.Test.cs index f950252..804057b 100644 --- a/Source/Collections/MultiDictionary.Test.cs +++ b/Source/Collections/MultiDictionary.Test.cs @@ -206,6 +206,184 @@ namespace Nuclex.Support.Collections { Assert.IsFalse(dictionary.ContainsKey(20)); } + /// + /// Verifies that the key collection can be retrieved from the dictionary + /// + [Test] + public void KeyCollectionCanBeRetrieved() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + + ICollection keys = dictionary.Keys; + Assert.IsNotNull(keys); + Assert.AreEqual(1, keys.Count); + } + + /// + /// Verifies that the key collection can be retrieved from the dictionary + /// + [Test] + public void ValueCollectionCanBeRetrieved() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + dictionary.Add(20, "twenty"); + + ICollection values = dictionary.Values; + Assert.IsNotNull(values); + Assert.AreEqual(3, values.Count); + } + + /// + /// Verifies that TryGetValue() returns false and doesn't throw if a key + /// is not found in the collection + /// + [Test] + public void TryGetValueReturnsFalseOnMissingKey() { + var dictionary = new MultiDictionary(); + ICollection values; + Assert.IsFalse(dictionary.TryGetValue(123, out values)); + } + + /// Verifies that keys can be looked up via TryGetValue() + [Test] + public void TryGetValueCanLookUpValues() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + ICollection values; + Assert.IsTrue(dictionary.TryGetValue(10, out values)); + Assert.AreEqual(2, values.Count); + } + + /// + /// Verifies that assigning null to a key deletes all the values stored + /// under it + /// + [Test] + public void AssigningNullToKeyRemovesAllValues() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + dictionary.Add(20, "twenty"); + + Assert.AreEqual(3, dictionary.Count); + dictionary[10] = null; + Assert.AreEqual(1, dictionary.Count); + Assert.IsFalse(dictionary.ContainsKey(10)); + } + + /// + /// Verifies that assigning null to a key deletes all the values stored + /// under it + /// + [Test] + public void ValueListCanBeAssignedToNewKey() { + var dictionary = new MultiDictionary(); + dictionary[3] = new List() { "three", "drei" }; + + Assert.AreEqual(2, dictionary.Count); + Assert.IsTrue(dictionary.Contains(new KeyValuePair(3, "three"))); + } + + /// + /// Verifies that assigning null to a key deletes all the values stored + /// under it + /// + [Test] + public void ValueListCanOverwriteExistingKey() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "dix"); + + Assert.AreEqual(1, dictionary.Count); + + dictionary[10] = new List() { "ten", "zehn" }; + + Assert.AreEqual(2, dictionary.Count); + Assert.IsFalse(dictionary.Contains(new KeyValuePair(10, "dix"))); + Assert.IsTrue(dictionary.Contains(new KeyValuePair(10, "ten"))); + } + + /// + /// Verifies that nothing bad happens when a key is removed from the dictionary + /// that it doesn't contain + /// + [Test] + public void NonExistingKeyCanBeRemoved() { + var dictionary = new MultiDictionary(); + Assert.AreEqual(0, dictionary.RemoveKey(123)); + } + + /// + /// Verifies that the remove method returns the number of values that have + /// been removed from the dictionary + /// + [Test] + public void RemoveReturnsNumberOfValuesRemoved() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + Assert.AreEqual(2, dictionary.RemoveKey(10)); + } + + /// + /// Verifies that the dictionary becomes empty after clearing it + /// + [Test] + public void DictionaryIsEmptyAfterClear() { + var dictionary = new MultiDictionary(); + dictionary.Add(10, "ten"); + dictionary.Add(10, "zehn"); + dictionary.Add(20, "twenty"); + Assert.AreEqual(3, dictionary.Count); + dictionary.Clear(); + Assert.AreEqual(0, dictionary.Count); + } + + /// + /// Verifies that non-existing values can be removed from the dictionary + /// + [Test] + public void NonExistingValueCanBeRemoved() { + var dictionary = new MultiDictionary(); + Assert.IsFalse(dictionary.Remove(123, "test")); + } + + /// + /// Verifies that nothing bad happens when the last value under a key is removed + /// + [Test] + public void LastValueOfKeyCanBeRemoved() { + var dictionary = new MultiDictionary(); + dictionary.Add(123, "test"); + dictionary.Remove(123, "test"); + Assert.AreEqual(0, dictionary.CountValues(123)); + } + + /// + /// Verifies that the dictionary can be copied into an array + /// + [Test] + public void DictionaryCanBeCopiedIntoArray() { + var expected = new List>() { + new KeyValuePair(1, "one"), + new KeyValuePair(1, "eins"), + new KeyValuePair(2, "two"), + new KeyValuePair(2, "zwei") + }; + + var dictionary = new MultiDictionary(); + foreach(KeyValuePair entry in expected) { + dictionary.Add(entry.Key, entry.Value); + } + + var actual = new KeyValuePair[4]; + dictionary.CopyTo(actual, 0); + + CollectionAssert.AreEquivalent(expected, actual); + } + } } // namespace Nuclex.Support.Collections diff --git a/Source/Collections/MultiDictionary.cs b/Source/Collections/MultiDictionary.cs index 9674990..d627192 100644 --- a/Source/Collections/MultiDictionary.cs +++ b/Source/Collections/MultiDictionary.cs @@ -328,43 +328,17 @@ namespace Nuclex.Support.Collections { get { return this.typedDictionary[key]; } set { if(value == null) { - this.typedDictionary.Remove(key); - } - - ICollection currentValues; - if(this.typedDictionary.TryGetValue(key, out currentValues)) { - ValueList currentValueList = (ValueList)currentValues; - - int index = 0; - foreach(TValue addedValue in value) { - if(index < currentValueList.Count) { - TValue original = currentValueList[index]; - currentValueList[index] = addedValue; - OnReplaced( - new KeyValuePair(key, original), - new KeyValuePair(key, addedValue) - ); - } else { - currentValueList.Add(addedValue); - OnAdded(new KeyValuePair(key, addedValue)); - } - ++index; - } - - int count = currentValueList.Count; - while(count > index) { - --count; - TValue removedValue = currentValueList[count]; - currentValueList.RemoveAt(count); - OnRemoved(new KeyValuePair(key, removedValue)); - } + RemoveKey(key); } else { - currentValues = new ValueList(this); - this.typedDictionary.Add(key, currentValues); - + ICollection currentValues; + if(this.typedDictionary.TryGetValue(key, out currentValues)) { + currentValues.Clear(); + } else { + currentValues = new ValueList(this); + this.typedDictionary.Add(key, currentValues); + } foreach(TValue addedValue in value) { currentValues.Add(addedValue); - OnAdded(new KeyValuePair(key, addedValue)); } } } @@ -381,7 +355,6 @@ namespace Nuclex.Support.Collections { } values.Add(value); - OnAdded(new KeyValuePair(key, value)); } /// @@ -397,12 +370,9 @@ namespace Nuclex.Support.Collections { ICollection values; if(this.typedDictionary.TryGetValue(key, out values)) { values.Remove(value); - if(values.Count == 0) { this.typedDictionary.Remove(key); } - - OnRemoved(new KeyValuePair(key, value)); return true; } else { return false; @@ -418,10 +388,6 @@ namespace Nuclex.Support.Collections { if(this.typedDictionary.TryGetValue(key, out values)) { this.count -= values.Count; this.typedDictionary.Remove(key); - - foreach(TValue value in values) { - OnRemoved(new KeyValuePair(key, value)); - } return values.Count; } else { return 0; @@ -430,33 +396,10 @@ namespace Nuclex.Support.Collections { /// Removes all items from the Dictionary public void Clear() { - OnClearing(); this.typedDictionary.Clear(); this.count = 0; - OnCleared(); } - /// Fires the 'ItemAdded' event - /// Item that has been added to the collection - protected virtual void OnAdded(KeyValuePair item) { } - - /// Fires the 'ItemRemoved' event - /// Item that has been removed from the collection - protected virtual void OnRemoved(KeyValuePair item) { } - - /// Fires the 'ItemReplaced' event - /// Item that was replaced in the collection - /// Item that the original was replaced by - protected virtual void OnReplaced( - KeyValuePair oldItem, KeyValuePair newItem - ) { } - - /// Fires the 'Clearing' event - protected virtual void OnClearing() { } - - /// Fires the 'Cleared' event - protected virtual void OnCleared() { } - /// The wrapped Dictionary under its type-safe interface private IDictionary> typedDictionary; /// The wrapped Dictionary under its object interface