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.");
+ }
+}