diff --git a/src/core/IronPython/Runtime/Operations/FloatOps.cs b/src/core/IronPython/Runtime/Operations/FloatOps.cs index 3e428537e..c8c52737f 100644 --- a/src/core/IronPython/Runtime/Operations/FloatOps.cs +++ b/src/core/IronPython/Runtime/Operations/FloatOps.cs @@ -1132,17 +1132,17 @@ public static object __new__(CodeContext/*!*/ context, PythonType cls, object x) return (float)(double)DoubleOps.NewFloat(context, TypeCache.Single, x); } - public static string __str__(CodeContext/*!*/ context, float x) { - // Python does not natively support System.Single. However, we try to provide - // formatting consistent with System.Double. - return StringFormatter.Format(context, "%.6g", x, trailingZeroAfterWholeFloat: true); - } + public static string __str__(CodeContext/*!*/ context, float x) + => Schubfach.ftoa(x); public static string __repr__(CodeContext/*!*/ context, float self) { return __str__(context, self); } public static string __format__(CodeContext/*!*/ context, float self, [NotNone] string/*!*/ formatSpec) { + // format(x, '') must be equal to str(x), which is not the same as str((double)x) + if (formatSpec.Length == 0) return __str__(context, self); + return DoubleOps.__format__(context, self, formatSpec); } diff --git a/src/core/IronPython/Runtime/Schubfach.cs b/src/core/IronPython/Runtime/Schubfach.cs index 7250fc7e3..f70666332 100644 --- a/src/core/IronPython/Runtime/Schubfach.cs +++ b/src/core/IronPython/Runtime/Schubfach.cs @@ -7,6 +7,7 @@ #nullable enable using System; +using System.Runtime.CompilerServices; namespace IronPython.Runtime { internal static partial class Schubfach { @@ -95,11 +96,11 @@ private static void write8digits(Span buffer, ref int pos, uint n) { ]; // Rewrites the digits in buffer[start..pos] in fixed notation if Python's - // repr would, i.e. for -4 < dec_point <= 16. - private static bool try_write_fixed(Span buffer, ref int pos, int start, int dec_exp, bool add_dot_0) { + // repr would, i.e. for -4 < dec_point <= max_dec_point (16 for double). + private static bool try_write_fixed(Span buffer, ref int pos, int start, int dec_exp, int max_dec_point, bool add_dot_0) { int num_digits = pos - start; int dec_point = dec_exp + 1; - if (dec_point <= -4 || dec_point > 16) return false; + if (dec_point <= -4 || dec_point > max_dec_point) return false; if (dec_point <= 0) { // 0.000ddd @@ -127,7 +128,7 @@ private static bool try_write_fixed(Span buffer, ref int pos, int start, i } // Writes the decimal FP number dec_sig * 10**dec_exp to buffer. - private static string write(bool is_negative, ulong dec_sig, int dec_exp, bool add_dot_0) { + private static string write(bool is_negative, ulong dec_sig, int dec_exp, int max_dec_point, bool add_dot_0) { Span buffer = stackalloc char[buffer_size]; int pos = 0; if (is_negative) buffer[pos++] = '-'; @@ -149,7 +150,7 @@ private static string write(bool is_negative, ulong dec_sig, int dec_exp, bool a // Remove trailing zeros. while (buffer[pos - 1] == '0') --pos; - if (try_write_fixed(buffer, ref pos, start, dec_exp, add_dot_0)) { + if (try_write_fixed(buffer, ref pos, start, dec_exp, max_dec_point, add_dot_0)) { return buffer.Slice(0, pos).ToString(); } @@ -202,11 +203,44 @@ public static string dtoa(double value, bool add_dot_0 = true) { bin_sig ^= implicit_bit; bin_exp -= num_sig_bits + 1023; // Remove the exponent bias. + return to_decimal(is_negative, bin_sig, bin_exp, regular, num_sig_bits, 16, add_dot_0); + } + + /// Returns the shortest decimal representation of `value` that round-trips as a float, + /// formatted like Python's repr but switching to exponent notation from 1e6 like numpy. + public static string ftoa(float value, bool add_dot_0 = true) { + int bitsInt = Unsafe.As(ref value); + bool is_negative = bitsInt < 0; + ulong bits = unchecked((uint)bitsInt); + + const int num_sig_bits = 24 - 1; // 24 = std::numeric_limits::digits + const int exp_mask = 0xff; + int bin_exp = (int)(bits >> num_sig_bits) & exp_mask; + + const ulong implicit_bit = (ulong)1 << num_sig_bits; + ulong bin_sig = bits & (implicit_bit - 1); // binary significand + + bool regular = bin_sig != 0; + if (((bin_exp + 1) & exp_mask) <= 1) { + if (bin_exp != 0) return bin_sig != 0 ? "nan" : is_negative ? "-inf" : "inf"; + if (bin_sig == 0) return is_negative ? (add_dot_0 ? "-0.0" : "-0") : (add_dot_0 ? "0.0" : "0"); + // Handle subnormals. + bin_sig ^= implicit_bit; + ++bin_exp; + regular = true; + } + bin_sig ^= implicit_bit; + bin_exp -= num_sig_bits + 127; // Remove the exponent bias. + + return to_decimal(is_negative, bin_sig, bin_exp, regular, num_sig_bits, 6, add_dot_0); + } + + private static string to_decimal(bool is_negative, ulong bin_sig, int bin_exp, bool regular, int num_sig_bits, int max_dec_point, bool add_dot_0) { // Handle small integers. if ((bin_exp < 0) & (bin_exp >= -num_sig_bits)) { ulong f = bin_sig >> -bin_exp; if (f << -bin_exp == bin_sig) { - return write(is_negative, f, 0, add_dot_0); + return write(is_negative, f, 0, max_dec_point, add_dot_0); } } @@ -260,7 +294,7 @@ public static string dtoa(double value, bool add_dot_0 = true) { bool under_in2 = lower + bin_sig_lsb <= (dec_sig_under2 << 2); bool over_in2 = (dec_sig_over2 << 2) + bin_sig_lsb <= upper; if (under_in2 != over_in2) { - return write(is_negative, under_in2 ? dec_sig_under2 : dec_sig_over2, dec_exp, add_dot_0); + return write(is_negative, under_in2 ? dec_sig_under2 : dec_sig_over2, dec_exp, max_dec_point, add_dot_0); } } @@ -269,13 +303,13 @@ public static string dtoa(double value, bool add_dot_0 = true) { bool over_in = (dec_sig_over << 2) + bin_sig_lsb <= upper; if (under_in != over_in) { // Only one of dec_sig_under or dec_sig_over are in the rounding interval. - return write(is_negative, under_in ? dec_sig_under : dec_sig_over, dec_exp, add_dot_0); + return write(is_negative, under_in ? dec_sig_under : dec_sig_over, dec_exp, max_dec_point, add_dot_0); } // Both dec_sig_under and dec_sig_over are in the interval - pick the closest. int cmp = unchecked((int)(scaled_sig - ((dec_sig_under + dec_sig_over) << 1))); bool under_closer = cmp < 0 || cmp == 0 && (dec_sig_under & 1) == 0; - return write(is_negative, under_closer ? dec_sig_under : dec_sig_over, dec_exp, add_dot_0); + return write(is_negative, under_closer ? dec_sig_under : dec_sig_over, dec_exp, max_dec_point, add_dot_0); } // std::countl_zero diff --git a/src/core/IronPython/Runtime/StringFormatter.cs b/src/core/IronPython/Runtime/StringFormatter.cs index b327fab24..3200a2323 100644 --- a/src/core/IronPython/Runtime/StringFormatter.cs +++ b/src/core/IronPython/Runtime/StringFormatter.cs @@ -33,8 +33,6 @@ internal class StringFormatter { // The options for formatting the current formatting specifier in the format string private FormatSettings _opts; - // Should ddd.0 be displayed as "ddd" or "ddd.0". "'%g' % ddd.0" needs "ddd", but str(ddd.0) needs "ddd.0" - private bool _trailingZeroAfterWholeFloat; private bool _asBytes; @@ -75,8 +73,8 @@ private StringFormatter(CodeContext/*!*/ context, string str, object? data) { #region Public API Surface - public static string Format(CodeContext/*!*/ context, string str, object? data, bool trailingZeroAfterWholeFloat = false) - => new StringFormatter(context, str, data) { _trailingZeroAfterWholeFloat = trailingZeroAfterWholeFloat }.Format(); + public static string Format(CodeContext/*!*/ context, string str, object? data) + => new StringFormatter(context, str, data).Format(); internal static byte[] FormatBytes(CodeContext/*!*/ context, ReadOnlySpan str, object? data) => new StringFormatter(context, str.MakeString(), data) { _asBytes = true }.Format().MakeByteArray(); @@ -544,11 +542,6 @@ private void AppendFloat(char format) { var fPos = DoubleOps.Sign(val) >= 0 || double.IsNaN(val); var str = FloatFormatter.Format(val, format, _opts.Precision, _opts.AltForm); - // str(System.Single) uses "%.6g" but needs "ddd.0" for whole numbers - if (_trailingZeroAfterWholeFloat && !double.IsNaN(val) && !double.IsInfinity(val) && str.IndexOfAny(['.', 'e', 'E']) == -1) { - str += ".0"; - } - if (!fPos) { str = "-" + str; } else if (_opts.SignChar) { diff --git a/tests/suite/test_formatting.py b/tests/suite/test_formatting.py index b2df47886..d031f4bcf 100644 --- a/tests/suite/test_formatting.py +++ b/tests/suite/test_formatting.py @@ -204,10 +204,14 @@ def test_single(self): self.assertEqual(str(f), "1.1") self.assertEqual("%g" % f, "1.1") f = IronPythonTest.DoubleToFloat.ToFloat(1.2345678) - self.assertEqual(str(f), "1.23457") + self.assertEqual(str(f), "1.2345678") + self.assertEqual(format(f), "1.2345678") self.assertEqual("%g" % f, "1.23457") + f = IronPythonTest.DoubleToFloat.ToFloat(123456.0) + self.assertEqual(str(f), "123456.0") + self.assertEqual("%g" % f, "123456") f = IronPythonTest.DoubleToFloat.ToFloat(1234567.8) - self.assertEqual(str(f), "1.23457e+06") + self.assertEqual(str(f), "1.2345678e+06") self.assertEqual("%g" % f, "1.23457e+06") def test_long(self):