From f2a16dc1a3f68e33914da8eef073ee239c720d9e Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 19 Apr 2024 11:39:16 -0600 Subject: [PATCH] Added Justifier class with robust unit tests --- Terminal.Gui/Drawing/Justification.cs | 170 ++++++++++++++++++++ Terminal.sln.DotSettings | 1 + UnitTests/Drawing/JustifierTests.cs | 220 ++++++++++++++++++++++++++ 3 files changed, 391 insertions(+) create mode 100644 Terminal.Gui/Drawing/Justification.cs create mode 100644 UnitTests/Drawing/JustifierTests.cs diff --git a/Terminal.Gui/Drawing/Justification.cs b/Terminal.Gui/Drawing/Justification.cs new file mode 100644 index 000000000..88d63fb29 --- /dev/null +++ b/Terminal.Gui/Drawing/Justification.cs @@ -0,0 +1,170 @@ +namespace Terminal.Gui; + +/// +/// Controls how items are justified within a container. Used by . +/// +public enum Justification +{ + /// + /// The items will be left-justified. + /// + Left, + + /// + /// The items will be right-justified. + /// + Right, + + /// + /// The items will be centered. + /// + Centered, + + /// + /// 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. + /// + Justified, + + RightJustified, + LeftJustified +} + +/// +/// Justifies items within a container based on the specified . +/// +public class Justifier +{ + /// + /// Justifies the within a container wide based on the specified + /// . + /// + /// + /// + /// + /// + 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; + } +} diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 3bc1fab5d..cdfa4823d 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -438,5 +438,6 @@ Concurrency Issue (?<=\W|^)(?<TAG>CONCURRENCY:)(\W|$)(.*) Warning + True True diff --git a/UnitTests/Drawing/JustifierTests.cs b/UnitTests/Drawing/JustifierTests.cs new file mode 100644 index 000000000..62f036aa0 --- /dev/null +++ b/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 { 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 { 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 { 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 { 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 (() => Justifier.Justify (sizes, Justification.Left, 100)); + } + + [Fact] + public void TestNegativeLengths () + { + int [] sizes = { -10, -20, -30 }; + Assert.Throws (() => 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 (); + } + +}