From 7f2c2a6df32357cf89ca08cfb29aeb70be7c9cbf Mon Sep 17 00:00:00 2001 From: dahall Date: Tue, 22 Sep 2020 13:57:52 -0600 Subject: [PATCH] Added `Vanara.Collections.History` class --- Core/Collections/History.cs | 258 +++++++++++++++++++++++++++++ Core/Vanara.Core.csproj | 3 + UnitTests/Core/Collections/HistoryTests.cs | 77 +++++++++ UnitTests/Core/Core.csproj | 1 + 4 files changed, 339 insertions(+) create mode 100644 Core/Collections/History.cs create mode 100644 UnitTests/Core/Collections/HistoryTests.cs diff --git a/Core/Collections/History.cs b/Core/Collections/History.cs new file mode 100644 index 00000000..732c36d6 --- /dev/null +++ b/Core/Collections/History.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.IO; + +namespace Vanara.Collections +{ + /// Provides an interface for a history of items. + public interface IHistory : IEnumerable, INotifyCollectionChanged, INotifyPropertyChanged + { + /// Indicates the presence of items in the history that can be reached by calling . + /// if this instance can seek backward; otherwise, . + bool CanSeekBackward { get; } + + /// Indicates the presence of items in the history that can be reached by calling . + /// if this instance can seek forward; otherwise, . + bool CanSeekForward { get; } + + /// Gets the items in the history. + /// The number of items. + int Count { get; } + + /// Gets the value at a pointer within the history that represents the current item. + /// The current item. + /// There are no items in the history. + T Current { get; } + + /// Adds the specified item as the last history entry and sets the property to it's value. + /// The item to add to the history. + void Add(T item); + + /// Clears the history of all items. + void Clear(); + + /// Gets a specified number of items starting at a location within the history. + /// The maximum number of items to retrieve. The actual number of items returned may be less if not avaialable. + /// The reference point within the history at which to start fetching items. + /// A read-only list of items. + IReadOnlyList GetItems(int count, SeekOrigin origin); + + /// + /// Seeks through the history a given number of items starting at a known location within the history. This updates the property. + /// + /// The number of items to move. This value can be negative to search backwards or positive to search forwards. + /// The reference point within the history at which to start seeking. + /// The value at the new current pointer position. + /// Cannot seek on an empty history. + /// + /// The number of items to move cannot be accomplished given the number of items in the history and the seek origin. + /// + T Seek(int count, SeekOrigin origin); + + /// Seeks one position backwards. + /// The value at the new current pointer position. + T SeekBackward(); + + /// Seeks one position forwards. + /// The value at the new current pointer position. + T SeekForward(); + } + + /// Provides a history of items that lives efficiently in memory and whose size can change easily. + /// The type of item to hold. + /// + /// + /// + public class History : IHistory + { + private readonly LinkedList activeHistory = new LinkedList(); + private int capacity; + private LinkedListNode current; + + /// Initializes a new instance of the class with a capacity of 256 items. + public History() : this(256) + { + } + + /// Initializes a new instance of the class with a variable capacity. + /// The capacity. + public History(int capacity) => Capacity = capacity; + + /// Initializes a new instance of the class with a initial list of items. + /// The items with which to initialize the history. + public History(IEnumerable items) + { + foreach (var i in items) + activeHistory.AddLast(i); + capacity = activeHistory.Count; + GetCurrent(); + } + + /// Occurs when an item is added, removed, changed, moved, or the entire list is refreshed. + public event NotifyCollectionChangedEventHandler CollectionChanged; + + /// Occurs when a property value changes. + public event PropertyChangedEventHandler PropertyChanged; + + /// Indicates the presence of items in the history that can be reached by calling . + /// if this instance can seek backward; otherwise, . + public bool CanSeekBackward => GetCurrent()?.Previous != null; + + /// Indicates the presence of items in the history that can be reached by calling . + /// if this instance can seek forward; otherwise, . + public bool CanSeekForward => GetCurrent()?.Next != null; + + /// Gets or sets the capacity of the history, or the maximum number of items that it will hold. + /// The history's capacity. + public int Capacity + { + get => capacity; + set + { + if (capacity == value) return; + if (value < activeHistory.Count) + { + var list = new List(); + while (activeHistory.Count > value) + { + list.Add(activeHistory.First.Value); + activeHistory.RemoveFirst(); + } + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, list)); + } + capacity = value; + OnPropertyChanged(); + } + } + + /// Gets the value at a pointer within the history that represents the current item. + /// The current item. + /// There are no items in the history. + public T Current => !(GetCurrent() is null) ? current.Value : throw new InvalidOperationException("There are no items in the history."); + + /// Gets the items in the history. + /// The number of items. + public int Count => activeHistory.Count; + + /// Adds the specified item as the last history entry and sets the property to it's value. + /// The item to add to the history. + public void Add(T item) + { + var added = activeHistory.AddLast(item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + if (activeHistory.Count > Capacity) + { + var first = activeHistory.First; + activeHistory.RemoveFirst(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, first.Value)); + } + OnPropertyChanged(nameof(Count)); + current = added; + OnPropertyChanged(nameof(Current)); + } + + /// Clears the history of all items. + public void Clear() + { + if (Count == 0) return; + activeHistory.Clear(); + current = null; + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + OnPropertyChanged(nameof(Count)); + OnPropertyChanged(nameof(Current)); + } + + /// Returns an enumerator that iterates through the collection. + /// A that can be used to iterate through the collection. + public IEnumerator GetEnumerator() => activeHistory.GetEnumerator(); + + /// Gets a specified number of items starting at a location within the history. + /// The maximum number of items to retrieve. The actual number of items returned may be less if not avaialable. + /// The reference point within the history at which to start fetching items. + /// A read-only list of items. + public IReadOnlyList GetItems(int count, SeekOrigin origin) + { + if (count == 0 || Count == 0) return (IReadOnlyList)new List(0); + var ptr = origin switch + { + SeekOrigin.Begin => activeHistory.First, + SeekOrigin.Current => GetCurrent(), + SeekOrigin.End => activeHistory.Last, + _ => throw new ArgumentOutOfRangeException(nameof(origin)), + }; + var items = new List(); + for (int i = 0; i < Math.Abs(count) && ptr != null; i++) + { + items.Add(ptr.Value); + ptr = count > 0 ? ptr.Next : ptr.Previous; + } + return (IReadOnlyList)items; + } + + /// + /// Seeks through the history a given number of items starting at a known location within the history. This updates the property. + /// + /// The number of items to move. This value can be negative to search backwards or positive to search forwards. + /// The reference point within the history at which to start seeking. + /// The value at the new current pointer position. + /// Cannot seek on an empty history. + /// + /// The number of items to move cannot be accomplished given the number of items in the history and the seek origin. + /// + public T Seek(int count, SeekOrigin origin) + { + if (activeHistory.Count == 0) throw new InvalidOperationException("Cannot seek on an empty history."); + var ptr = origin switch + { + SeekOrigin.Begin => activeHistory.First, + SeekOrigin.Current => GetCurrent(), + SeekOrigin.End => activeHistory.Last, + _ => throw new ArgumentOutOfRangeException(nameof(origin)), + }; + for (int i = 0; i < Math.Abs(count); i++) + { + if (ptr is null) throw new ArgumentOutOfRangeException(nameof(count)); + ptr = count > 0 ? ptr.Next : ptr.Previous; + } + current = ptr; + OnPropertyChanged(nameof(Current)); + return Current; + } + + /// Seeks one position backwards. + /// The value at the new current pointer position. + public T SeekBackward() => CanSeekBackward ? Seek(-1, SeekOrigin.Current) : default; + + /// Seeks one position forwards. + /// The value at the new current pointer position. + public T SeekForward() => CanSeekForward ? Seek(1, SeekOrigin.Current) : default; + + /// Returns an enumerator that iterates through a collection. + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Raises the event. + /// The instance containing the event data. + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) => CollectionChanged?.Invoke(this, e); + + /// Raises the event. + /// Name of the property that has changed. + protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + private LinkedListNode GetCurrent() + { + if (current is null) + { + current = activeHistory.Last; + if (current != null) + OnPropertyChanged(nameof(Current)); + } + return current; + } + } +} \ No newline at end of file diff --git a/Core/Vanara.Core.csproj b/Core/Vanara.Core.csproj index e2c1a767..7650e092 100644 --- a/Core/Vanara.Core.csproj +++ b/Core/Vanara.Core.csproj @@ -29,6 +29,9 @@ CorrespondingAction, StringListPackMethod + + + 3.1.4 diff --git a/UnitTests/Core/Collections/HistoryTests.cs b/UnitTests/Core/Collections/HistoryTests.cs new file mode 100644 index 00000000..b486f8b2 --- /dev/null +++ b/UnitTests/Core/Collections/HistoryTests.cs @@ -0,0 +1,77 @@ +using NUnit.Framework; +using System; +using System.ComponentModel; +using System.Linq; +using Vanara.Extensions.Reflection; + +namespace Vanara.Collections.Tests +{ + [TestFixture()] + public class HistoryTests + { + [Test] + public void Test() + { + var history = new History(Enumerable.Range(1, 300)); + history.CollectionChanged += (s, e) => TestContext.WriteLine($"{e.Action}: New={e.NewItems?.Count ?? 0} Old={e.OldItems?.Count ?? 0}"); + history.PropertyChanged += GetPropVal; + Assert.That(history.Count, Is.EqualTo(300)); + + history.Capacity = 20; + Assert.That(history.Capacity, Is.EqualTo(20)); + Assert.That(history.Count, Is.EqualTo(20)); + Assert.That(history.Count(), Is.EqualTo(20)); + Assert.That(history.Current, Is.EqualTo(300)); + + Assert.That(history, Is.EquivalentTo(Enumerable.Range(281, 20))); + Assert.That(history.GetItems(10, System.IO.SeekOrigin.Begin), Is.EquivalentTo(Enumerable.Range(281, 10))); + Assert.That(history.GetItems(-10, System.IO.SeekOrigin.End), Is.EquivalentTo(Enumerable.Range(291, 10))); + + Assert.That(history.CanSeekForward, Is.False); + Assert.That(history.CanSeekBackward, Is.True); + + Assert.That(history.SeekBackward, Is.EqualTo(299)); + Assert.That(history.CanSeekForward, Is.True); + Assert.That(history.CanSeekBackward, Is.True); + + Assert.That(history.Seek(0, System.IO.SeekOrigin.Begin), Is.EqualTo(281)); + Assert.That(history.CanSeekForward, Is.True); + Assert.That(history.CanSeekBackward, Is.False); + + Assert.That(history.SeekForward, Is.EqualTo(282)); + Assert.That(history.CanSeekForward, Is.True); + Assert.That(history.CanSeekBackward, Is.True); + + Assert.That(history.Seek(9, System.IO.SeekOrigin.Current), Is.EqualTo(291)); + Assert.That(history.CanSeekForward, Is.True); + Assert.That(history.CanSeekBackward, Is.True); + + Assert.That(history.Seek(-5, System.IO.SeekOrigin.Current), Is.EqualTo(286)); + Assert.That(history.CanSeekForward, Is.True); + Assert.That(history.CanSeekBackward, Is.True); + + Assert.That(history.Seek(0, System.IO.SeekOrigin.End), Is.EqualTo(300)); + Assert.That(history.CanSeekForward, Is.False); + Assert.That(history.CanSeekBackward, Is.True); + + history.Add(301); + Assert.That(history.Current, Is.EqualTo(301)); + Assert.That(history.CanSeekForward, Is.False); + Assert.That(history.CanSeekBackward, Is.True); + Assert.That(history.Seek(0, System.IO.SeekOrigin.Begin), Is.EqualTo(282)); + + history.Clear(); + Assert.That(history.CanSeekForward, Is.False); + Assert.That(history.CanSeekBackward, Is.False); + Assert.That(() => history.Current, Throws.Exception); + + void GetPropVal(object sender, PropertyChangedEventArgs e) + { + var pi = history.GetType().GetProperty(e.PropertyName); + object obj = null; + try { obj = pi.GetValue(history); } catch (Exception ex) { obj = ex.GetType().Name; } + TestContext.WriteLine($"{e.PropertyName}={obj}"); + } + } + } +} \ No newline at end of file diff --git a/UnitTests/Core/Core.csproj b/UnitTests/Core/Core.csproj index c93c4a8c..7b84e3bb 100644 --- a/UnitTests/Core/Core.csproj +++ b/UnitTests/Core/Core.csproj @@ -43,6 +43,7 @@ +