diff --git a/README.md b/README.md index bcfc7db..b23c84b 100644 --- a/README.md +++ b/README.md @@ -5,4 +5,9 @@ ### Collections - Bag : A List with fast removing but no consistency indexing - - Array2D : A generic 2d array + - Grid2 : A generic 2d array + - NamedBag : A dictionary with a string key associated to a generic value + +### Types + + - Size : A struct to represent a size with width and height diff --git a/src/Ugtk.Foster/Types/SizeExtensions.cs b/src/Ugtk.Foster/Types/SizeExtensions.cs new file mode 100644 index 0000000..adbe9e9 --- /dev/null +++ b/src/Ugtk.Foster/Types/SizeExtensions.cs @@ -0,0 +1,22 @@ +using Foster.Framework; +using System.Drawing; + +namespace Ugtk.Foster.Types; + +/// +/// Provides extension methods for the struct. +/// +public static class SizeExtensions +{ + /// + /// Converts a object to a object. + /// + /// The instance to convert. + /// + /// A object with the same width and height as the instance. + /// + public static Point2 ToPoint2(this Size size) + { + return new Point2(size.Width, size.Height); + } +} \ No newline at end of file diff --git a/src/Ugtk/Collections/Array2d.cs b/src/Ugtk/Collections/Grid2.cs similarity index 70% rename from src/Ugtk/Collections/Array2d.cs rename to src/Ugtk/Collections/Grid2.cs index 390d72a..41acac6 100644 --- a/src/Ugtk/Collections/Array2d.cs +++ b/src/Ugtk/Collections/Grid2.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Drawing; namespace Ugtk.Collections; @@ -8,7 +9,7 @@ namespace Ugtk.Collections; /// Store objects in a 2 dimensional array /// /// The type of object to store in the array -public sealed class Array2d : IEnumerable +public sealed class Grid2 : IEnumerable { private readonly T[] _items; @@ -39,17 +40,24 @@ public sealed class Array2d : IEnumerable /// /// The number of columns /// The number of rows - /// - public Array2d(int columnsCount, int rowsCount) + /// + public Grid2(int columnsCount, int rowsCount) { - if (columnsCount <= 0) throw new ArgumentException("The number of columns must be greater than zero", nameof(columnsCount)); - if (rowsCount <= 0) throw new ArgumentException("The number of rows must be greater than zero", nameof(rowsCount)); + if (columnsCount <= 0) throw new ArgumentOutOfRangeException("The number of columns must be greater than zero", nameof(columnsCount)); + if (rowsCount <= 0) throw new ArgumentOutOfRangeException("The number of rows must be greater than zero", nameof(rowsCount)); _items = new T[columnsCount * rowsCount]; ColumnsCount = columnsCount; RowsCount = rowsCount; } + /// + /// Contruct a new 2d array + /// + /// The size of the array + /// + public Grid2(Size size) : this(size.Width, size.Height) { } + /// /// Get the array enumerator /// diff --git a/src/Ugtk/Collections/NamedBag.cs b/src/Ugtk/Collections/NamedBag.cs new file mode 100644 index 0000000..9591de0 --- /dev/null +++ b/src/Ugtk/Collections/NamedBag.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; + +namespace Ugtk.Collections; + + +/// +/// Represents a collection of named items that supports fast retrieval by name. +/// +/// The type of items stored in the collection. +public sealed class NamedBag +{ + /// + /// Represents an item with a name and a value. + /// + private readonly struct NamedItem(string name, T? item) : IComparable> + { + public readonly string Name = name; + public readonly T Item = item!; + + /// + /// Initializes a new instance of the struct with a name. + /// + /// The name of the item. + public NamedItem(string name) : this(name, default) { } + + /// + /// Compares the current instance with another based on the name. + /// + /// The other named item to compare. + /// An integer indicating the relative order of the items. + public int CompareTo(NamedItem other) + { + return Name.CompareTo(other.Name); + } + } + + private readonly List> _items = []; + private readonly List _namesToBeAdded= []; + private readonly List _itemsToBeAdded = []; + + /// + /// Adds a new item to the collection. + /// + /// The name of the item. + /// The item to add. + /// Thrown if or is null. + /// Thrown if is empty or whitespace. + public void Add(string name, TItem item) + { + Add(name, item, true); + } + + /// + /// Adds a new item to the collection. + /// + /// The name of the item. + /// The item to add. + /// Indicates whether the collection should be sorted after adding the item. + /// Thrown if or is null. + /// Thrown if is empty or whitespace. + private void Add(string name, TItem item, bool mustSort) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Name cannot be empty or whitespace.", nameof(name)); + } + + ArgumentNullException.ThrowIfNull(item); + + _items.Add(new NamedItem(name, item)); + + if (mustSort) + { + _items.Sort(); + } + } + + /// + /// Begins a batch operation for adding items to the collection. + /// + public NamedBag BatchBeginAdd() + { + _namesToBeAdded.Clear(); + _itemsToBeAdded.Clear(); + + return this; + } + + /// + /// Adds a new item to the batch. + /// + /// The name of the item. + /// The item to add. + /// Thrown if or is null. + /// Thrown if is empty or whitespace. + public NamedBag BatchAdd(string name, TItem item) + { + _namesToBeAdded.Add(name); + _itemsToBeAdded.Add(item); + + return this; + } + + /// + /// Ends the batch operation and adds all items to the collection. + /// + public void BatchEndAdd() + { + for(int i = 0; i < _namesToBeAdded.Count; i++) + { + Add(_namesToBeAdded[i], _itemsToBeAdded[i], false); + } + + _items.Sort(); + } + + /// + /// Removes all items from the collection. + /// + public void Clear() + { + _items.Clear(); + } + + /// + /// Retrieves an item by its name. + /// + /// The name of the item to retrieve. + /// The item associated with the specified name. + /// Thrown if no item with the specified name exists. + public TItem GetValue(string name) + { + var index = _items.BinarySearch(new NamedItem(name)); + if (index < 0) + { + throw new KeyNotFoundException($"The named item `{name}` cannot be found."); + } + + return _items[index].Item; + } +} diff --git a/src/Ugtk/Types/Size.cs b/src/Ugtk/Types/Size.cs new file mode 100644 index 0000000..4fa4974 --- /dev/null +++ b/src/Ugtk/Types/Size.cs @@ -0,0 +1,85 @@ +using System; + +namespace Ugtk.Types; + +/// +/// Represents a size with a width and a height. +/// +public readonly struct Size : IEquatable +{ + /// + /// Gets the width of the size. + /// + public int Width { get; } + + /// + /// Gets the height of the size. + /// + public int Height { get; } + + /// + /// Initializes a new instance of the struct. + /// + /// The width of the size. + /// The height of the size. + /// When width or height is not positive + public Size(int width, int height) + { + if (width < 0) + { + throw new ArgumentOutOfRangeException(nameof(width), "Width must be non-negative."); + } + + if (height < 0) + { + throw new ArgumentOutOfRangeException(nameof(height), "Height must be non-negative."); + } + + Width = width; + Height = height; + } + + /// + /// Determines whether the current instance is equal to another instance. + /// + /// The other instance to compare. + /// + /// true if the two instances have the same and ; otherwise, false. + /// + public bool Equals(Size other) => (Width, Height) == (other.Width, other.Height); + + /// + /// Determines whether the current instance is equal to a specified object. + /// + /// The object to compare with the current instance. + /// + /// true if is a and is equal to the current instance; otherwise, false. + /// + public override bool Equals(object? obj) => obj is Size size && Equals(size); + + /// + /// Returns a hash code for the current instance. + /// + /// A hash code for the current instance. + public override int GetHashCode() => HashCode.Combine(Width, Height); + + /// + /// Determines whether two instances are equal. + /// + /// The first instance. + /// The second instance. + /// + /// true if the two instances are equal; otherwise, false. + /// + public static bool operator ==(Size left, Size right) => left.Equals(right); + + /// + /// Determines whether two instances are not equal. + /// + /// The first instance. + /// The second instance. + /// + /// true if the two instances are not equal; otherwise, false. + /// + public static bool operator !=(Size left, Size right) => !(left == right); +} \ No newline at end of file diff --git a/tests/Ugtk.Tests/Collections/Array2dTests.cs b/tests/Ugtk.Tests/Collections/Grid2Tests.cs similarity index 85% rename from tests/Ugtk.Tests/Collections/Array2dTests.cs rename to tests/Ugtk.Tests/Collections/Grid2Tests.cs index a8778a4..749e8ca 100644 --- a/tests/Ugtk.Tests/Collections/Array2dTests.cs +++ b/tests/Ugtk.Tests/Collections/Grid2Tests.cs @@ -6,7 +6,7 @@ namespace Ugtk.Tests.Collections { - public sealed class Array2dTests + public sealed class Grid2Tests { [Theory] [InlineData(3, 2)] @@ -15,7 +15,7 @@ public sealed class Array2dTests public void Constructor_ShouldInitializeArray_WithCorrectDimensions(int columns, int rows) { // Act - var array2d = new Array2d(columns, rows); + var array2d = new Grid2(columns, rows); // Assert Check.That(array2d.ColumnsCount).IsEqualTo(columns); @@ -29,7 +29,7 @@ public void Constructor_ShouldInitializeArray_WithCorrectDimensions(int columns, public void Constructor_ShouldThrowArgumentException_WhenDimensionsAreInvalid(int columns, int rows) { // Act & Assert - Check.ThatCode(() => new Array2d(columns, rows)) + Check.ThatCode(() => new Grid2(columns, rows)) .Throws(); } @@ -37,7 +37,7 @@ public void Constructor_ShouldThrowArgumentException_WhenDimensionsAreInvalid(in public void Indexer_ShouldSetAndGetValuesCorrectly() { // Arrange - var array2d = new Array2d(2, 2); + var array2d = new Grid2(2, 2); var value = "TestValue"; // Act @@ -51,7 +51,7 @@ public void Indexer_ShouldSetAndGetValuesCorrectly() public void Indexer_ShouldThrowIndexOutOfRangeException_WhenAccessingInvalidIndex() { // Arrange - var array2d = new Array2d(2, 2); + var array2d = new Grid2(2, 2); // Act & Assert Check.ThatCode(() => array2d[2, 2] = 42) @@ -63,7 +63,7 @@ public void GetEnumerator_ShouldEnumerateAllItems() { // Arrange - var array2d = new Array2d(2, 2); + var array2d = new Grid2(2, 2); array2d[0, 0] = 1; array2d[1, 0] = 2; array2d[0, 1] = 3; diff --git a/tests/Ugtk.Tests/Collections/NamedBagTests.cs b/tests/Ugtk.Tests/Collections/NamedBagTests.cs new file mode 100644 index 0000000..0d93be4 --- /dev/null +++ b/tests/Ugtk.Tests/Collections/NamedBagTests.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using Ugtk.Collections; +using NFluent; + +namespace Ugtk.Tests.Collections; + +public sealed class NamedBagTests +{ + [Fact] + public void Add_ShouldAddItemToCollection() + { + // Arrange + var bag = new NamedBag(); + + // Act + bag.Add("Item1", "Value1"); + + // Assert + Check.That(bag.GetValue("Item1")).IsEqualTo("Value1"); + } + + [Fact] + public void Add_ShouldThrowArgumentException_WhenNameIsEmpty() + { + // Arrange + var bag = new NamedBag(); + + // Act & Assert + Check.ThatCode(() => bag.Add("", "Value1")) + .Throws() + .WithMessage("Name cannot be empty or whitespace. (Parameter 'name')"); + } + + [Fact] + public void Add_ShouldThrowArgumentNullException_WhenItemIsNull() + { + // Arrange + var bag = new NamedBag(); + + // Act & Assert + Check.ThatCode(() => bag.Add("Item1", null)) + .Throws(); + } + + [Fact] + public void BatchBeginAdd_ShouldClearPendingItems() + { + // Arrange + var bag = new NamedBag(); + bag.BatchBeginAdd().BatchAdd("Item1", "Value1"); + + // Act + bag.BatchBeginAdd(); + + // Assert + Check.ThatCode(() => bag.GetValue("Item1")) + .Throws(); + } + + [Fact] + public void BatchAdd_ShouldAddItemsToBatch() + { + // Arrange + var bag = new NamedBag(); + bag.BatchBeginAdd(); + + // Act + bag.BatchAdd("Item1", "Value1").BatchAdd("Item2", "Value2"); + bag.BatchEndAdd(); + + // Assert + Check.That(bag.GetValue("Item1")).IsEqualTo("Value1"); + Check.That(bag.GetValue("Item2")).IsEqualTo("Value2"); + } + + [Fact] + public void BatchEndAdd_ShouldSortItemsAfterAdding() + { + // Arrange + var bag = new NamedBag(); + bag.BatchBeginAdd() + .BatchAdd("B", "ValueB") + .BatchAdd("A", "ValueA"); + + // Act + bag.BatchEndAdd(); + + // Assert + Check.That(bag.GetValue("A")).IsEqualTo("ValueA"); + Check.That(bag.GetValue("B")).IsEqualTo("ValueB"); + } + + [Fact] + public void Clear_ShouldRemoveAllItems() + { + // Arrange + var bag = new NamedBag(); + bag.Add("Item1", "Value1"); + + // Act + bag.Clear(); + + // Assert + Check.ThatCode(() => bag.GetValue("Item1")) + .Throws(); + } + + [Fact] + public void GetValue_ShouldReturnItem_WhenNameExists() + { + // Arrange + var bag = new NamedBag(); + bag.Add("Item1", "Value1"); + + // Act + var value = bag.GetValue("Item1"); + + // Assert + Check.That(value).IsEqualTo("Value1"); + } + + [Fact] + public void GetValue_ShouldThrowKeyNotFoundException_WhenNameDoesNotExist() + { + // Arrange + var bag = new NamedBag(); + + // Act & Assert + Check.ThatCode(() => bag.GetValue("NonExistent")) + .Throws() + .WithMessage("The named item `NonExistent` cannot be found."); + } +}