From 2ba7965a08f1f89e10939753d33ff5f56c8074de Mon Sep 17 00:00:00 2001 From: jdymitarai Date: Thu, 10 Sep 2026 13:09:44 +0800 Subject: [PATCH] fix(stdlib): renormalize %e/%g mantissa carry and fix %g precision 0 (fixes #1327) --- stdlib/std.jsonnet | 33 ++++++++++++++++++++++----------- test_suite/format.jsonnet | 14 ++++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/stdlib/std.jsonnet b/stdlib/std.jsonnet index 9eee818ff..33097b06f 100644 --- a/stdlib/std.jsonnet +++ b/stdlib/std.jsonnet @@ -613,15 +613,18 @@ limitations under the License. // Render floating point in scientific form local render_float_sci(n__, zero_pad, blank, plus, ensure_pt, trailing, caps, prec) = - local exponent = if n__ == 0 then 0 else std.floor(std.log(std.abs(n__)) / std.log(10)); - local suff = (if caps then 'E' else 'e') - + render_int(exponent < 0, std.abs(exponent), 3, 0, false, true, 10, ''); - local mantissa = if exponent == -324 then + local initial_exp = if n__ == 0 then 0 else std.floor(std.log(std.abs(n__)) / std.log(10)); + local initial_mantissa = if initial_exp == -324 then // Avoid a rounding error where std.pow(10, -324) is 0 // -324 is the smallest exponent possible. - n__ * 10 / std.pow(10, exponent + 1) + n__ * 10 / std.pow(10, initial_exp + 1) else - n__ / std.pow(10, exponent); + n__ / std.pow(10, initial_exp); + local carry = n__ != 0 && (std.floor(std.abs(initial_mantissa) * std.pow(10, prec) + 0.5) >= 10 * std.pow(10, prec)); + local exponent = if carry then initial_exp + 1 else initial_exp; + local mantissa = if carry then initial_mantissa / 10 else initial_mantissa; + local suff = (if caps then 'E' else 'e') + + render_int(exponent < 0, std.abs(exponent), 3, 0, false, true, 10, ''); local zp2 = zero_pad - std.length(suff); render_float_dec(mantissa, zp2, blank, plus, ensure_pt, trailing, prec) + suff; @@ -688,8 +691,16 @@ limitations under the License. error 'Format required number at ' + i + ', got ' + std.type(val) else - local exponent = if val != 0 then std.floor(std.log(std.abs(val)) / std.log(10)) else 0; - if exponent < -4 || exponent >= fpprec then + // Per Python formatting rules, a precision of 0 is treated as 1. + local gprec = if prec_or_null != null then std.max(1, prec_or_null) else 6; + local initial_exp = if val != 0 then std.floor(std.log(std.abs(val)) / std.log(10)) else 0; + local initial_mantissa = if initial_exp == -324 then + val * 10 / std.pow(10, initial_exp + 1) + else + val / std.pow(10, initial_exp); + local carry = val != 0 && (std.floor(std.abs(initial_mantissa) * std.pow(10, gprec - 1) + 0.5) >= 10 * std.pow(10, gprec - 1)); + local exponent = if carry then initial_exp + 1 else initial_exp; + if exponent < -4 || exponent >= gprec then render_float_sci(val, zp, cflags.blank, @@ -697,16 +708,16 @@ limitations under the License. cflags.alt, cflags.alt, code.caps, - fpprec - 1) + gprec - 1) else - local digits_before_pt = std.max(1, exponent + 1); + local dec_prec = if exponent >= 0 then gprec - (exponent + 1) else gprec - exponent - 1; render_float_dec(val, zp, cflags.blank, cflags.plus, cflags.alt, cflags.alt, - fpprec - digits_before_pt) + dec_prec) else if code.ctype == 'c' then if std.type(val) == 'number' then std.char(val) diff --git a/test_suite/format.jsonnet b/test_suite/format.jsonnet index a7b8f0620..068021eb5 100644 --- a/test_suite/format.jsonnet +++ b/test_suite/format.jsonnet @@ -158,6 +158,8 @@ std.assertEqual(std.format('%e', [910]), '9.100000e+02') && std.assertEqual(std.format('%e', [0]), '0.000000e+00') && std.assertEqual(std.format('%.0le', [910]), '9e+02') && std.assertEqual(std.format('%.0le', [0]), '0e+00') && +std.assertEqual(std.format('%.0e', [9.5e10]), '1e+11') && +std.assertEqual(std.format('%.1e', [9.99]), '1.0e+01') && std.assertEqual(std.format('%#e', [-910]), '-9.100000e+02') && std.assertEqual(std.format('%16e', [910]), ' 9.100000e+02') && std.assertEqual(std.format('%016e', [910]), '00009.100000e+02') && @@ -179,6 +181,8 @@ std.assertEqual(std.type(std.format('%e', [3.94066e-324])), 'string') && // E std.assertEqual(std.format('%E', [910]), '9.100000E+02') && std.assertEqual(std.format('%.0lE', [910]), '9E+02') && +std.assertEqual(std.format('%.0E', [9.5e10]), '1E+11') && +std.assertEqual(std.format('%.1E', [9.99]), '1.0E+01') && std.assertEqual(std.format('%#E', [-910]), '-9.100000E+02') && std.assertEqual(std.format('%16E', [910]), ' 9.100000E+02') && std.assertEqual(std.format('%016E', [910]), '00009.100000E+02') && @@ -218,6 +222,11 @@ std.assertEqual(std.format('%.4f', [0.99995]), '1.0000') && // g std.assertEqual(std.format('%g', [0]), '0') && +std.assertEqual(std.format('%.0g', [1.0]), '1') && +std.assertEqual(std.format('%+.0g', [1.0]), '+1') && +std.assertEqual(std.format('%.0g', [0.1]), '0.1') && +std.assertEqual(std.format('%.1g', [9.9]), '1e+01') && +std.assertEqual(std.format('%.2g', [99.9]), '1e+02') && std.assertEqual(std.format('%#.3g', [1000000001]), '1.00e+09') && std.assertEqual(std.format('%#.3g', [1100]), '1.10e+03') && std.assertEqual(std.format('%#.3g', [1.1]), '1.10') && @@ -252,6 +261,11 @@ std.assertEqual(std.format('%10.5g', [1.1]), ' 1.1') && // G std.assertEqual(std.format('%G', [0]), '0') && +std.assertEqual(std.format('%.0G', [1.0]), '1') && +std.assertEqual(std.format('%+.0G', [1.0]), '+1') && +std.assertEqual(std.format('%.0G', [0.1]), '0.1') && +std.assertEqual(std.format('%.1G', [9.9]), '1E+01') && +std.assertEqual(std.format('%.2G', [99.9]), '1E+02') && std.assertEqual(std.format('%#.3G', [1000000001]), '1.00E+09') && std.assertEqual(std.format('%#.3G', [1100]), '1.10E+03') && std.assertEqual(std.format('%#.3G', [1.1]), '1.10') &&