diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs index 34443dd2..3a4b9e9b 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs @@ -6,12 +6,14 @@ using FluentAssertions.Execution; using Xunit; using static System.Environment; -using TwoStrings = System.ValueTuple; namespace Games.Common.IO { public class TokenReaderTests { + const string NumberExpected = "!Number expected - retry input line"; + const string ExtraInput = "!Extra input ignored"; + private readonly StringWriter _outputWriter; public TokenReaderTests() @@ -22,7 +24,7 @@ namespace Games.Common.IO [Fact] public void ReadTokens_QuantityNeededZero_ThrowsArgumentException() { - var sut = CreateTokenReader(""); + var sut = TokenReader.ForStrings(new TextIO(new StringReader(""), _outputWriter)); Action readTokens = () => sut.ReadTokens("", 0); @@ -34,28 +36,41 @@ namespace Games.Common.IO [Theory] [MemberData(nameof(ReadTokensTestCases))] - public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( + public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( string prompt, uint tokenCount, string input, string expectedOutput, - T[] expectedResult) + string[] expectedResult) { - var sut = CreateTokenReader(input); + 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.ToString()).Should().BeEquivalentTo(expectedResult); + result.Select(t => t.String).Should().BeEquivalentTo(expectedResult); } - private TokenReader CreateTokenReader(string input) => - new TokenReader( - new TextIO( - new StringReader(input + NewLine), - _outputWriter)); + [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() { @@ -68,20 +83,26 @@ namespace Games.Common.IO "Foo", 6, $"1,2{NewLine}\" a,b \"{NewLine},\"\"c,d{NewLine}d\"x,e,f", - $"Foo? ?? ?? ?? !Extra input ingored{NewLine}", + $"Foo? ?? ?? ?? {ExtraInput}{NewLine}", new[] { "1", "2", " a,b ", "", "", "d\"x" } } }; } - public static TheoryData, string, string, TwoStrings> Read2StringsTestCases() + public static TheoryData ReadNumericTokensTestCases() { - static Func Read2Strings(string prompt) => io => io.Read2Strings(prompt); - return new() { - { Read2Strings("2 strings"), ",", "2 strings? ", ("", "") }, - { Read2Strings("Input please"), "aBc , DeF ", "Input please? ", ("aBc", "DeF") }, + { "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 new file mode 100644 index 00000000..f716ca78 --- /dev/null +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs @@ -0,0 +1,43 @@ +using FluentAssertions; +using Xunit; + +namespace Games.Common.IO +{ + public class TokenTests + { + [Theory] + [MemberData(nameof(TokenTestCases))] + public void Ctor_PopulatesProperties(string value, bool isNumber, float number) + { + var expected = new { String = value, IsNumber = isNumber, Number = number }; + + 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 } + }; + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/Token.cs b/00_Common/dotnet/Games.Common/IO/Token.cs index 4faea1fc..f1520c8e 100644 --- a/00_Common/dotnet/Games.Common/IO/Token.cs +++ b/00_Common/dotnet/Games.Common/IO/Token.cs @@ -1,17 +1,33 @@ using System.Text; +using System.Text.RegularExpressions; namespace Games.Common.IO { internal class Token { - private readonly string _value; + private static readonly Regex _numberPattern = new(@"^[+\-]?\d*(\.\d*)?([eE][+\-]?\d*)?"); - private Token(string value) + internal Token(string value) { - _value = value; + String = value; + + var match = _numberPattern.Match(String); + + 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() => _value; + public string String { get; } + public bool IsNumber { get; } + public float Number { get; } + + public override string ToString() => String; internal class Builder { diff --git a/00_Common/dotnet/Games.Common/IO/TokenReader.cs b/00_Common/dotnet/Games.Common/IO/TokenReader.cs index 0045433d..f2046dc3 100644 --- a/00_Common/dotnet/Games.Common/IO/TokenReader.cs +++ b/00_Common/dotnet/Games.Common/IO/TokenReader.cs @@ -5,15 +5,21 @@ namespace Games.Common.IO { internal class TokenReader { - private readonly TextIO _io; - private readonly Func _isTokenValid; + private const string NumberExpected = "!Number expected - retry input line"; + private const string ExtraInput = "!Extra input ignored"; - public TokenReader(TextIO io, Func? isTokenValid = null) + private readonly TextIO _io; + private readonly Predicate _isTokenValid; + + private TokenReader(TextIO io, Predicate isTokenValid) { _io = io; _isTokenValid = isTokenValid ?? (t => true); } + 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) @@ -44,8 +50,9 @@ namespace Games.Common.IO { if (!_isTokenValid(token)) { + _io.WriteLine(NumberExpected); tokensValid = false; - prompt = "?"; + prompt = ""; break; } @@ -64,7 +71,7 @@ namespace Games.Common.IO { if (++tokenCount > maxCount) { - _io.WriteLine("!Extra input ingored"); + _io.WriteLine(ExtraInput); break; }