mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-23 07:29:02 -08:00
Add token reader
This commit is contained in:
@@ -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") },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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" } }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
75
00_Common/dotnet/Games.Common/IO/TokenReader.cs
Normal file
75
00_Common/dotnet/Games.Common/IO/TokenReader.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user