Browse Source

Added Justifier class with robust unit tests

Tig 1 year ago
parent
commit
f2a16dc1a3
3 changed files with 391 additions and 0 deletions
  1. 170 0
      Terminal.Gui/Drawing/Justification.cs
  2. 1 0
      Terminal.sln.DotSettings
  3. 220 0
      UnitTests/Drawing/JustifierTests.cs

+ 170 - 0
Terminal.Gui/Drawing/Justification.cs

@@ -0,0 +1,170 @@
+namespace Terminal.Gui;
+
+/// <summary>
+///     Controls how items are justified within a container. Used by <see cref="Justifier"/>.
+/// </summary>
+public enum Justification
+{
+    /// <summary>
+    ///     The items will be left-justified.
+    /// </summary>
+    Left,
+
+    /// <summary>
+    ///     The items will be right-justified.
+    /// </summary>
+    Right,
+
+    /// <summary>
+    ///     The items will be centered.
+    /// </summary>
+    Centered,
+
+    /// <summary>
+    ///     The items will be justified. Space will be added between the items such that the first item
+    ///     is at the start and the right side of the last item against the end.
+    /// </summary>
+    Justified,
+
+    RightJustified,
+    LeftJustified
+}
+
+/// <summary>
+///     Justifies items within a container based on the specified <see cref="Justification"/>.
+/// </summary>
+public class Justifier
+{
+    /// <summary>
+    ///     Justifies the <paramref name="sizes"/> within a container <see cref="totalSize"/> wide based on the specified
+    ///     <see cref="Justification"/>.
+    /// </summary>
+    /// <param name="sizes"></param>
+    /// <param name="justification"></param>
+    /// <param name="totalSize"></param>
+    /// <returns></returns>
+    public static int [] Justify (int [] sizes, Justification justification, int totalSize)
+    {
+        var positions = new int [sizes.Length];
+        int totalItemsSize = sizes.Sum ();
+
+        if (totalItemsSize > totalSize)
+        {
+            throw new ArgumentException ("The sum of the sizes is greater than the total size.");
+        }
+
+        switch (justification)
+        {
+            case Justification.Left:
+                var currentPosition = 0;
+
+                for (var i = 0; i < sizes.Length; i++)
+                {
+                    if (sizes [i] < 0)
+                    {
+                        throw new ArgumentException ("The size of an item cannot be negative.");
+                    }
+
+                    positions [i] = currentPosition;
+                    currentPosition += sizes [i];
+                }
+
+                break;
+            case Justification.Right:
+                currentPosition = totalSize - totalItemsSize;
+
+                for (var i = 0; i < sizes.Length; i++)
+                {
+                    if (sizes [i] < 0)
+                    {
+                        throw new ArgumentException ("The size of an item cannot be negative.");
+                    }
+
+                    positions [i] = currentPosition;
+                    currentPosition += sizes [i];
+                }
+
+                break;
+            case Justification.Centered:
+                currentPosition = (totalSize - totalItemsSize) / 2;
+
+                for (var i = 0; i < sizes.Length; i++)
+                {
+                    if (sizes [i] < 0)
+                    {
+                        throw new ArgumentException ("The size of an item cannot be negative.");
+                    }
+
+                    positions [i] = currentPosition;
+                    currentPosition += sizes [i];
+                }
+
+                break;
+
+            case Justification.Justified:
+                int spaceBetween = sizes.Length > 1 ? (totalSize - totalItemsSize) / (sizes.Length - 1) : 0;
+                int remainder = sizes.Length > 1 ? (totalSize - totalItemsSize) % (sizes.Length - 1) : 0;
+                currentPosition = 0;
+                for (var i = 0; i < sizes.Length; i++)
+                {
+                    if (sizes [i] < 0)
+                    {
+                        throw new ArgumentException ("The size of an item cannot be negative.");
+                    }
+                    positions [i] = currentPosition;
+                    int extraSpace = i < remainder ? 1 : 0;
+                    currentPosition += sizes [i] + spaceBetween + extraSpace;
+                }
+                break;
+
+            case Justification.LeftJustified:
+                if (sizes.Length > 1)
+                {
+                    int spaceBetweenLeft = totalSize - sizes.Sum () + 1; // +1 for the extra space
+                    currentPosition = 0;
+                    for (var i = 0; i < sizes.Length - 1; i++)
+                    {
+                        if (sizes [i] < 0)
+                        {
+                            throw new ArgumentException ("The size of an item cannot be negative.");
+                        }
+                        positions [i] = currentPosition;
+                        currentPosition += sizes [i] + 1; // +1 for the extra space
+                    }
+                    positions [sizes.Length - 1] = totalSize - sizes [sizes.Length - 1];
+                }
+                else if (sizes.Length == 1)
+                {
+                    positions [0] = 0;
+                }
+                break;
+
+            case Justification.RightJustified:
+                if (sizes.Length > 1)
+                {
+                    totalItemsSize = sizes.Sum ();
+                    int totalSpaces = totalSize - totalItemsSize;
+                    int bigSpace = totalSpaces - (sizes.Length - 2);
+
+                    positions [0] = 0; // first item is flush left
+                    positions [1] = sizes [0] + bigSpace; // second item has the big space before it
+
+                    // remaining items have one space between them
+                    for (var i = 2; i < sizes.Length; i++)
+                    {
+                        positions [i] = positions [i - 1] + sizes [i - 1] + 1;
+                    }
+                }
+                else if (sizes.Length == 1)
+                {
+                    positions [0] = 0; // single item is flush left
+                }
+                break;
+
+
+
+        }
+
+        return positions;
+    }
+}

+ 1 - 0
Terminal.sln.DotSettings

@@ -438,5 +438,6 @@
 	<s:String x:Key="/Default/PatternsAndTemplates/Todo/TodoPatterns/=B0C2F2A1AF61DA42BBF270980E3DCEF7/Name/@EntryValue">Concurrency Issue</s:String>
 	<s:String x:Key="/Default/PatternsAndTemplates/Todo/TodoPatterns/=B0C2F2A1AF61DA42BBF270980E3DCEF7/Pattern/@EntryValue">(?&lt;=\W|^)(?&lt;TAG&gt;CONCURRENCY:)(\W|$)(.*)</s:String>
 	<s:String x:Key="/Default/PatternsAndTemplates/Todo/TodoPatterns/=B0C2F2A1AF61DA42BBF270980E3DCEF7/TodoIconStyle/@EntryValue">Warning</s:String>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=Justifier/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=unsynchronized/@EntryIndexedValue">True</s:Boolean>
 </wpf:ResourceDictionary>

+ 220 - 0
UnitTests/Drawing/JustifierTests.cs

@@ -0,0 +1,220 @@
+
+using System.Text;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Terminal.Gui.DrawingTests;
+
+public class JustifierTests (ITestOutputHelper output)
+{
+
+    private readonly ITestOutputHelper _output = output;
+
+    [Fact]
+    public void TestLeftJustification ()
+    {
+        int [] sizes = { 10, 20, 30 };
+        var positions = Justifier.Justify (sizes, Justification.Left, 100);
+        Assert.Equal (new List<int> { 0, 10, 30 }, positions);
+    }
+
+    [Fact]
+    public void TestRightJustification ()
+    {
+        int [] sizes = { 10, 20, 30 };
+        var positions = Justifier.Justify (sizes, Justification.Right, 100);
+        Assert.Equal (new List<int> { 40, 50, 70 }, positions);
+    }
+
+    [Fact]
+    public void TestCenterJustification ()
+    {
+        int [] sizes = { 10, 20, 30 };
+        var positions = Justifier.Justify (sizes, Justification.Centered, 100);
+        Assert.Equal (new List<int> { 20, 30, 50 }, positions);
+    }
+
+    [Fact]
+    public void TestJustifiedJustification ()
+    {
+        int [] sizes = { 10, 20, 30 };
+        var positions = Justifier.Justify (sizes, Justification.Justified, 100);
+        Assert.Equal (new List<int> { 0, 30, 70 }, positions);
+    }
+
+    [Fact]
+    public void TestNoItems ()
+    {
+        int [] sizes = { };
+        var positions = Justifier.Justify (sizes, Justification.Left, 100);
+        Assert.Equal (new int [] { }, positions);
+    }
+
+    [Fact]
+    public void TestTenItems ()
+    {
+        int [] sizes = { 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 };
+        var positions = Justifier.Justify (sizes, Justification.Left, 100);
+        Assert.Equal (new int [] { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 }, positions);
+    }
+
+    [Fact]
+    public void TestZeroLengthItems ()
+    {
+        int [] sizes = { 0, 0, 0 };
+        var positions = Justifier.Justify (sizes, Justification.Left, 100);
+        Assert.Equal (new int [] { 0, 0, 0 }, positions);
+    }
+
+    [Fact]
+    public void TestLongItems ()
+    {
+        int [] sizes = { 1000, 2000, 3000 };
+        Assert.Throws<ArgumentException> (() => Justifier.Justify (sizes, Justification.Left, 100));
+    }
+
+    [Fact]
+    public void TestNegativeLengths ()
+    {
+        int [] sizes = { -10, -20, -30 };
+        Assert.Throws<ArgumentException> (() => Justifier.Justify (sizes, Justification.Left, 100));
+    }
+
+    [Theory]
+    [InlineData (Justification.Left, new int [] { 10, 20, 30 }, 100, new int [] { 0, 10, 30 })]
+    [InlineData (Justification.Left, new int [] { 33, 33, 33 }, 100, new int [] { 0, 33, 66 })]
+    [InlineData (Justification.Left, new int [] { 10 }, 101, new int [] { 0 })]
+    [InlineData (Justification.Left, new int [] { 10, 20 }, 101, new int [] { 0, 10 })]
+    [InlineData (Justification.Left, new int [] { 10, 20, 30 }, 101, new int [] { 0, 10, 30 })]
+    [InlineData (Justification.Left, new int [] { 10, 20, 30, 40 }, 101, new int [] { 0, 10, 30, 60 })]
+    [InlineData (Justification.Left, new int [] { 10, 20, 30, 40, 50 }, 151, new int [] { 0, 10, 30, 60, 100 })]
+
+    [InlineData (Justification.Right, new int [] { 10, 20, 30 }, 100, new int [] { 40, 50, 70 })]
+    [InlineData (Justification.Right, new int [] { 33, 33, 33 }, 100, new int [] { 1, 34, 67 })]
+    [InlineData (Justification.Right, new int [] { 10 }, 101, new int [] { 91 })]
+    [InlineData (Justification.Right, new int [] { 10, 20 }, 101, new int [] { 71, 81 })]
+    [InlineData (Justification.Right, new int [] { 10, 20, 30 }, 101, new int [] { 41, 51, 71 })]
+    [InlineData (Justification.Right, new int [] { 10, 20, 30, 40 }, 101, new int [] { 1, 11, 31, 61 })]
+    [InlineData (Justification.Right, new int [] { 10, 20, 30, 40, 50 }, 151, new int [] { 1, 11, 31, 61, 101 })]
+
+    [InlineData (Justification.Centered, new int [] { 10, 20, 30 }, 100, new int [] { 20, 30, 50 })]
+    [InlineData (Justification.Centered, new int [] { 33, 33, 33 }, 99, new int [] { 0, 33, 66 })]
+    [InlineData (Justification.Centered, new int [] { 33, 33, 33 }, 100, new int [] { 0, 33, 66 })]
+    [InlineData (Justification.Centered, new int [] { 33, 33, 33 }, 101, new int [] { 1, 34, 67 })]
+    [InlineData (Justification.Centered, new int [] { 33, 33, 33 }, 102, new int [] { 1, 34, 67 })]
+    [InlineData (Justification.Centered, new int [] { 33, 33, 33 }, 104, new int [] { 2, 35, 68 })]
+    [InlineData (Justification.Centered, new int [] { 10 }, 101, new int [] { 45 })]
+    [InlineData (Justification.Centered, new int [] { 10, 20 }, 101, new int [] { 35, 45 })]
+    [InlineData (Justification.Centered, new int [] { 10, 20, 30 }, 101, new int [] { 20, 30, 50 })]
+    [InlineData (Justification.Centered, new int [] { 10, 20, 30, 40 }, 101, new int [] { 0, 10, 30, 60 })]
+    [InlineData (Justification.Centered, new int [] { 10, 20, 30, 40, 50 }, 151, new int [] { 0, 10, 30, 60, 100 })]
+
+    [InlineData (Justification.Justified, new int [] { 10, 20, 30, 40, 50 }, 151, new int [] { 0, 11, 31, 61, 101 })]
+    [InlineData (Justification.Justified, new int [] { 10, 20, 30, 40 }, 101, new int [] { 0, 11, 31, 61 })]
+    [InlineData (Justification.Justified, new int [] { 10, 20, 30 }, 100, new int [] { 0, 30, 70 })]
+    [InlineData (Justification.Justified, new int [] { 10, 20, 30 }, 101, new int [] { 0, 31, 71 })]
+    [InlineData (Justification.Justified, new int [] { 33, 33, 33 }, 100, new int [] { 0, 34, 67 })]
+    [InlineData (Justification.Justified, new int [] { 11, 17, 23 }, 100, new int [] { 0, 36, 77 })]
+    [InlineData (Justification.Justified, new int [] { 1, 2, 3 }, 11, new int [] { 0, 4, 8 })]
+    [InlineData (Justification.Justified, new int [] { 10, 20 }, 101, new int [] { 0, 81 })]
+    [InlineData (Justification.Justified, new int [] { 10 }, 101, new int [] { 0 })]
+    [InlineData (Justification.Justified, new int [] { 3, 3, 3 }, 21, new int [] { 0, 9, 18 })]
+    [InlineData (Justification.Justified, new int [] { 3, 4, 5 }, 21, new int [] { 0, 8, 16 })]
+    [InlineData (Justification.Justified, new int [] { 3, 4, 5, 6 }, 18, new int [] { 0, 3, 7, 12 })]
+    [InlineData (Justification.Justified, new int [] { 3, 4, 5, 6 }, 19, new int [] { 0, 4, 8, 13 })]
+    [InlineData (Justification.Justified, new int [] { 3, 4, 5, 6 }, 20, new int [] { 0, 4, 9, 14 })]
+    [InlineData (Justification.Justified, new int [] { 3, 4, 5, 6 }, 21, new int [] { 0, 4, 9, 15 })]
+    [InlineData (Justification.Justified, new int [] { 6, 5, 4, 3 }, 22, new int [] { 0, 8, 14, 19 })]
+    [InlineData (Justification.Justified, new int [] { 6, 5, 4, 3 }, 23, new int [] { 0, 8, 15, 20, })]
+    [InlineData (Justification.Justified, new int [] { 6, 5, 4, 3 }, 24, new int [] { 0, 8, 15, 21 })]
+    [InlineData (Justification.Justified, new int [] { 6, 5, 4, 3 }, 25, new int [] { 0, 9, 16, 22 })]
+    [InlineData (Justification.Justified, new int [] { 6, 5, 4, 3 }, 26, new int [] { 0, 9, 17, 23 })]
+    [InlineData (Justification.Justified, new int [] { 6, 5, 4, 3 }, 31, new int [] { 0, 11, 20, 28 })]
+
+    [InlineData (Justification.LeftJustified, new int [] { 10, 20, 30 }, 100, new int [] { 0, 11, 70 })]
+    [InlineData (Justification.LeftJustified, new int [] { 33, 33, 33 }, 100, new int [] { 0, 34, 67 })]
+    [InlineData (Justification.LeftJustified, new int [] { 10 }, 101, new int [] { 0 })]
+    [InlineData (Justification.LeftJustified, new int [] { 10, 20 }, 101, new int [] { 0, 81 })]
+    [InlineData (Justification.LeftJustified, new int [] { 10, 20, 30 }, 101, new int [] { 0, 11, 71 })]
+    [InlineData (Justification.LeftJustified, new int [] { 10, 20, 30, 40 }, 101, new int [] { 0, 11, 32, 61 })]
+    [InlineData (Justification.LeftJustified, new int [] { 10, 20, 30, 40, 50 }, 151, new int [] { 0, 11, 32, 63, 101 })]
+    [InlineData (Justification.LeftJustified, new int [] { 3, 3, 3 }, 21, new int [] { 0, 4, 18 })]
+    [InlineData (Justification.LeftJustified, new int [] { 3, 4, 5 }, 21, new int [] { 0, 4, 16 })]
+
+    [InlineData (Justification.RightJustified, new int [] { 10, 20, 30 }, 100, new int [] { 0, 49, 70 })]
+    [InlineData (Justification.RightJustified, new int [] { 33, 33, 33 }, 100, new int [] { 0, 33, 67 })]
+    [InlineData (Justification.RightJustified, new int [] { 10 }, 101, new int [] { 0 })]
+    [InlineData (Justification.RightJustified, new int [] { 10, 20 }, 101, new int [] { 0, 81 })]
+    [InlineData (Justification.RightJustified, new int [] { 10, 20, 30 }, 101, new int [] { 0, 50, 71 })]
+    [InlineData (Justification.RightJustified, new int [] { 10, 20, 30, 40 }, 101, new int [] { 0, 9, 30, 61 })]
+    [InlineData (Justification.RightJustified, new int [] { 10, 20, 30, 40, 50 }, 151, new int [] { 0, 8, 29, 60, 101 })]
+    [InlineData (Justification.RightJustified, new int [] { 3, 3, 3 }, 21, new int [] { 0, 14, 18 })]
+    [InlineData (Justification.RightJustified, new int [] { 3, 4, 5 }, 21, new int [] { 0, 11, 16 })]
+
+    public void TestJustifications (Justification justification, int [] sizes, int totalSize, int [] expected)
+    {
+        var positions = Justifier.Justify (sizes, justification, totalSize);
+        AssertJustification (justification, sizes, totalSize, positions, expected);
+    }
+
+    public void AssertJustification (Justification justification, int [] sizes, int totalSize, int [] positions, int [] expected)
+    {
+        try
+        {
+            _output.WriteLine ($"Testing: {RenderJustification (justification, sizes, totalSize, expected)}");
+        }
+        catch (Exception e)
+        {
+            _output.WriteLine ($"Exception rendering expected: {e.Message}");
+            _output.WriteLine ($"Actual: {RenderJustification (justification, sizes, totalSize, positions)}");
+        }
+
+        if (!expected.SequenceEqual(positions))
+        {
+            _output.WriteLine ($"Expected: {RenderJustification (justification, sizes, totalSize, expected)}");
+            _output.WriteLine ($"Actual: {RenderJustification (justification, sizes, totalSize, positions)}");
+            Assert.Fail(" Expected and actual do not match");
+        }
+    }
+
+
+    public string RenderJustification (Justification justification, int [] sizes, int totalSize, int [] positions)
+    {
+        var output = new StringBuilder ();
+        output.AppendLine ($"Justification: {justification}, Positions: {string.Join (", ", positions)}, TotalSize: {totalSize}");
+        for (int i = 0; i <= totalSize / 10; i++)
+        {
+            output.Append (i.ToString ().PadRight (9) + " ");
+        }
+        output.AppendLine ();
+
+        for (int i = 0; i < totalSize; i++)
+        {
+            output.Append (i % 10);
+        }
+        output.AppendLine ();
+
+        var items = new char [totalSize];
+        for (int position = 0; position < positions.Length; position++)
+        {
+            try
+            {
+                for (int j = 0; j < sizes [position]; j++)
+                {
+                    items [positions [position] + j] = (position + 1).ToString () [0];
+                }
+            } catch(Exception e)
+            {
+                output.AppendLine ($"{e.Message} - position = {position}, positions[{position}]: {positions[position]}, sizes[{position}]: {sizes[position]}, totalSize: {totalSize}");
+                output.Append (new string (items).Replace ('\0', ' '));
+
+               Assert.Fail(e.Message + output.ToString ());
+            }
+        }
+
+        output.Append (new string (items).Replace ('\0', ' '));
+
+        return output.ToString ();
+    }
+
+}