diff --git a/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs new file mode 100644 index 00000000..3b5de46d --- /dev/null +++ b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +using TwoStrings = System.ValueTuple; +using TwoNumbers = System.ValueTuple; +using ThreeNumbers = System.ValueTuple; +using FourNumbers = System.ValueTuple; + +using static System.Environment; +using static Games.Common.IO.Strings; + +namespace Games.Common.IO.TextIOTests +{ + public class ReadMethodTests + { + [Theory] + [MemberData(nameof(ReadStringTestCases))] + [MemberData(nameof(Read2StringsTestCases))] + [MemberData(nameof(ReadNumberTestCases))] + [MemberData(nameof(Read2NumbersTestCases))] + [MemberData(nameof(Read3NumbersTestCases))] + [MemberData(nameof(Read4NumbersTestCases))] + [MemberData(nameof(ReadNumbersTestCases))] + public void ReadingValuesHasExpectedPromptsAndResults( + Func read, + string input, + string expectedOutput, + T expectedResult) + { + var inputReader = new StringReader(input + Environment.NewLine); + var outputWriter = new StringWriter(); + var io = new TextIO(inputReader, outputWriter); + + var result = read.Invoke(io); + var output = outputWriter.ToString(); + + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Should().BeEquivalentTo(expectedResult); + } + + [Fact] + public void ReadNumbers_ArrayEmpty_ThrowsArgumentException() + { + var io = new TextIO(new StringReader(""), new StringWriter()); + + Action readNumbers = () => io.ReadNumbers("foo", Array.Empty()); + + readNumbers.Should().Throw() + .WithMessage("'values' must have a non-zero length.*") + .WithParameterName("values"); + } + + public static TheoryData, string, string, string> ReadStringTestCases() + { + static Func ReadString(string prompt) => io => io.ReadString(prompt); + + return new() + { + { ReadString("Name"), "", "Name? ", "" }, + { ReadString("prompt"), " foo ,bar", $"prompt? {ExtraInput}{NewLine}", "foo" } + }; + } + + public static TheoryData, string, string, TwoStrings> Read2StringsTestCases() + { + static Func Read2Strings(string prompt) => io => io.Read2Strings(prompt); + + return new() + { + { Read2Strings("2 strings"), ",", "2 strings? ", ("", "") }, + { + Read2Strings("Input please"), + $"{NewLine}x,y", + $"Input please? ?? {ExtraInput}{NewLine}", + ("", "x") + } + }; + } + + public static TheoryData, string, string, float> ReadNumberTestCases() + { + static Func ReadNumber(string prompt) => io => io.ReadNumber(prompt); + + return new() + { + { ReadNumber("Age"), $"{NewLine}42,", $"Age? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", 42 }, + { ReadNumber("Guess"), "3,4,5", $"Guess? {ExtraInput}{NewLine}", 3 } + }; + } + + public static TheoryData, string, string, TwoNumbers> Read2NumbersTestCases() + { + static Func Read2Numbers(string prompt) => io => io.Read2Numbers(prompt); + + return new() + { + { Read2Numbers("Point"), "3,4,5", $"Point? {ExtraInput}{NewLine}", (3, 4) }, + { + Read2Numbers("Foo"), + $"x,4,5{NewLine}4,5,x", + $"Foo? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (4, 5) + } + }; + } + + public static TheoryData, string, string, ThreeNumbers> Read3NumbersTestCases() + { + static Func Read3Numbers(string prompt) => io => io.Read3Numbers(prompt); + + return new() + { + { Read3Numbers("Point"), "3.2, 4.3, 5.4, 6.5", $"Point? {ExtraInput}{NewLine}", (3.2F, 4.3F, 5.4F) }, + { + Read3Numbers("Bar"), + $"x,4,5{NewLine}4,5,x{NewLine}6,7,8,y", + $"Bar? {NumberExpected}{NewLine}? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (6, 7, 8) + } + }; + } + + public static TheoryData, string, string, FourNumbers> Read4NumbersTestCases() + { + static Func Read4Numbers(string prompt) => io => io.Read4Numbers(prompt); + + return new() + { + { Read4Numbers("Point"), "3,4,5,6,7", $"Point? {ExtraInput}{NewLine}", (3, 4, 5, 6) }, + { + Read4Numbers("Baz"), + $"x,4,5,6{NewLine} 4, 5 , 6,7 ,x", + $"Baz? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (4, 5, 6, 7) + } + }; + } + + public static TheoryData>, string, string, float[]> ReadNumbersTestCases() + { + static Func> ReadNumbers(string prompt) => + io => + { + var numbers = new float[6]; + io.ReadNumbers(prompt, numbers); + return numbers; + }; + + return new() + { + { ReadNumbers("Primes"), "2, 3, 5, 7, 11, 13", $"Primes? ", new float[] { 2, 3, 5, 7, 11, 13 } }, + { + ReadNumbers("Qux"), + $"42{NewLine}3.141, 2.718{NewLine}3.0e8, 6.02e23{NewLine}9.11E-28", + $"Qux? ?? ?? ?? ", + new[] { 42, 3.141F, 2.718F, 3.0e8F, 6.02e23F, 9.11E-28F } + } + }; + } + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs index 883e069d..c5d24101 100644 --- a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs +++ b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs @@ -37,8 +37,8 @@ namespace Games.Common.IO /// Read numbers from input to fill an array. /// /// The text to display to prompt for the values. - /// A to be filled with values from input. - void ReadNumbers(string prompt, float[] numbers); + /// A to be filled with values from input. + void ReadNumbers(string prompt, float[] values); /// /// Reads a value from input. diff --git a/00_Common/dotnet/Games.Common/IO/TextIO.cs b/00_Common/dotnet/Games.Common/IO/TextIO.cs new file mode 100644 index 00000000..e210ec4e --- /dev/null +++ b/00_Common/dotnet/Games.Common/IO/TextIO.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Games.Common.IO +{ + /// + /// + /// Implements with input read from a and output written to a + /// . + /// + public class TextIO : IReadWrite + { + private readonly TextReader _input; + private readonly TextWriter _output; + private readonly TokenReader _stringTokenReader; + private readonly TokenReader _numberTokenReader; + + public TextIO(TextReader input, TextWriter output) + { + _input = input ?? throw new ArgumentNullException(nameof(input)); + _output = output ?? throw new ArgumentNullException(nameof(output)); + _stringTokenReader = TokenReader.ForStrings(this); + _numberTokenReader = TokenReader.ForNumbers(this); + } + + public float ReadNumber(string prompt) => ReadNumbers(prompt, 1)[0]; + + public (float, float) Read2Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 2); + return (numbers[0], numbers[1]); + } + + public (float, float, float) Read3Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 3); + return (numbers[0], numbers[1], numbers[2]); + } + + public (float, float, float, float) Read4Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 4); + return (numbers[0], numbers[1], numbers[2], numbers[3]); + } + + public void ReadNumbers(string prompt, float[] values) + { + if (values.Length == 0) + { + throw new ArgumentException($"'{nameof(values)}' must have a non-zero length.", nameof(values)); + } + + var numbers = _numberTokenReader.ReadTokens(prompt, (uint)values.Length).Select(t => t.Number).ToArray(); + numbers.CopyTo(values.AsSpan()); + } + + private IReadOnlyList ReadNumbers(string prompt, uint quantity) => + (quantity > 0) + ? _numberTokenReader.ReadTokens(prompt, quantity).Select(t => t.Number).ToList() + : throw new ArgumentOutOfRangeException( + nameof(quantity), + $"'{nameof(quantity)}' must be greater than zero."); + + public void Write(string value) => _output.Write(value); + + public void WriteLine(string value) => _output.WriteLine(value); + + public string ReadString(string prompt) + { + return ReadStrings(prompt, 1)[0]; + } + + public (string, string) Read2Strings(string prompt) + { + var values = ReadStrings(prompt, 2); + return (values[0], values[1]); + } + + private IReadOnlyList ReadStrings(string prompt, uint quantityRequired) => + _stringTokenReader.ReadTokens(prompt, quantityRequired).Select(t => t.String).ToList(); + + internal string ReadLine(string prompt) + { + Write(prompt + "? "); + return _input.ReadLine(); + } + } +} \ No newline at end of file