From b8a63c0d5ce7a8fad9e91f3ddc777c58734d9926 Mon Sep 17 00:00:00 2001 From: Oliver Booth Date: Tue, 11 Apr 2023 22:27:31 +0100 Subject: [PATCH] feat: add float/double support for WriteNoAlloc and WriteLineNoAlloc --- X10D.Tests/src/IO/TextWriterTests.Double.cs | 131 ++++++++++++++++++ X10D.Tests/src/IO/TextWriterTests.Single.cs | 131 ++++++++++++++++++ ...riterExtensions.WriteLineNoAlloc.Double.cs | 83 +++++++++++ ...riterExtensions.WriteLineNoAlloc.Single.cs | 83 +++++++++++ ...extWriterExtensions.WriteNoAlloc.Double.cs | 91 ++++++++++++ ...extWriterExtensions.WriteNoAlloc.Single.cs | 91 ++++++++++++ 6 files changed, 610 insertions(+) create mode 100644 X10D.Tests/src/IO/TextWriterTests.Double.cs create mode 100644 X10D.Tests/src/IO/TextWriterTests.Single.cs create mode 100644 X10D/src/IO/TextWriterExtensions.WriteLineNoAlloc.Double.cs create mode 100644 X10D/src/IO/TextWriterExtensions.WriteLineNoAlloc.Single.cs create mode 100644 X10D/src/IO/TextWriterExtensions.WriteNoAlloc.Double.cs create mode 100644 X10D/src/IO/TextWriterExtensions.WriteNoAlloc.Single.cs diff --git a/X10D.Tests/src/IO/TextWriterTests.Double.cs b/X10D.Tests/src/IO/TextWriterTests.Double.cs new file mode 100644 index 0000000..6f6ed7f --- /dev/null +++ b/X10D.Tests/src/IO/TextWriterTests.Double.cs @@ -0,0 +1,131 @@ +using System.Globalization; +using System.Text; +using NUnit.Framework; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class TextWriterTests +{ + [Test] + public void WriteNoAlloc_ShouldThrowArgumentNullException_GivenDouble_AndNullWriter() + { + TextWriter writer = null!; + Assert.Throws(() => writer.WriteNoAlloc(420.0)); + Assert.Throws(() => writer.WriteNoAlloc(420.0, "N0")); + Assert.Throws(() => writer.WriteNoAlloc(420.0, "N0", null)); + } + + [Test] + public void WriteNoAlloc_ShouldThrowObjectDisposedException_GivenDouble_AndDisposedStream() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream, Encoding.UTF8); + writer.Dispose(); + stream.Dispose(); + + Assert.Throws(() => writer.WriteNoAlloc(420.0)); + Assert.Throws(() => writer.WriteNoAlloc(420.0, "N0")); + Assert.Throws(() => writer.WriteNoAlloc(420.0, "N0", null)); + } + + [Test] + public void WriteLineNoAlloc_ShouldThrowArgumentNullException_GivenDouble_AndNullWriter() + { + TextWriter writer = null!; + Assert.Throws(() => writer.WriteLineNoAlloc(420.0)); + Assert.Throws(() => writer.WriteLineNoAlloc(420.0, "N0")); + Assert.Throws(() => writer.WriteLineNoAlloc(420.0, "N0", null)); + } + + [Test] + public void WriteLineNoAlloc_ShouldThrowObjectDisposedException_GivenDouble_AndDisposedStream() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream, Encoding.UTF8); + writer.Dispose(); + stream.Dispose(); + + Assert.Throws(() => writer.WriteLineNoAlloc(420.0)); + Assert.Throws(() => writer.WriteLineNoAlloc(420.0, "N0")); + Assert.Throws(() => writer.WriteLineNoAlloc(420.0, "N0", null)); + } + + [Test] + public void WriteNoAlloc_ShouldWriteTextValue_GivenDouble() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteNoAlloc(420.0); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + const string expected = "420"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteNoAlloc_ShouldWriteTextValue_GivenDouble_AndFormatString() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteNoAlloc(420.0, "N0"); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + const string expected = "420"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteNoAlloc_ShouldWriteTextValue_GivenDouble_AndFormatString_AndCultureInfo() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteNoAlloc(420.0, "N0", CultureInfo.CurrentCulture); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + const string expected = "420"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteLineNoAlloc_ShouldWriteTextValue_GivenDouble() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteLineNoAlloc(420.0); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + var expected = $"420{Environment.NewLine}"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteLineNoAlloc_ShouldWriteTextValue_GivenDouble_AndFormatString() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteLineNoAlloc(420.0, "N0"); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + var expected = $"420{Environment.NewLine}"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteLineNoAlloc_ShouldWriteTextValue_GivenDouble_AndFormatString_AndCultureInfo() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteLineNoAlloc(420.0, "N0", CultureInfo.CurrentCulture); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + var expected = $"420{Environment.NewLine}"; + + Assert.That(actual, Is.EqualTo(expected)); + } +} diff --git a/X10D.Tests/src/IO/TextWriterTests.Single.cs b/X10D.Tests/src/IO/TextWriterTests.Single.cs new file mode 100644 index 0000000..3ccba4f --- /dev/null +++ b/X10D.Tests/src/IO/TextWriterTests.Single.cs @@ -0,0 +1,131 @@ +using System.Globalization; +using System.Text; +using NUnit.Framework; +using X10D.IO; + +namespace X10D.Tests.IO; + +public partial class TextWriterTests +{ + [Test] + public void WriteNoAlloc_ShouldThrowArgumentNullException_GivenSingle_AndNullWriter() + { + TextWriter writer = null!; + Assert.Throws(() => writer.WriteNoAlloc(420.0f)); + Assert.Throws(() => writer.WriteNoAlloc(420.0f, "N0")); + Assert.Throws(() => writer.WriteNoAlloc(420.0f, "N0", null)); + } + + [Test] + public void WriteNoAlloc_ShouldThrowObjectDisposedException_GivenSingle_AndDisposedStream() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream, Encoding.UTF8); + writer.Dispose(); + stream.Dispose(); + + Assert.Throws(() => writer.WriteNoAlloc(420.0f)); + Assert.Throws(() => writer.WriteNoAlloc(420.0f, "N0")); + Assert.Throws(() => writer.WriteNoAlloc(420.0f, "N0", null)); + } + + [Test] + public void WriteLineNoAlloc_ShouldThrowArgumentNullException_GivenSingle_AndNullWriter() + { + TextWriter writer = null!; + Assert.Throws(() => writer.WriteLineNoAlloc(420.0f)); + Assert.Throws(() => writer.WriteLineNoAlloc(420.0f, "N0")); + Assert.Throws(() => writer.WriteLineNoAlloc(420.0f, "N0", null)); + } + + [Test] + public void WriteLineNoAlloc_ShouldThrowObjectDisposedException_GivenSingle_AndDisposedStream() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream, Encoding.UTF8); + writer.Dispose(); + stream.Dispose(); + + Assert.Throws(() => writer.WriteLineNoAlloc(420.0f)); + Assert.Throws(() => writer.WriteLineNoAlloc(420.0f, "N0")); + Assert.Throws(() => writer.WriteLineNoAlloc(420.0f, "N0", null)); + } + + [Test] + public void WriteNoAlloc_ShouldWriteTextValue_GivenSingle() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteNoAlloc(420.0f); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + const string expected = "420"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteNoAlloc_ShouldWriteTextValue_GivenSingle_AndFormatString() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteNoAlloc(420.0f, "N0"); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + const string expected = "420"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteNoAlloc_ShouldWriteTextValue_GivenSingle_AndFormatString_AndCultureInfo() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteNoAlloc(420.0f, "N0", CultureInfo.CurrentCulture); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + const string expected = "420"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteLineNoAlloc_ShouldWriteTextValue_GivenSingle() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteLineNoAlloc(420.0f); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + var expected = $"420{Environment.NewLine}"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteLineNoAlloc_ShouldWriteTextValue_GivenSingle_AndFormatString() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteLineNoAlloc(420.0f, "N0"); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + var expected = $"420{Environment.NewLine}"; + + Assert.That(actual, Is.EqualTo(expected)); + } + + [Test] + public void WriteLineNoAlloc_ShouldWriteTextValue_GivenSingle_AndFormatString_AndCultureInfo() + { + Assert.That(_stream.Length, Is.Zero); + _writer.WriteLineNoAlloc(420.0f, "N0", CultureInfo.CurrentCulture); + _writer.Flush(); + + string actual = Encoding.UTF8.GetString(_stream.ToArray()); + var expected = $"420{Environment.NewLine}"; + + Assert.That(actual, Is.EqualTo(expected)); + } +} diff --git a/X10D/src/IO/TextWriterExtensions.WriteLineNoAlloc.Double.cs b/X10D/src/IO/TextWriterExtensions.WriteLineNoAlloc.Double.cs new file mode 100644 index 0000000..1a63f42 --- /dev/null +++ b/X10D/src/IO/TextWriterExtensions.WriteLineNoAlloc.Double.cs @@ -0,0 +1,83 @@ +using System.Globalization; + +namespace X10D.IO; + +public static partial class TextWriterExtensions +{ + /// + /// Writes the text representation of an 8-byte floating-point value to the text stream, followed by a line terminator, + /// without allocating a string. + /// + /// The to write to. + /// The 8-byte floating-point value to write. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteLineNoAlloc(this TextWriter writer, double value) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + writer.WriteLineNoAlloc(value, "N0".AsSpan(), CultureInfo.CurrentCulture); + } + + /// + /// Writes the text representation of an 8-byte floating-point value to the text stream, followed by a line terminator, + /// without allocating a string. + /// + /// The to write to. + /// The 8-byte floating-point value to write. + /// A standard or custom numeric format string. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteLineNoAlloc(this TextWriter writer, double value, ReadOnlySpan format) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + WriteLineNoAlloc(writer, value, format, CultureInfo.CurrentCulture); + } + + /// + /// Writes the text representation of an 8-byte floating-point value to the text stream, followed by a line terminator, + /// without allocating a string. + /// + /// The to write to. + /// The 8-byte floating-point value to write. + /// A standard or custom numeric format string. + /// An object that supplies culture-specific formatting information. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteLineNoAlloc(this TextWriter writer, double value, ReadOnlySpan format, + IFormatProvider? formatProvider) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + writer.WriteNoAlloc(value, format, formatProvider); + writer.WriteLine(); + } +} diff --git a/X10D/src/IO/TextWriterExtensions.WriteLineNoAlloc.Single.cs b/X10D/src/IO/TextWriterExtensions.WriteLineNoAlloc.Single.cs new file mode 100644 index 0000000..bb6d2e0 --- /dev/null +++ b/X10D/src/IO/TextWriterExtensions.WriteLineNoAlloc.Single.cs @@ -0,0 +1,83 @@ +using System.Globalization; + +namespace X10D.IO; + +public static partial class TextWriterExtensions +{ + /// + /// Writes the text representation of a 4-byte floating-point value to the text stream, followed by a line terminator, + /// without allocating a string. + /// + /// The to write to. + /// The 4-byte floating-point value to write. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteLineNoAlloc(this TextWriter writer, float value) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + writer.WriteLineNoAlloc(value, "N0".AsSpan(), CultureInfo.CurrentCulture); + } + + /// + /// Writes the text representation of a 4-byte floating-point value to the text stream, followed by a line terminator, + /// without allocating a string. + /// + /// The to write to. + /// The 4-byte floating-point value to write. + /// A standard or custom numeric format string. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteLineNoAlloc(this TextWriter writer, float value, ReadOnlySpan format) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + WriteLineNoAlloc(writer, value, format, CultureInfo.CurrentCulture); + } + + /// + /// Writes the text representation of a 4-byte floating-point value to the text stream, followed by a line terminator, + /// without allocating a string. + /// + /// The to write to. + /// The 4-byte floating-point value to write. + /// A standard or custom numeric format string. + /// An object that supplies culture-specific formatting information. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteLineNoAlloc(this TextWriter writer, float value, ReadOnlySpan format, + IFormatProvider? formatProvider) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + writer.WriteNoAlloc(value, format, formatProvider); + writer.WriteLine(); + } +} diff --git a/X10D/src/IO/TextWriterExtensions.WriteNoAlloc.Double.cs b/X10D/src/IO/TextWriterExtensions.WriteNoAlloc.Double.cs new file mode 100644 index 0000000..6b6de18 --- /dev/null +++ b/X10D/src/IO/TextWriterExtensions.WriteNoAlloc.Double.cs @@ -0,0 +1,91 @@ +using System.Globalization; + +namespace X10D.IO; + +public static partial class TextWriterExtensions +{ + /// + /// Writes the text representation of an 8-byte floating-point value to the text stream, without allocating a string. + /// + /// The to write to. + /// The 8-byte floating-point value to write. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteNoAlloc(this TextWriter writer, double value) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + WriteNoAlloc(writer, value, "N0".AsSpan(), CultureInfo.CurrentCulture); + } + + /// + /// Writes the text representation of an 8-byte floating-point value to the text stream, without allocating a string. + /// + /// The to write to. + /// The 8-byte floating-point value to write. + /// A standard or custom numeric format string. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteNoAlloc(this TextWriter writer, double value, ReadOnlySpan format) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + WriteNoAlloc(writer, value, format, CultureInfo.CurrentCulture); + } + + /// + /// Writes the text representation of an 8-byte floating-point value to the text stream, without allocating a string. + /// + /// The to write to. + /// The 8-byte floating-point value to write. + /// A standard or custom numeric format string. + /// An object that supplies culture-specific formatting information. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteNoAlloc(this TextWriter writer, double value, ReadOnlySpan format, + IFormatProvider? formatProvider) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + Span buffer = stackalloc char[100]; + if (value.TryFormat(buffer, out int charsWritten, format, formatProvider)) + { + Span truncated = buffer[..charsWritten]; + for (var index = 0; index < truncated.Length; index++) + { + writer.Write(truncated[index]); + } + } + else + { + writer.Write(value.ToString(format.ToString(), formatProvider)); + } + } +} diff --git a/X10D/src/IO/TextWriterExtensions.WriteNoAlloc.Single.cs b/X10D/src/IO/TextWriterExtensions.WriteNoAlloc.Single.cs new file mode 100644 index 0000000..f595593 --- /dev/null +++ b/X10D/src/IO/TextWriterExtensions.WriteNoAlloc.Single.cs @@ -0,0 +1,91 @@ +using System.Globalization; + +namespace X10D.IO; + +public static partial class TextWriterExtensions +{ + /// + /// Writes the text representation of a 4-byte floating-point value to the text stream, without allocating a string. + /// + /// The to write to. + /// The 4-byte floating-point value to write. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteNoAlloc(this TextWriter writer, float value) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + WriteNoAlloc(writer, value, "N0".AsSpan(), CultureInfo.CurrentCulture); + } + + /// + /// Writes the text representation of a 4-byte floating-point value to the text stream, without allocating a string. + /// + /// The to write to. + /// The 4-byte floating-point value to write. + /// A standard or custom numeric format string. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteNoAlloc(this TextWriter writer, float value, ReadOnlySpan format) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + WriteNoAlloc(writer, value, format, CultureInfo.CurrentCulture); + } + + /// + /// Writes the text representation of a 4-byte floating-point value to the text stream, without allocating a string. + /// + /// The to write to. + /// The 4-byte floating-point value to write. + /// A standard or custom numeric format string. + /// An object that supplies culture-specific formatting information. + /// This method may still allocate if the integer is too large to fit in a stack-allocated buffer. + /// is . + /// The is closed. + /// An I/O error occurs. + public static void WriteNoAlloc(this TextWriter writer, float value, ReadOnlySpan format, + IFormatProvider? formatProvider) + { +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(writer); +#else + if (writer is null) + { + throw new ArgumentNullException(nameof(writer)); + } +#endif + + Span buffer = stackalloc char[100]; + if (value.TryFormat(buffer, out int charsWritten, format, formatProvider)) + { + Span truncated = buffer[..charsWritten]; + for (var index = 0; index < truncated.Length; index++) + { + writer.Write(truncated[index]); + } + } + else + { + writer.Write(value.ToString(format.ToString(), formatProvider)); + } + } +}