mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-21 23:00:43 -08:00
Add TextIO implemnetation of IReadWrite
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using FluentAssertions;
|
||||||
|
using FluentAssertions.Execution;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
using TwoStrings = System.ValueTuple<string, string>;
|
||||||
|
using TwoNumbers = System.ValueTuple<float, float>;
|
||||||
|
using ThreeNumbers = System.ValueTuple<float, float, float>;
|
||||||
|
using FourNumbers = System.ValueTuple<float, float, float, float>;
|
||||||
|
|
||||||
|
using static System.Environment;
|
||||||
|
using static Games.Common.IO.Strings;
|
||||||
|
|
||||||
|
namespace Games.Common.IO.TextIOTests
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var inputReader = new StringReader(input + Environment.NewLine);
|
||||||
|
var outputWriter = new StringWriter();
|
||||||
|
var io = new TextIO(inputReader, outputWriter);
|
||||||
|
|
||||||
|
var result = read.Invoke(io);
|
||||||
|
var output = outputWriter.ToString();
|
||||||
|
|
||||||
|
using var _ = new AssertionScope();
|
||||||
|
output.Should().Be(expectedOutput);
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,8 +37,8 @@ namespace Games.Common.IO
|
|||||||
/// 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="numbers">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[] numbers);
|
void ReadNumbers(string prompt, float[] values);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a <see cref="string" /> value from input.
|
/// Reads a <see cref="string" /> value from input.
|
||||||
|
|||||||
90
00_Common/dotnet/Games.Common/IO/TextIO.cs
Normal file
90
00_Common/dotnet/Games.Common/IO/TextIO.cs
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
private readonly TextReader _input;
|
||||||
|
private readonly TextWriter _output;
|
||||||
|
private readonly TokenReader _stringTokenReader;
|
||||||
|
private readonly TokenReader _numberTokenReader;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user