diff --git a/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs index 3b5de46d..119bacd0 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs @@ -13,154 +13,153 @@ using FourNumbers = System.ValueTuple; using static System.Environment; using static Games.Common.IO.Strings; -namespace Games.Common.IO.TextIOTests +namespace Games.Common.IO.TextIOTests; + +public class ReadMethodTests { - 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) { - [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 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(); + 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 } - } - }; - } + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Should().BeEquivalentTo(expectedResult); } -} \ No newline at end of file + + [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 } + } + }; + } +} diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs index 65b33908..d4d8458e 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs @@ -8,100 +8,99 @@ using Xunit; using static System.Environment; using static Games.Common.IO.Strings; -namespace Games.Common.IO +namespace Games.Common.IO; + +public class TokenReaderTests { - public class TokenReaderTests + private readonly StringWriter _outputWriter; + + public TokenReaderTests() { - private readonly StringWriter _outputWriter; - - public TokenReaderTests() - { - _outputWriter = new StringWriter(); - } - - [Fact] - public void ReadTokens_QuantityNeededZero_ThrowsArgumentException() - { - var sut = TokenReader.ForStrings(new TextIO(new StringReader(""), _outputWriter)); - - Action readTokens = () => sut.ReadTokens("", 0); - - readTokens.Should().Throw() - .WithMessage("'quantityNeeded' must be greater than zero.*") - .WithParameterName("quantityNeeded"); - } - - - [Theory] - [MemberData(nameof(ReadTokensTestCases))] - public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( - string prompt, - uint tokenCount, - string input, - string expectedOutput, - string[] expectedResult) - { - var sut = TokenReader.ForStrings(new TextIO(new StringReader(input + NewLine), _outputWriter)); - - var result = sut.ReadTokens(prompt, tokenCount); - var output = _outputWriter.ToString(); - - using var _ = new AssertionScope(); - output.Should().Be(expectedOutput); - result.Select(t => t.String).Should().BeEquivalentTo(expectedResult); - } - - [Theory] - [MemberData(nameof(ReadNumericTokensTestCases))] - public void ReadTokens_Numeric_ReadingValuesHasExpectedPromptsAndResults( - string prompt, - uint tokenCount, - string input, - string expectedOutput, - float[] expectedResult) - { - var sut = TokenReader.ForNumbers(new TextIO(new StringReader(input + NewLine), _outputWriter)); - - var result = sut.ReadTokens(prompt, tokenCount); - var output = _outputWriter.ToString(); - - using var _ = new AssertionScope(); - output.Should().Be(expectedOutput); - result.Select(t => t.Number).Should().BeEquivalentTo(expectedResult); - } - - public static TheoryData ReadTokensTestCases() - { - return new() - { - { "Name", 1, "Bill", "Name? ", new[] { "Bill" } }, - { "Names", 2, " Bill , Bloggs ", "Names? ", new[] { "Bill", "Bloggs" } }, - { "Names", 2, $" Bill{NewLine}Bloggs ", "Names? ?? ", new[] { "Bill", "Bloggs" } }, - { - "Foo", - 6, - $"1,2{NewLine}\" a,b \"{NewLine},\"\"c,d{NewLine}d\"x,e,f", - $"Foo? ?? ?? ?? {ExtraInput}{NewLine}", - new[] { "1", "2", " a,b ", "", "", "d\"x" } - } - }; - } - - public static TheoryData ReadNumericTokensTestCases() - { - return new() - { - { "Age", 1, "23", "Age? ", new[] { 23F } }, - { "Constants", 2, " 3.141 , 2.71 ", "Constants? ", new[] { 3.141F, 2.71F } }, - { "Answer", 1, $"Forty-two{NewLine}42 ", $"Answer? {NumberExpected}{NewLine}? ", new[] { 42F } }, - { - "Foo", - 6, - $"1,2{NewLine}\" a,b \"{NewLine}3, 4 {NewLine}5.6,7,a, b", - $"Foo? ?? {NumberExpected}{NewLine}? ?? {ExtraInput}{NewLine}", - new[] { 1, 2, 3, 4, 5.6F, 7 } - } - }; - } + _outputWriter = new StringWriter(); } -} \ No newline at end of file + + [Fact] + public void ReadTokens_QuantityNeededZero_ThrowsArgumentException() + { + var sut = TokenReader.ForStrings(new TextIO(new StringReader(""), _outputWriter)); + + Action readTokens = () => sut.ReadTokens("", 0); + + readTokens.Should().Throw() + .WithMessage("'quantityNeeded' must be greater than zero.*") + .WithParameterName("quantityNeeded"); + } + + + [Theory] + [MemberData(nameof(ReadTokensTestCases))] + public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( + string prompt, + uint tokenCount, + string input, + string expectedOutput, + string[] expectedResult) + { + var sut = TokenReader.ForStrings(new TextIO(new StringReader(input + NewLine), _outputWriter)); + + var result = sut.ReadTokens(prompt, tokenCount); + var output = _outputWriter.ToString(); + + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Select(t => t.String).Should().BeEquivalentTo(expectedResult); + } + + [Theory] + [MemberData(nameof(ReadNumericTokensTestCases))] + public void ReadTokens_Numeric_ReadingValuesHasExpectedPromptsAndResults( + string prompt, + uint tokenCount, + string input, + string expectedOutput, + float[] expectedResult) + { + var sut = TokenReader.ForNumbers(new TextIO(new StringReader(input + NewLine), _outputWriter)); + + var result = sut.ReadTokens(prompt, tokenCount); + var output = _outputWriter.ToString(); + + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Select(t => t.Number).Should().BeEquivalentTo(expectedResult); + } + + public static TheoryData ReadTokensTestCases() + { + return new() + { + { "Name", 1, "Bill", "Name? ", new[] { "Bill" } }, + { "Names", 2, " Bill , Bloggs ", "Names? ", new[] { "Bill", "Bloggs" } }, + { "Names", 2, $" Bill{NewLine}Bloggs ", "Names? ?? ", new[] { "Bill", "Bloggs" } }, + { + "Foo", + 6, + $"1,2{NewLine}\" a,b \"{NewLine},\"\"c,d{NewLine}d\"x,e,f", + $"Foo? ?? ?? ?? {ExtraInput}{NewLine}", + new[] { "1", "2", " a,b ", "", "", "d\"x" } + } + }; + } + + public static TheoryData ReadNumericTokensTestCases() + { + return new() + { + { "Age", 1, "23", "Age? ", new[] { 23F } }, + { "Constants", 2, " 3.141 , 2.71 ", "Constants? ", new[] { 3.141F, 2.71F } }, + { "Answer", 1, $"Forty-two{NewLine}42 ", $"Answer? {NumberExpected}{NewLine}? ", new[] { 42F } }, + { + "Foo", + 6, + $"1,2{NewLine}\" a,b \"{NewLine}3, 4 {NewLine}5.6,7,a, b", + $"Foo? ?? {NumberExpected}{NewLine}? ?? {ExtraInput}{NewLine}", + new[] { 1, 2, 3, 4, 5.6F, 7 } + } + }; + } +} diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs index f716ca78..e91922d8 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs @@ -1,43 +1,42 @@ using FluentAssertions; using Xunit; -namespace Games.Common.IO +namespace Games.Common.IO; + +public class TokenTests { - public class TokenTests + [Theory] + [MemberData(nameof(TokenTestCases))] + public void Ctor_PopulatesProperties(string value, bool isNumber, float number) { - [Theory] - [MemberData(nameof(TokenTestCases))] - public void Ctor_PopulatesProperties(string value, bool isNumber, float number) - { - var expected = new { String = value, IsNumber = isNumber, Number = number }; + var expected = new { String = value, IsNumber = isNumber, Number = number }; - var token = new Token(value); + var token = new Token(value); - token.Should().BeEquivalentTo(expected); - } - - public static TheoryData TokenTestCases() => new() - { - { "", false, float.NaN }, - { "abcde", false, float.NaN }, - { "123 ", true, 123 }, - { "+42 ", true, 42 }, - { "-42 ", true, -42 }, - { "+3.14159 ", true, 3.14159F }, - { "-3.14159 ", true, -3.14159F }, - { " 123", false, float.NaN }, - { "1.2e4", true, 12000 }, - { "2.3e-5", true, 0.000023F }, - { "1e100", true, float.MaxValue }, - { "-1E100", true, float.MinValue }, - { "1E-100", true, 0 }, - { "-1e-100", true, 0 }, - { "100abc", true, 100 }, - { "1,2,3", true, 1 }, - { "42,a,b", true, 42 }, - { "1.2.3", true, 1.2F }, - { "12e.5", false, float.NaN }, - { "12e0.5", true, 12 } - }; + token.Should().BeEquivalentTo(expected); } -} \ No newline at end of file + + public static TheoryData TokenTestCases() => new() + { + { "", false, float.NaN }, + { "abcde", false, float.NaN }, + { "123 ", true, 123 }, + { "+42 ", true, 42 }, + { "-42 ", true, -42 }, + { "+3.14159 ", true, 3.14159F }, + { "-3.14159 ", true, -3.14159F }, + { " 123", false, float.NaN }, + { "1.2e4", true, 12000 }, + { "2.3e-5", true, 0.000023F }, + { "1e100", true, float.MaxValue }, + { "-1E100", true, float.MinValue }, + { "1E-100", true, 0 }, + { "-1e-100", true, 0 }, + { "100abc", true, 100 }, + { "1,2,3", true, 1 }, + { "42,a,b", true, 42 }, + { "1.2.3", true, 1.2F }, + { "12e.5", false, float.NaN }, + { "12e0.5", true, 12 } + }; +} diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs index a4e2f762..30e54b0f 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs @@ -2,33 +2,32 @@ using System.Linq; using FluentAssertions; using Xunit; -namespace Games.Common.IO +namespace Games.Common.IO; + +public class TokenizerTests { - public class TokenizerTests + [Theory] + [MemberData(nameof(TokenizerTestCases))] + public void ParseTokens_SplitsStringIntoExpectedTokens(string input, string[] expected) { - [Theory] - [MemberData(nameof(TokenizerTestCases))] - public void ParseTokens_SplitsStringIntoExpectedTokens(string input, string[] expected) - { - var result = Tokenizer.ParseTokens(input); + var result = Tokenizer.ParseTokens(input); - result.Select(t => t.ToString()).Should().BeEquivalentTo(expected); - } - - public static TheoryData TokenizerTestCases() => new() - { - { "", new[] { "" } }, - { "aBc", new[] { "aBc" } }, - { " Foo ", new[] { "Foo" } }, - { " \" Foo \" ", new[] { " Foo " } }, - { " \" Foo ", new[] { " Foo " } }, - { "\"\"abc", new[] { "" } }, - { "a\"\"bc", new[] { "a\"\"bc" } }, - { "\"\"", new[] { "" } }, - { ",", new[] { "", "" } }, - { " foo ,bar", new[] { "foo", "bar" } }, - { "\"a\"bc,de", new[] { "a" } }, - { "a\"b,\" c,d\", f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } } - }; + result.Select(t => t.ToString()).Should().BeEquivalentTo(expected); } -} \ No newline at end of file + + public static TheoryData TokenizerTestCases() => new() + { + { "", new[] { "" } }, + { "aBc", new[] { "aBc" } }, + { " Foo ", new[] { "Foo" } }, + { " \" Foo \" ", new[] { " Foo " } }, + { " \" Foo ", new[] { " Foo " } }, + { "\"\"abc", new[] { "" } }, + { "a\"\"bc", new[] { "a\"\"bc" } }, + { "\"\"", new[] { "" } }, + { ",", new[] { "", "" } }, + { " foo ,bar", new[] { "foo", "bar" } }, + { "\"a\"bc,de", new[] { "a" } }, + { "a\"b,\" c,d\", f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } } + }; +} diff --git a/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs b/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs index 842eb01c..a24238bf 100644 --- a/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs +++ b/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs @@ -1,16 +1,15 @@ using System; -namespace Games.Common.IO +namespace Games.Common.IO; + +/// +/// An implementation of with input begin read for STDIN and output being written to +/// STDOUT. +/// +public sealed class ConsoleIO : TextIO { - /// - /// An implementation of with input begin read for STDIN and output being written to - /// STDOUT. - /// - public sealed class ConsoleIO : TextIO + public ConsoleIO() + : base(Console.In, Console.Out) { - public ConsoleIO() - : base(Console.In, Console.Out) - { - } } -} \ 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 c5d24101..629b22aa 100644 --- a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs +++ b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs @@ -1,69 +1,68 @@ -namespace Games.Common.IO +namespace Games.Common.IO; + +/// +/// Provides for input and output of strings and numbers. +/// +public interface IReadWrite { /// - /// Provides for input and output of strings and numbers. + /// Reads a value from input. /// - public interface IReadWrite - { - /// - /// Reads a value from input. - /// - /// The text to display to prompt for the value. - /// A , being the value entered. - float ReadNumber(string prompt); + /// The text to display to prompt for the value. + /// A , being the value entered. + float ReadNumber(string prompt); - /// - /// Reads 2 values from input. - /// - /// The text to display to prompt for the values. - /// A , being the values entered. - (float, float) Read2Numbers(string prompt); + /// + /// Reads 2 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float) Read2Numbers(string prompt); - /// - /// Reads 3 values from input. - /// - /// The text to display to prompt for the values. - /// A , being the values entered. - (float, float, float) Read3Numbers(string prompt); + /// + /// Reads 3 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float, float) Read3Numbers(string prompt); - /// - /// Reads 4 values from input. - /// - /// The text to display to prompt for the values. - /// A , being the values entered. - (float, float, float, float) Read4Numbers(string prompt); + /// + /// Reads 4 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float, float, float) Read4Numbers(string prompt); - /// - /// 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[] values); + /// + /// 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[] values); - /// - /// Reads a value from input. - /// - /// The text to display to prompt for the value. - /// A , being the value entered. - string ReadString(string prompt); + /// + /// Reads a value from input. + /// + /// The text to display to prompt for the value. + /// A , being the value entered. + string ReadString(string prompt); - /// - /// Reads 2 values from input. - /// - /// The text to display to prompt for the values. - /// A , being the values entered. - (string, string) Read2Strings(string prompt); + /// + /// Reads 2 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (string, string) Read2Strings(string prompt); - /// - /// Writes a to output. - /// - /// The to be written. - void Write(string message); + /// + /// Writes a to output. + /// + /// The to be written. + void Write(string message); - /// - /// Writes a to output, followed by a new-line. - /// - /// The to be written. - void WriteLine(string message); - } -} \ No newline at end of file + /// + /// Writes a to output, followed by a new-line. + /// + /// The to be written. + void WriteLine(string message); +} diff --git a/00_Common/dotnet/Games.Common/IO/Strings.cs b/00_Common/dotnet/Games.Common/IO/Strings.cs index 3d955409..cf7d0f43 100644 --- a/00_Common/dotnet/Games.Common/IO/Strings.cs +++ b/00_Common/dotnet/Games.Common/IO/Strings.cs @@ -1,8 +1,7 @@ -namespace Games.Common.IO +namespace Games.Common.IO; + +internal static class Strings { - internal static class Strings - { - internal const string NumberExpected = "!Number expected - retry input line"; - internal const string ExtraInput = "!Extra input ignored"; - } -} \ No newline at end of file + internal const string NumberExpected = "!Number expected - retry input line"; + internal const string ExtraInput = "!Extra input ignored"; +} diff --git a/00_Common/dotnet/Games.Common/IO/TextIO.cs b/00_Common/dotnet/Games.Common/IO/TextIO.cs index e210ec4e..d4d80e54 100644 --- a/00_Common/dotnet/Games.Common/IO/TextIO.cs +++ b/00_Common/dotnet/Games.Common/IO/TextIO.cs @@ -3,88 +3,87 @@ using System.Collections.Generic; using System.IO; using System.Linq; -namespace Games.Common.IO +namespace Games.Common.IO; + +/// +/// +/// Implements with input read from a and output written to a +/// . +/// +public class TextIO : IReadWrite { - /// - /// - /// 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) { - 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(); - } + _input = input ?? throw new ArgumentNullException(nameof(input)); + _output = output ?? throw new ArgumentNullException(nameof(output)); + _stringTokenReader = TokenReader.ForStrings(this); + _numberTokenReader = TokenReader.ForNumbers(this); } -} \ No newline at end of file + + 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(); + } +} diff --git a/00_Common/dotnet/Games.Common/IO/Token.cs b/00_Common/dotnet/Games.Common/IO/Token.cs index f1520c8e..dc432e0c 100644 --- a/00_Common/dotnet/Games.Common/IO/Token.cs +++ b/00_Common/dotnet/Games.Common/IO/Token.cs @@ -1,60 +1,59 @@ using System.Text; using System.Text.RegularExpressions; -namespace Games.Common.IO +namespace Games.Common.IO; + +internal class Token { - internal class Token + private static readonly Regex _numberPattern = new(@"^[+\-]?\d*(\.\d*)?([eE][+\-]?\d*)?"); + + internal Token(string value) { - private static readonly Regex _numberPattern = new(@"^[+\-]?\d*(\.\d*)?([eE][+\-]?\d*)?"); + String = value; - internal Token(string value) + var match = _numberPattern.Match(String); + + IsNumber = float.TryParse(match.Value, out var number); + Number = (IsNumber, number) switch { - String = value; + (false, _) => float.NaN, + (true, float.PositiveInfinity) => float.MaxValue, + (true, float.NegativeInfinity) => float.MinValue, + (true, _) => number + }; + } - var match = _numberPattern.Match(String); + public string String { get; } + public bool IsNumber { get; } + public float Number { get; } - IsNumber = float.TryParse(match.Value, out var number); - Number = (IsNumber, number) switch - { - (false, _) => float.NaN, - (true, float.PositiveInfinity) => float.MaxValue, - (true, float.NegativeInfinity) => float.MinValue, - (true, _) => number - }; + public override string ToString() => String; + + internal class Builder + { + private readonly StringBuilder _builder = new(); + private bool _isQuoted; + private int _trailingWhiteSpaceCount; + + public Builder Append(char character) + { + _builder.Append(character); + + _trailingWhiteSpaceCount = char.IsWhiteSpace(character) ? _trailingWhiteSpaceCount + 1 : 0; + + return this; } - public string String { get; } - public bool IsNumber { get; } - public float Number { get; } - - public override string ToString() => String; - - internal class Builder + public Builder SetIsQuoted() { - private readonly StringBuilder _builder = new(); - private bool _isQuoted; - private int _trailingWhiteSpaceCount; + _isQuoted = true; + return this; + } - public Builder Append(char character) - { - _builder.Append(character); - - _trailingWhiteSpaceCount = char.IsWhiteSpace(character) ? _trailingWhiteSpaceCount + 1 : 0; - - return this; - } - - public Builder SetIsQuoted() - { - _isQuoted = true; - return this; - } - - public Token Build() - { - if (!_isQuoted) { _builder.Length -= _trailingWhiteSpaceCount; } - return new Token(_builder.ToString()); - } + public Token Build() + { + if (!_isQuoted) { _builder.Length -= _trailingWhiteSpaceCount; } + return new Token(_builder.ToString()); } } -} \ No newline at end of file +} diff --git a/00_Common/dotnet/Games.Common/IO/TokenReader.cs b/00_Common/dotnet/Games.Common/IO/TokenReader.cs index b7eb3774..3219c459 100644 --- a/00_Common/dotnet/Games.Common/IO/TokenReader.cs +++ b/00_Common/dotnet/Games.Common/IO/TokenReader.cs @@ -3,79 +3,78 @@ using System.Collections.Generic; using static Games.Common.IO.Strings; -namespace Games.Common.IO +namespace Games.Common.IO; + +internal class TokenReader { - internal class TokenReader + private readonly TextIO _io; + private readonly Predicate _isTokenValid; + + private TokenReader(TextIO io, Predicate isTokenValid) { - private readonly TextIO _io; - private readonly Predicate _isTokenValid; + _io = io; + _isTokenValid = isTokenValid ?? (t => true); + } - private TokenReader(TextIO io, Predicate isTokenValid) + public static TokenReader ForStrings(TextIO io) => new(io, t => true); + public static TokenReader ForNumbers(TextIO io) => new(io, t => t.IsNumber); + + public IEnumerable ReadTokens(string prompt, uint quantityNeeded) + { + if (quantityNeeded == 0) { - _io = io; - _isTokenValid = isTokenValid ?? (t => true); + throw new ArgumentOutOfRangeException( + nameof(quantityNeeded), + $"'{nameof(quantityNeeded)}' must be greater than zero."); } - public static TokenReader ForStrings(TextIO io) => new(io, t => true); - public static TokenReader ForNumbers(TextIO io) => new(io, t => t.IsNumber); + var tokens = new List(); - public IEnumerable ReadTokens(string prompt, uint quantityNeeded) + while (tokens.Count < quantityNeeded) { - if (quantityNeeded == 0) - { - throw new ArgumentOutOfRangeException( - nameof(quantityNeeded), - $"'{nameof(quantityNeeded)}' must be greater than zero."); - } + tokens.AddRange(ReadValidTokens(prompt, quantityNeeded - (uint)tokens.Count)); + prompt = "?"; + } + return tokens; + } + + private IEnumerable ReadValidTokens(string prompt, uint maxCount) + { + while (true) + { + var tokensValid = true; var tokens = new List(); - - while(tokens.Count < quantityNeeded) + foreach (var token in ReadLineOfTokens(prompt, maxCount)) { - tokens.AddRange(ReadValidTokens(prompt, quantityNeeded - (uint)tokens.Count)); - prompt = "?"; - } - - return tokens; - } - - private IEnumerable ReadValidTokens(string prompt, uint maxCount) - { - while (true) - { - var tokensValid = true; - var tokens = new List(); - foreach (var token in ReadLineOfTokens(prompt, maxCount)) + if (!_isTokenValid(token)) { - if (!_isTokenValid(token)) - { - _io.WriteLine(NumberExpected); - tokensValid = false; - prompt = ""; - break; - } - - tokens.Add(token); - } - - if (tokensValid) { return tokens; } - } - } - - private IEnumerable ReadLineOfTokens(string prompt, uint maxCount) - { - var tokenCount = 0; - - foreach (var token in Tokenizer.ParseTokens(_io.ReadLine(prompt))) - { - if (++tokenCount > maxCount) - { - _io.WriteLine(ExtraInput); + _io.WriteLine(NumberExpected); + tokensValid = false; + prompt = ""; break; } - yield return token; + tokens.Add(token); } + + if (tokensValid) { return tokens; } } } -} \ No newline at end of file + + private IEnumerable ReadLineOfTokens(string prompt, uint maxCount) + { + var tokenCount = 0; + + foreach (var token in Tokenizer.ParseTokens(_io.ReadLine(prompt))) + { + if (++tokenCount > maxCount) + { + _io.WriteLine(ExtraInput); + break; + } + + yield return token; + } + } +} diff --git a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs index 857b3331..fdaf829e 100644 --- a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs +++ b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs @@ -1,102 +1,101 @@ using System; using System.Collections.Generic; -namespace Games.Common.IO +namespace Games.Common.IO; + +internal class Tokenizer { - internal class Tokenizer + private const char Quote = '"'; + private const char Separator = ','; + + private readonly Queue _characters; + + private Tokenizer(string input) => _characters = new Queue(input); + + public static IEnumerable ParseTokens(string input) { - private const char Quote = '"'; - private const char Separator = ','; + if (input is null) { throw new ArgumentNullException(nameof(input)); } - private readonly Queue _characters; + return new Tokenizer(input).ParseTokens(); + } - private Tokenizer(string input) => _characters = new Queue(input); - - public static IEnumerable ParseTokens(string input) + private IEnumerable ParseTokens() + { + while (true) { - if (input is null) { throw new ArgumentNullException(nameof(input)); } + var (token, isLastToken) = Consume(_characters); + yield return token; - return new Tokenizer(input).ParseTokens(); - } - - private IEnumerable ParseTokens() - { - while (true) - { - var (token, isLastToken) = Consume(_characters); - yield return token; - - if (isLastToken) { break; } - } - } - - public (Token, bool) Consume(Queue characters) - { - var tokenBuilder = new Token.Builder(); - var state = ITokenizerState.LookForStartOfToken; - - while (characters.TryDequeue(out var character)) - { - (state, tokenBuilder) = state.Consume(character, tokenBuilder); - if (state is AtEndOfTokenState) { return (tokenBuilder.Build(), false); } - } - - return (tokenBuilder.Build(), true); - } - - private interface ITokenizerState - { - public static ITokenizerState LookForStartOfToken { get; } = new LookForStartOfTokenState(); - - (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder); - } - - private struct LookForStartOfTokenState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - character switch - { - Separator => (new AtEndOfTokenState(), tokenBuilder), - Quote => (new InQuotedTokenState(), tokenBuilder.SetIsQuoted()), - _ when char.IsWhiteSpace(character) => (this, tokenBuilder), - _ => (new InTokenState(), tokenBuilder.Append(character)) - }; - } - - private struct InTokenState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - character == Separator - ? (new AtEndOfTokenState(), tokenBuilder) - : (this, tokenBuilder.Append(character)); - } - - private struct InQuotedTokenState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - character == Quote - ? (new ExpectSeparatorState(), tokenBuilder) - : (this, tokenBuilder.Append(character)); - } - - private struct ExpectSeparatorState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - character == Separator - ? (new AtEndOfTokenState(), tokenBuilder) - : (new IgnoreRestOfLineState(), tokenBuilder); - } - - private struct IgnoreRestOfLineState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - (this, tokenBuilder); - } - - private struct AtEndOfTokenState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - throw new InvalidOperationException(); + if (isLastToken) { break; } } } -} \ No newline at end of file + + public (Token, bool) Consume(Queue characters) + { + var tokenBuilder = new Token.Builder(); + var state = ITokenizerState.LookForStartOfToken; + + while (characters.TryDequeue(out var character)) + { + (state, tokenBuilder) = state.Consume(character, tokenBuilder); + if (state is AtEndOfTokenState) { return (tokenBuilder.Build(), false); } + } + + return (tokenBuilder.Build(), true); + } + + private interface ITokenizerState + { + public static ITokenizerState LookForStartOfToken { get; } = new LookForStartOfTokenState(); + + (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder); + } + + private struct LookForStartOfTokenState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character switch + { + Separator => (new AtEndOfTokenState(), tokenBuilder), + Quote => (new InQuotedTokenState(), tokenBuilder.SetIsQuoted()), + _ when char.IsWhiteSpace(character) => (this, tokenBuilder), + _ => (new InTokenState(), tokenBuilder.Append(character)) + }; + } + + private struct InTokenState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Separator + ? (new AtEndOfTokenState(), tokenBuilder) + : (this, tokenBuilder.Append(character)); + } + + private struct InQuotedTokenState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Quote + ? (new ExpectSeparatorState(), tokenBuilder) + : (this, tokenBuilder.Append(character)); + } + + private struct ExpectSeparatorState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Separator + ? (new AtEndOfTokenState(), tokenBuilder) + : (new IgnoreRestOfLineState(), tokenBuilder); + } + + private struct IgnoreRestOfLineState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + (this, tokenBuilder); + } + + private struct AtEndOfTokenState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + throw new InvalidOperationException(); + } +}