From 926624a2d4914a22d59c759dd5ea0dc2771dfe7a Mon Sep 17 00:00:00 2001 From: RKS Date: Wed, 9 Sep 2026 12:50:50 -0400 Subject: [PATCH] fix: preserve zero fractions when formatting integral floats --- stdlib/std.jsonnet | 8 +++++--- test_suite/format.jsonnet | 9 +++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/stdlib/std.jsonnet b/stdlib/std.jsonnet index 9eee818ff..a324e7f75 100644 --- a/stdlib/std.jsonnet +++ b/stdlib/std.jsonnet @@ -590,15 +590,17 @@ limitations under the License. // Render floating point in decimal form local render_float_dec(n__, zero_pad, blank, plus, ensure_pt, trailing, prec) = local n_ = std.abs(n__); - local whole = std.floor(n_); + local is_integral = std.floor(n_) == n_; // Represent the rounded number as an integer * 1/10**prec. // Note that it can also be equal to 10**prec and we'll need to carry // over to the wholes. We operate on the absolute numbers, so that we // don't have trouble with the rounding direction. local denominator = std.pow(10, prec); local numerator = std.abs(n_) * denominator + 0.5; - local whole = std.sign(n_) * std.floor(numerator / denominator); - local frac = std.floor(numerator) % denominator; + // Integral values already have a zero fraction. Scaling them can lose + // whole digits, introduce a spurious remainder, or overflow. + local whole = if is_integral then n_ else std.sign(n_) * std.floor(numerator / denominator); + local frac = if is_integral then 0 else std.floor(numerator) % denominator; local dot_size = if prec == 0 && !ensure_pt then 0 else 1; local zp = zero_pad - prec - dot_size; local str = render_int(n__ < 0, whole, zp, 0, blank, plus, 10, ''); diff --git a/test_suite/format.jsonnet b/test_suite/format.jsonnet index a7b8f0620..ab033d102 100644 --- a/test_suite/format.jsonnet +++ b/test_suite/format.jsonnet @@ -281,6 +281,15 @@ std.assertEqual(std.format('%10.5G', [1100]), ' 1100') && std.assertEqual(std.format('%10.5G', [110]), ' 110') && std.assertEqual(std.format('%10.5G', [1.1]), ' 1.1') && +// Scaling integral floats by the precision must not invent fractional digits. +std.assertEqual(std.format('%f', 1e20), '100000000000000000000.000000') && +std.assertEqual(std.format('%.3f', -1e20), '-100000000000000000000.000') && +std.assertEqual(std.format('%+.0f', 1e20), '+100000000000000000000') && +std.assertEqual(std.format('%#.0f', 1e20), '100000000000000000000.') && +std.assertEqual(std.format('%.6f', 9007199254740991), '9007199254740991.000000') && +std.assertEqual(std.format('%08.2f', 42), '00042.00') && +std.assertEqual(std.format('%.2f', 1.999), '2.00') && + // lots together, also test % operator std.assertEqual('%s[%05d]-%2x%2x%2x%c' % ['foo', 3991, 17, 18, 17, 100], 'foo[03991]-111211d') &&