Add token reader

This commit is contained in:
Andrew Cooper
2022-02-10 22:40:12 +11:00
parent 25c8dad512
commit 3b42ffd18d
4 changed files with 177 additions and 8 deletions

View File

@@ -0,0 +1,88 @@
using System;
using System.IO;
using System.Linq;
using FluentAssertions;
using FluentAssertions.Execution;
using Xunit;
using static System.Environment;
using TwoStrings = System.ValueTuple<string, string>;
namespace Games.Common.IO
{
public class TokenReaderTests
{
private readonly StringWriter _outputWriter;
public TokenReaderTests()
{
_outputWriter = new StringWriter();
}
[Fact]
public void ReadTokens_QuantityNeededZero_ThrowsArgumentException()
{
var sut = CreateTokenReader("");
Action readTokens = () => sut.ReadTokens("", 0);
readTokens.Should().Throw<ArgumentOutOfRangeException>()
.WithMessage("'quantityNeeded' must be greater than zero.*")
.WithParameterName("quantityNeeded");
}
[Theory]
[MemberData(nameof(ReadTokensTestCases))]
public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults<T>(
string prompt,
uint tokenCount,
string input,
string expectedOutput,
T[] expectedResult)
{
var sut = CreateTokenReader(input);
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);
}
private TokenReader CreateTokenReader(string input) =>
new TokenReader(
new TextIO(
new StringReader(input + NewLine),
_outputWriter));
public static TheoryData<string, uint, string, string, string[]> 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? ?? ?? ?? !Extra input ingored{NewLine}",
new[] { "1", "2", " a,b ", "", "", "d\"x" }
}
};
}
public static TheoryData<Func<IReadWrite, TwoStrings>, string, string, TwoStrings> Read2StringsTestCases()
{
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") },
};
}
}
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using FluentAssertions; using FluentAssertions;
using Xunit; using Xunit;
@@ -11,7 +12,7 @@ namespace Games.Common.IO
{ {
var result = Tokenizer.ParseTokens(input); var result = Tokenizer.ParseTokens(input);
result.Should().BeEquivalentTo(expected); result.Select(t => t.ToString()).Should().BeEquivalentTo(expected);
} }
public static TheoryData<string, string[]> TokenizerTestCases() => new() public static TheoryData<string, string[]> TokenizerTestCases() => new()
@@ -26,8 +27,8 @@ namespace Games.Common.IO
{ "\"\"", new[] { "" } }, { "\"\"", new[] { "" } },
{ ",", new[] { "", "" } }, { ",", new[] { "", "" } },
{ " foo ,bar", new[] { "foo", "bar" } }, { " foo ,bar", new[] { "foo", "bar" } },
{ "\"\"bc,de", new[] { "", "de" } }, { "\"a\"bc,de", new[] { "a" } },
{ "a\"b,\" c,d\"e, f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } } { "a\"b,\" c,d\", f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } }
}; };
} }
} }

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
namespace Games.Common.IO
{
internal class TokenReader
{
private readonly TextIO _io;
private readonly Func<Token, bool> _isTokenValid;
public TokenReader(TextIO io, Func<Token, bool>? isTokenValid = null)
{
_io = io;
_isTokenValid = isTokenValid ?? (t => true);
}
public IEnumerable<Token> ReadTokens(string prompt, uint quantityNeeded)
{
if (quantityNeeded == 0)
{
throw new ArgumentOutOfRangeException(
nameof(quantityNeeded),
$"'{nameof(quantityNeeded)}' must be greater than zero.");
}
var tokens = new List<Token>();
while(tokens.Count < quantityNeeded)
{
tokens.AddRange(ReadValidTokens(prompt, quantityNeeded - (uint)tokens.Count));
prompt = "?";
}
return tokens;
}
private IEnumerable<Token> ReadValidTokens(string prompt, uint maxCount)
{
while (true)
{
var tokensValid = true;
var tokens = new List<Token>();
foreach (var token in ReadLineOfTokens(prompt, maxCount))
{
if (!_isTokenValid(token))
{
tokensValid = false;
prompt = "?";
break;
}
tokens.Add(token);
}
if (tokensValid) { return tokens; }
}
}
private IEnumerable<Token> ReadLineOfTokens(string prompt, uint maxCount)
{
var tokenCount = 0;
foreach (var token in Tokenizer.ParseTokens(_io.ReadLine(prompt)))
{
if (++tokenCount > maxCount)
{
_io.WriteLine("!Extra input ingored");
break;
}
yield return token;
}
}
}
}

View File

@@ -12,14 +12,14 @@ namespace Games.Common.IO
private Tokenizer(string input) => _characters = new Queue<char>(input); private Tokenizer(string input) => _characters = new Queue<char>(input);
public static IEnumerable<string> ParseTokens(string input) public static IEnumerable<Token> ParseTokens(string input)
{ {
if (input is null) { throw new ArgumentNullException(nameof(input)); } if (input is null) { throw new ArgumentNullException(nameof(input)); }
return new Tokenizer(input).ParseTokens(); return new Tokenizer(input).ParseTokens();
} }
private IEnumerable<string> ParseTokens() private IEnumerable<Token> ParseTokens()
{ {
while (true) while (true)
{ {
@@ -72,13 +72,18 @@ namespace Games.Common.IO
private struct InQuotedTokenState : ITokenizerState private struct InQuotedTokenState : ITokenizerState
{ {
public (ITokenizerState, Token) Consume(char character, Token token) => public (ITokenizerState, Token) Consume(char character, Token token) =>
character == Quote ? (new LookForSeparatorState(), token) : (this, token.Append(character)); character == Quote ? (new ExpectSeparatorState(), token) : (this, token.Append(character));
} }
private struct LookForSeparatorState : ITokenizerState private struct ExpectSeparatorState : ITokenizerState
{ {
public (ITokenizerState, Token) Consume(char character, Token token) => public (ITokenizerState, Token) Consume(char character, Token token) =>
(character == Separator ? new AtEndOfTokenState() : this, token); character == Separator ? (new AtEndOfTokenState(), token) : (new IgnoreRestOfLineState(), token);
}
private struct IgnoreRestOfLineState : ITokenizerState
{
public (ITokenizerState, Token) Consume(char character, Token token) => (this, token);
} }
private struct AtEndOfTokenState : ITokenizerState private struct AtEndOfTokenState : ITokenizerState