From b9c61b1011e78235de4520d89faff1e821ea7ef5 Mon Sep 17 00:00:00 2001 From: Adien Akhmad Date: Sat, 16 Aug 2025 11:37:02 +0700 Subject: [PATCH 1/2] fix Utf8Formatter mistakenly removing zeros when formatting a floating-point whole number The prev logic would format floating-point value of 10 as "1". To fix this, the removal of trailing zeros and the decimal separator should be handled separately. --- .../Utf8FormatterTests.cs | 52 +++++++++++++++++++ Hexa.NET.Utilities/Text/Utf8Formatter.cs | 14 ++++- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 Hexa.NET.Utilities.Tests/Utf8FormatterTests.cs diff --git a/Hexa.NET.Utilities.Tests/Utf8FormatterTests.cs b/Hexa.NET.Utilities.Tests/Utf8FormatterTests.cs new file mode 100644 index 0000000..b02c3cc --- /dev/null +++ b/Hexa.NET.Utilities.Tests/Utf8FormatterTests.cs @@ -0,0 +1,52 @@ +using System.Globalization; +using System.Text; +using Hexa.NET.Utilities.Text; + +namespace Hexa.NET.Utilities.Tests; + +public class Utf8FormatterTests +{ + [TestCase(1f, 0, "1")] + [TestCase(10f, 0, "10")] + [TestCase(0.5f, 1, "0.5")] + [TestCase(0.75f, 2, "0.75")] + [TestCase(0.125f, 3, "0.125")] + [TestCase(0.0625f, 4, "0.0625")] + [TestCase(0.03125f, 5, "0.03125")] + [TestCase(0.015625f, 6, "0.015625")] + [TestCase(0.0078125f, 7, "0.0078125")] + public unsafe void FormatFloatTest(float value, int digit, string expected) + { + // Arrange + byte* buffer = stackalloc byte[128]; + + // Act + int len = Utf8Formatter.Format(value, buffer, 128, CultureInfo.InvariantCulture, digit); + ReadOnlySpan utf8Span = new ReadOnlySpan(buffer, len); + + // Assert + Assert.That(Encoding.UTF8.GetString(utf8Span), Is.EqualTo(expected)); + } + + [TestCase(1, 0, "1")] + [TestCase(10, 0, "10")] + [TestCase(0.5, 1, "0.5")] + [TestCase(0.75, 2, "0.75")] + [TestCase(0.125, 3, "0.125")] + [TestCase(0.0625, 4, "0.0625")] + [TestCase(0.03125, 5, "0.03125")] + [TestCase(0.015625, 6, "0.015625")] + [TestCase(0.0078125, 7, "0.0078125")] + public unsafe void FormatDoubleTest(double value, int digit, string expected) + { + // Arrange + byte* buffer = stackalloc byte[128]; + + // Act + int len = Utf8Formatter.Format(value, buffer, 128, CultureInfo.InvariantCulture, digit); + ReadOnlySpan utf8Span = new ReadOnlySpan(buffer, len); + + // Assert + Assert.That(Encoding.UTF8.GetString(utf8Span), Is.EqualTo(expected)); + } +} \ No newline at end of file diff --git a/Hexa.NET.Utilities/Text/Utf8Formatter.cs b/Hexa.NET.Utilities/Text/Utf8Formatter.cs index fc771c0..92df152 100644 --- a/Hexa.NET.Utilities/Text/Utf8Formatter.cs +++ b/Hexa.NET.Utilities/Text/Utf8Formatter.cs @@ -340,7 +340,12 @@ public static unsafe int Format(float value, byte* buffer, int bufSize, CultureI if (fraction < 1e-14) break; } - while (*(buffer - 1) == '0' || *(buffer - 1) == '.') + while (*(buffer - 1) == '0') + { + buffer--; + } + + while (*(buffer - 1) == '.') { buffer--; } @@ -418,7 +423,12 @@ public static unsafe int Format(double value, byte* buffer, int bufSize, Culture if (fraction < 1e-14) break; } - while (*(buffer - 1) == '0' || *(buffer - 1) == '.') + while (*(buffer - 1) == '0') + { + buffer--; + } + + while (*(buffer - 1) == '.') { buffer--; } From 7fc5a2eec9be149ac2577563a129a7e89fbc1edb Mon Sep 17 00:00:00 2001 From: Adien Akhmad Date: Sat, 16 Aug 2025 12:03:31 +0700 Subject: [PATCH 2/2] fix Utf8Formatter always assume invariant culture when formatting a floating-point whole number --- .../Utf8FormatterTests.cs | 48 +++++++++++++++++++ Hexa.NET.Utilities/Text/Utf8Formatter.cs | 18 ++++--- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/Hexa.NET.Utilities.Tests/Utf8FormatterTests.cs b/Hexa.NET.Utilities.Tests/Utf8FormatterTests.cs index b02c3cc..e7d282a 100644 --- a/Hexa.NET.Utilities.Tests/Utf8FormatterTests.cs +++ b/Hexa.NET.Utilities.Tests/Utf8FormatterTests.cs @@ -49,4 +49,52 @@ public unsafe void FormatDoubleTest(double value, int digit, string expected) // Assert Assert.That(Encoding.UTF8.GetString(utf8Span), Is.EqualTo(expected)); } + + [TestCase(10, 0, ".","10")] + [TestCase(10, 0, "\uFF0C","10")] + [TestCase(0.75f, 2, ".","0.75")] + [TestCase(0.75f, 2, "\uFF0C","0\uFF0C75")] + public unsafe void FormatFloatCultureTest(float value, int digit, string separator, string expected) + { + // Arrange + byte* buffer = stackalloc byte[128]; + var culture = new CultureInfo( "", false ) + { + NumberFormat = + { + CurrencyDecimalSeparator = separator + } + }; + + // Act + int len = Utf8Formatter.Format(value, buffer, 128, culture, digit); + ReadOnlySpan utf8Span = new ReadOnlySpan(buffer, len); + + // Assert + Assert.That(Encoding.UTF8.GetString(utf8Span), Is.EqualTo(expected)); + } + + [TestCase(10, 0, ".","10")] + [TestCase(10, 0, "\uFF0C","10")] + [TestCase(0.75, 2, ".","0.75")] + [TestCase(0.75, 2, "\uFF0C","0\uFF0C75")] + public unsafe void FormatDoubleCultureTest(double value, int digit, string separator, string expected) + { + // Arrange + byte* buffer = stackalloc byte[128]; + var culture = new CultureInfo( "", false ) + { + NumberFormat = + { + CurrencyDecimalSeparator = separator + } + }; + + // Act + int len = Utf8Formatter.Format(value, buffer, 128, culture, digit); + var utf8Span = new ReadOnlySpan(buffer, len); + + // Assert + Assert.That(Encoding.UTF8.GetString(utf8Span), Is.EqualTo(expected)); + } } \ No newline at end of file diff --git a/Hexa.NET.Utilities/Text/Utf8Formatter.cs b/Hexa.NET.Utilities/Text/Utf8Formatter.cs index 92df152..192afc3 100644 --- a/Hexa.NET.Utilities/Text/Utf8Formatter.cs +++ b/Hexa.NET.Utilities/Text/Utf8Formatter.cs @@ -326,7 +326,9 @@ public static unsafe int Format(float value, byte* buffer, int bufSize, CultureI return (int)(buffer - start); } + byte* beforeSeparator = buffer; buffer += ConvertUtf16ToUtf8(format.CurrencyDecimalSeparator, buffer, (int)(end - buffer)); + byte* afterSeparator = buffer; digits = digits >= 0 ? digits : 7; @@ -340,14 +342,14 @@ public static unsafe int Format(float value, byte* buffer, int bufSize, CultureI if (fraction < 1e-14) break; } - while (*(buffer - 1) == '0') + while (buffer != afterSeparator && *(buffer - 1) == '0') { buffer--; } - while (*(buffer - 1) == '.') + if (buffer == afterSeparator) { - buffer--; + buffer = beforeSeparator; } end: @@ -409,7 +411,9 @@ public static unsafe int Format(double value, byte* buffer, int bufSize, Culture return (int)(buffer - start); } + byte* beforeSeparator = buffer; buffer += ConvertUtf16ToUtf8(format.CurrencyDecimalSeparator, buffer, (int)(end - buffer)); + byte* afterSeparator = buffer; digits = digits >= 0 ? digits : 7; @@ -423,14 +427,14 @@ public static unsafe int Format(double value, byte* buffer, int bufSize, Culture if (fraction < 1e-14) break; } - while (*(buffer - 1) == '0') + while (buffer != afterSeparator && *(buffer - 1) == '0') { buffer--; } - - while (*(buffer - 1) == '.') + + if (buffer == afterSeparator) { - buffer--; + buffer = beforeSeparator; } end: