Add reading of number tokens

This commit is contained in:
Andrew Cooper
2022-02-11 22:16:03 +11:00
parent 5930a93f66
commit b60ba0d749
4 changed files with 113 additions and 26 deletions

View File

@@ -6,12 +6,14 @@ using FluentAssertions.Execution;
using Xunit;
using static System.Environment;
using TwoStrings = System.ValueTuple<string, string>;
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<T>(
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<string, uint, string, string, string[]> 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<Func<IReadWrite, TwoStrings>, string, string, TwoStrings> Read2StringsTestCases()
public static TheoryData<string, uint, string, string, float[]> ReadNumericTokensTestCases()
{
static Func<IReadWrite, TwoStrings> 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 }
}
};
}
}

View File

@@ -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<string, bool, float> 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 }
};
}
}

View File

@@ -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
{

View File

@@ -5,15 +5,21 @@ namespace Games.Common.IO
{
internal class TokenReader
{
private readonly TextIO _io;
private readonly Func<Token, bool> _isTokenValid;
private const string NumberExpected = "!Number expected - retry input line";
private const string ExtraInput = "!Extra input ignored";
public TokenReader(TextIO io, Func<Token, bool>? isTokenValid = null)
private readonly TextIO _io;
private readonly Predicate<Token> _isTokenValid;
private TokenReader(TextIO io, Predicate<Token> 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<Token> 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;
}