mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-22 23:26:40 -08:00
Change to file-scoped namespaces
This commit is contained in:
@@ -13,154 +13,153 @@ using FourNumbers = System.ValueTuple<float, float, float, float>;
|
|||||||
using static System.Environment;
|
using static System.Environment;
|
||||||
using static Games.Common.IO.Strings;
|
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<T>(
|
||||||
|
Func<IReadWrite, T> read,
|
||||||
|
string input,
|
||||||
|
string expectedOutput,
|
||||||
|
T expectedResult)
|
||||||
{
|
{
|
||||||
[Theory]
|
var inputReader = new StringReader(input + Environment.NewLine);
|
||||||
[MemberData(nameof(ReadStringTestCases))]
|
var outputWriter = new StringWriter();
|
||||||
[MemberData(nameof(Read2StringsTestCases))]
|
var io = new TextIO(inputReader, outputWriter);
|
||||||
[MemberData(nameof(ReadNumberTestCases))]
|
|
||||||
[MemberData(nameof(Read2NumbersTestCases))]
|
|
||||||
[MemberData(nameof(Read3NumbersTestCases))]
|
|
||||||
[MemberData(nameof(Read4NumbersTestCases))]
|
|
||||||
[MemberData(nameof(ReadNumbersTestCases))]
|
|
||||||
public void ReadingValuesHasExpectedPromptsAndResults<T>(
|
|
||||||
Func<IReadWrite, T> 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 result = read.Invoke(io);
|
var result = read.Invoke(io);
|
||||||
var output = outputWriter.ToString();
|
var output = outputWriter.ToString();
|
||||||
|
|
||||||
using var _ = new AssertionScope();
|
using var _ = new AssertionScope();
|
||||||
output.Should().Be(expectedOutput);
|
output.Should().Be(expectedOutput);
|
||||||
result.Should().BeEquivalentTo(expectedResult);
|
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<float>());
|
|
||||||
|
|
||||||
readNumbers.Should().Throw<ArgumentException>()
|
|
||||||
.WithMessage("'values' must have a non-zero length.*")
|
|
||||||
.WithParameterName("values");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TheoryData<Func<IReadWrite, string>, string, string, string> ReadStringTestCases()
|
|
||||||
{
|
|
||||||
static Func<IReadWrite, string> ReadString(string prompt) => io => io.ReadString(prompt);
|
|
||||||
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
{ ReadString("Name"), "", "Name? ", "" },
|
|
||||||
{ ReadString("prompt"), " foo ,bar", $"prompt? {ExtraInput}{NewLine}", "foo" }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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"),
|
|
||||||
$"{NewLine}x,y",
|
|
||||||
$"Input please? ?? {ExtraInput}{NewLine}",
|
|
||||||
("", "x")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TheoryData<Func<IReadWrite, float>, string, string, float> ReadNumberTestCases()
|
|
||||||
{
|
|
||||||
static Func<IReadWrite, float> 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<Func<IReadWrite, TwoNumbers>, string, string, TwoNumbers> Read2NumbersTestCases()
|
|
||||||
{
|
|
||||||
static Func<IReadWrite, TwoNumbers> 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<Func<IReadWrite, ThreeNumbers>, string, string, ThreeNumbers> Read3NumbersTestCases()
|
|
||||||
{
|
|
||||||
static Func<IReadWrite, ThreeNumbers> 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<Func<IReadWrite, FourNumbers>, string, string, FourNumbers> Read4NumbersTestCases()
|
|
||||||
{
|
|
||||||
static Func<IReadWrite, FourNumbers> 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<Func<IReadWrite, IReadOnlyList<float>>, string, string, float[]> ReadNumbersTestCases()
|
|
||||||
{
|
|
||||||
static Func<IReadWrite, IReadOnlyList<float>> 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 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadNumbers_ArrayEmpty_ThrowsArgumentException()
|
||||||
|
{
|
||||||
|
var io = new TextIO(new StringReader(""), new StringWriter());
|
||||||
|
|
||||||
|
Action readNumbers = () => io.ReadNumbers("foo", Array.Empty<float>());
|
||||||
|
|
||||||
|
readNumbers.Should().Throw<ArgumentException>()
|
||||||
|
.WithMessage("'values' must have a non-zero length.*")
|
||||||
|
.WithParameterName("values");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<Func<IReadWrite, string>, string, string, string> ReadStringTestCases()
|
||||||
|
{
|
||||||
|
static Func<IReadWrite, string> ReadString(string prompt) => io => io.ReadString(prompt);
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
{ ReadString("Name"), "", "Name? ", "" },
|
||||||
|
{ ReadString("prompt"), " foo ,bar", $"prompt? {ExtraInput}{NewLine}", "foo" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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"),
|
||||||
|
$"{NewLine}x,y",
|
||||||
|
$"Input please? ?? {ExtraInput}{NewLine}",
|
||||||
|
("", "x")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<Func<IReadWrite, float>, string, string, float> ReadNumberTestCases()
|
||||||
|
{
|
||||||
|
static Func<IReadWrite, float> 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<Func<IReadWrite, TwoNumbers>, string, string, TwoNumbers> Read2NumbersTestCases()
|
||||||
|
{
|
||||||
|
static Func<IReadWrite, TwoNumbers> 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<Func<IReadWrite, ThreeNumbers>, string, string, ThreeNumbers> Read3NumbersTestCases()
|
||||||
|
{
|
||||||
|
static Func<IReadWrite, ThreeNumbers> 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<Func<IReadWrite, FourNumbers>, string, string, FourNumbers> Read4NumbersTestCases()
|
||||||
|
{
|
||||||
|
static Func<IReadWrite, FourNumbers> 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<Func<IReadWrite, IReadOnlyList<float>>, string, string, float[]> ReadNumbersTestCases()
|
||||||
|
{
|
||||||
|
static Func<IReadWrite, IReadOnlyList<float>> 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 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,100 +8,99 @@ using Xunit;
|
|||||||
using static System.Environment;
|
using static System.Environment;
|
||||||
using static Games.Common.IO.Strings;
|
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;
|
_outputWriter = new StringWriter();
|
||||||
|
|
||||||
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<ArgumentOutOfRangeException>()
|
|
||||||
.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<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? ?? ?? ?? {ExtraInput}{NewLine}",
|
|
||||||
new[] { "1", "2", " a,b ", "", "", "d\"x" }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TheoryData<string, uint, string, string, float[]> 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 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadTokens_QuantityNeededZero_ThrowsArgumentException()
|
||||||
|
{
|
||||||
|
var sut = TokenReader.ForStrings(new TextIO(new StringReader(""), _outputWriter));
|
||||||
|
|
||||||
|
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(
|
||||||
|
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<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? ?? ?? ?? {ExtraInput}{NewLine}",
|
||||||
|
new[] { "1", "2", " a,b ", "", "", "d\"x" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<string, uint, string, string, float[]> 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 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +1,42 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
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]
|
var expected = new { String = value, IsNumber = isNumber, Number = number };
|
||||||
[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);
|
var token = new Token(value);
|
||||||
|
|
||||||
token.Should().BeEquivalentTo(expected);
|
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 }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,33 +2,32 @@ using System.Linq;
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
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]
|
var result = Tokenizer.ParseTokens(input);
|
||||||
[MemberData(nameof(TokenizerTestCases))]
|
|
||||||
public void ParseTokens_SplitsStringIntoExpectedTokens(string input, string[] expected)
|
|
||||||
{
|
|
||||||
var result = Tokenizer.ParseTokens(input);
|
|
||||||
|
|
||||||
result.Select(t => t.ToString()).Should().BeEquivalentTo(expected);
|
result.Select(t => t.ToString()).Should().BeEquivalentTo(expected);
|
||||||
}
|
|
||||||
|
|
||||||
public static TheoryData<string, string[]> 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" } }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public static TheoryData<string, string[]> 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" } }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Games.Common.IO
|
namespace Games.Common.IO;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An implementation of <see cref="IReadWrite" /> with input begin read for STDIN and output being written to
|
||||||
|
/// STDOUT.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ConsoleIO : TextIO
|
||||||
{
|
{
|
||||||
/// <summary>
|
public ConsoleIO()
|
||||||
/// An implementation of <see cref="IReadWrite" /> with input begin read for STDIN and output being written to
|
: base(Console.In, Console.Out)
|
||||||
/// STDOUT.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ConsoleIO : TextIO
|
|
||||||
{
|
{
|
||||||
public ConsoleIO()
|
|
||||||
: base(Console.In, Console.Out)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,68 @@
|
|||||||
namespace Games.Common.IO
|
namespace Games.Common.IO;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides for input and output of strings and numbers.
|
||||||
|
/// </summary>
|
||||||
|
public interface IReadWrite
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides for input and output of strings and numbers.
|
/// Reads a <see cref="float" /> value from input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IReadWrite
|
/// <param name="prompt">The text to display to prompt for the value.</param>
|
||||||
{
|
/// <returns>A <see cref="float" />, being the value entered.</returns>
|
||||||
/// <summary>
|
float ReadNumber(string prompt);
|
||||||
/// Reads a <see cref="float" /> value from input.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="prompt">The text to display to prompt for the value.</param>
|
|
||||||
/// <returns>A <see cref="float" />, being the value entered.</returns>
|
|
||||||
float ReadNumber(string prompt);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads 2 <see cref="float" /> values from input.
|
/// Reads 2 <see cref="float" /> values from input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="prompt">The text to display to prompt for the values.</param>
|
/// <param name="prompt">The text to display to prompt for the values.</param>
|
||||||
/// <returns>A <see cref="ValueTuple{float, float}" />, being the values entered.</returns>
|
/// <returns>A <see cref="ValueTuple{float, float}" />, being the values entered.</returns>
|
||||||
(float, float) Read2Numbers(string prompt);
|
(float, float) Read2Numbers(string prompt);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads 3 <see cref="float" /> values from input.
|
/// Reads 3 <see cref="float" /> values from input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="prompt">The text to display to prompt for the values.</param>
|
/// <param name="prompt">The text to display to prompt for the values.</param>
|
||||||
/// <returns>A <see cref="ValueTuple{float, float, float}" />, being the values entered.</returns>
|
/// <returns>A <see cref="ValueTuple{float, float, float}" />, being the values entered.</returns>
|
||||||
(float, float, float) Read3Numbers(string prompt);
|
(float, float, float) Read3Numbers(string prompt);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads 4 <see cref="float" /> values from input.
|
/// Reads 4 <see cref="float" /> values from input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="prompt">The text to display to prompt for the values.</param>
|
/// <param name="prompt">The text to display to prompt for the values.</param>
|
||||||
/// <returns>A <see cref="ValueTuple{float, float, float, float}" />, being the values entered.</returns>
|
/// <returns>A <see cref="ValueTuple{float, float, float, float}" />, being the values entered.</returns>
|
||||||
(float, float, float, float) Read4Numbers(string prompt);
|
(float, float, float, float) Read4Numbers(string prompt);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Read numbers from input to fill an array.
|
/// Read numbers from input to fill an array.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="prompt">The text to display to prompt for the values.</param>
|
/// <param name="prompt">The text to display to prompt for the values.</param>
|
||||||
/// <param name="values">A <see cref="float[]" /> to be filled with values from input.</param>
|
/// <param name="values">A <see cref="float[]" /> to be filled with values from input.</param>
|
||||||
void ReadNumbers(string prompt, float[] values);
|
void ReadNumbers(string prompt, float[] values);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a <see cref="string" /> value from input.
|
/// Reads a <see cref="string" /> value from input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="prompt">The text to display to prompt for the value.</param>
|
/// <param name="prompt">The text to display to prompt for the value.</param>
|
||||||
/// <returns>A <see cref="string" />, being the value entered.</returns>
|
/// <returns>A <see cref="string" />, being the value entered.</returns>
|
||||||
string ReadString(string prompt);
|
string ReadString(string prompt);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads 2 <see cref="string" /> values from input.
|
/// Reads 2 <see cref="string" /> values from input.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="prompt">The text to display to prompt for the values.</param>
|
/// <param name="prompt">The text to display to prompt for the values.</param>
|
||||||
/// <returns>A <see cref="ValueTuple{string, string}" />, being the values entered.</returns>
|
/// <returns>A <see cref="ValueTuple{string, string}" />, being the values entered.</returns>
|
||||||
(string, string) Read2Strings(string prompt);
|
(string, string) Read2Strings(string prompt);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes a <see cref="string" /> to output.
|
/// Writes a <see cref="string" /> to output.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The <see cref="string" /> to be written.</param>
|
/// <param name="message">The <see cref="string" /> to be written.</param>
|
||||||
void Write(string message);
|
void Write(string message);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes a <see cref="string" /> to output, followed by a new-line.
|
/// Writes a <see cref="string" /> to output, followed by a new-line.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The <see cref="string" /> to be written.</param>
|
/// <param name="message">The <see cref="string" /> to be written.</param>
|
||||||
void WriteLine(string message);
|
void WriteLine(string message);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -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";
|
||||||
internal const string NumberExpected = "!Number expected - retry input line";
|
}
|
||||||
internal const string ExtraInput = "!Extra input ignored";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,88 +3,87 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Games.Common.IO
|
namespace Games.Common.IO;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
/// <summary>
|
||||||
|
/// Implements <see cref="IReadWrite" /> with input read from a <see cref="TextReader" /> and output written to a
|
||||||
|
/// <see cref="TextWriter" />.
|
||||||
|
/// </summary>
|
||||||
|
public class TextIO : IReadWrite
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
private readonly TextReader _input;
|
||||||
/// <summary>
|
private readonly TextWriter _output;
|
||||||
/// Implements <see cref="IReadWrite" /> with input read from a <see cref="TextReader" /> and output written to a
|
private readonly TokenReader _stringTokenReader;
|
||||||
/// <see cref="TextWriter" />.
|
private readonly TokenReader _numberTokenReader;
|
||||||
/// </summary>
|
|
||||||
public class TextIO : IReadWrite
|
public TextIO(TextReader input, TextWriter output)
|
||||||
{
|
{
|
||||||
private readonly TextReader _input;
|
_input = input ?? throw new ArgumentNullException(nameof(input));
|
||||||
private readonly TextWriter _output;
|
_output = output ?? throw new ArgumentNullException(nameof(output));
|
||||||
private readonly TokenReader _stringTokenReader;
|
_stringTokenReader = TokenReader.ForStrings(this);
|
||||||
private readonly TokenReader _numberTokenReader;
|
_numberTokenReader = TokenReader.ForNumbers(this);
|
||||||
|
|
||||||
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<float> 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<string> ReadStrings(string prompt, uint quantityRequired) =>
|
|
||||||
_stringTokenReader.ReadTokens(prompt, quantityRequired).Select(t => t.String).ToList();
|
|
||||||
|
|
||||||
internal string ReadLine(string prompt)
|
|
||||||
{
|
|
||||||
Write(prompt + "? ");
|
|
||||||
return _input.ReadLine();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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<float> 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<string> ReadStrings(string prompt, uint quantityRequired) =>
|
||||||
|
_stringTokenReader.ReadTokens(prompt, quantityRequired).Select(t => t.String).ToList();
|
||||||
|
|
||||||
|
internal string ReadLine(string prompt)
|
||||||
|
{
|
||||||
|
Write(prompt + "? ");
|
||||||
|
return _input.ReadLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,60 +1,59 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
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);
|
public override string ToString() => String;
|
||||||
Number = (IsNumber, number) switch
|
|
||||||
{
|
internal class Builder
|
||||||
(false, _) => float.NaN,
|
{
|
||||||
(true, float.PositiveInfinity) => float.MaxValue,
|
private readonly StringBuilder _builder = new();
|
||||||
(true, float.NegativeInfinity) => float.MinValue,
|
private bool _isQuoted;
|
||||||
(true, _) => number
|
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 Builder SetIsQuoted()
|
||||||
public bool IsNumber { get; }
|
|
||||||
public float Number { get; }
|
|
||||||
|
|
||||||
public override string ToString() => String;
|
|
||||||
|
|
||||||
internal class Builder
|
|
||||||
{
|
{
|
||||||
private readonly StringBuilder _builder = new();
|
_isQuoted = true;
|
||||||
private bool _isQuoted;
|
return this;
|
||||||
private int _trailingWhiteSpaceCount;
|
}
|
||||||
|
|
||||||
public Builder Append(char character)
|
public Token Build()
|
||||||
{
|
{
|
||||||
_builder.Append(character);
|
if (!_isQuoted) { _builder.Length -= _trailingWhiteSpaceCount; }
|
||||||
|
return new Token(_builder.ToString());
|
||||||
_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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,79 +3,78 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
using static Games.Common.IO.Strings;
|
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<Token> _isTokenValid;
|
||||||
|
|
||||||
|
private TokenReader(TextIO io, Predicate<Token> isTokenValid)
|
||||||
{
|
{
|
||||||
private readonly TextIO _io;
|
_io = io;
|
||||||
private readonly Predicate<Token> _isTokenValid;
|
_isTokenValid = isTokenValid ?? (t => true);
|
||||||
|
}
|
||||||
|
|
||||||
private TokenReader(TextIO io, Predicate<Token> isTokenValid)
|
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)
|
||||||
{
|
{
|
||||||
_io = io;
|
throw new ArgumentOutOfRangeException(
|
||||||
_isTokenValid = isTokenValid ?? (t => true);
|
nameof(quantityNeeded),
|
||||||
|
$"'{nameof(quantityNeeded)}' must be greater than zero.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TokenReader ForStrings(TextIO io) => new(io, t => true);
|
var tokens = new List<Token>();
|
||||||
public static TokenReader ForNumbers(TextIO io) => new(io, t => t.IsNumber);
|
|
||||||
|
|
||||||
public IEnumerable<Token> ReadTokens(string prompt, uint quantityNeeded)
|
while (tokens.Count < quantityNeeded)
|
||||||
{
|
{
|
||||||
if (quantityNeeded == 0)
|
tokens.AddRange(ReadValidTokens(prompt, quantityNeeded - (uint)tokens.Count));
|
||||||
{
|
prompt = "?";
|
||||||
throw new ArgumentOutOfRangeException(
|
}
|
||||||
nameof(quantityNeeded),
|
|
||||||
$"'{nameof(quantityNeeded)}' must be greater than zero.");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Token> ReadValidTokens(string prompt, uint maxCount)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var tokensValid = true;
|
||||||
var tokens = new List<Token>();
|
var tokens = new List<Token>();
|
||||||
|
foreach (var token in ReadLineOfTokens(prompt, maxCount))
|
||||||
while(tokens.Count < quantityNeeded)
|
|
||||||
{
|
{
|
||||||
tokens.AddRange(ReadValidTokens(prompt, quantityNeeded - (uint)tokens.Count));
|
if (!_isTokenValid(token))
|
||||||
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))
|
_io.WriteLine(NumberExpected);
|
||||||
{
|
tokensValid = false;
|
||||||
_io.WriteLine(NumberExpected);
|
prompt = "";
|
||||||
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(ExtraInput);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return token;
|
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(ExtraInput);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,102 +1,101 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
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<char> _characters;
|
||||||
|
|
||||||
|
private Tokenizer(string input) => _characters = new Queue<char>(input);
|
||||||
|
|
||||||
|
public static IEnumerable<Token> ParseTokens(string input)
|
||||||
{
|
{
|
||||||
private const char Quote = '"';
|
if (input is null) { throw new ArgumentNullException(nameof(input)); }
|
||||||
private const char Separator = ',';
|
|
||||||
|
|
||||||
private readonly Queue<char> _characters;
|
return new Tokenizer(input).ParseTokens();
|
||||||
|
}
|
||||||
|
|
||||||
private Tokenizer(string input) => _characters = new Queue<char>(input);
|
private IEnumerable<Token> ParseTokens()
|
||||||
|
{
|
||||||
public static IEnumerable<Token> ParseTokens(string input)
|
while (true)
|
||||||
{
|
{
|
||||||
if (input is null) { throw new ArgumentNullException(nameof(input)); }
|
var (token, isLastToken) = Consume(_characters);
|
||||||
|
yield return token;
|
||||||
|
|
||||||
return new Tokenizer(input).ParseTokens();
|
if (isLastToken) { break; }
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<Token> ParseTokens()
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var (token, isLastToken) = Consume(_characters);
|
|
||||||
yield return token;
|
|
||||||
|
|
||||||
if (isLastToken) { break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public (Token, bool) Consume(Queue<char> 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public (Token, bool) Consume(Queue<char> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user