From c5677c30966330c0a0b703c7130b53f189146e3e Mon Sep 17 00:00:00 2001 From: He-Pin Date: Wed, 29 Jul 2026 15:25:46 +0800 Subject: [PATCH 1/2] fix: preserve exact binary64 digits in decimal formats Motivation: High-precision %f and fixed-form %g formatting started from the shortest round-tripping decimal string, hiding meaningful trailing digits of the binary64 value. Modification: Use the exact binary64 decimal value when fixed formatting needs more than 15 digits, propagate the original %g significant precision, and use half-even rounding for the exact path. Add non-tie and exact-halfway regressions while preserving the public five-argument JVM descriptor. Result: High-precision fixed decimal output matches Python from the same IEEE-754 value, including ties-to-even, without changing the legacy low-precision rounding path or JVM binary compatibility. References: - https://github.com/databricks/sjsonnet/pull/1097 - https://jsonnet.org/ref/stdlib.html#std-format --- sjsonnet/src/sjsonnet/DecimalFormat.scala | 21 ++++++++++++++++--- sjsonnet/src/sjsonnet/Format.scala | 3 ++- .../format_exact_binary64_precision.jsonnet | 11 ++++++++++ ...at_exact_binary64_precision.jsonnet.golden | 1 + 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet.golden diff --git a/sjsonnet/src/sjsonnet/DecimalFormat.scala b/sjsonnet/src/sjsonnet/DecimalFormat.scala index b95cfe613..b0c8ccd9e 100644 --- a/sjsonnet/src/sjsonnet/DecimalFormat.scala +++ b/sjsonnet/src/sjsonnet/DecimalFormat.scala @@ -19,7 +19,16 @@ object DecimalFormat { hashes: Int, alternate: Boolean, expLengthOpt: Option[Int], - number: Double): String = { + number: Double): String = + format(zeroes, hashes, alternate, expLengthOpt, number, useExactDecimal = false) + + private[sjsonnet] def format( + zeroes: Int, + hashes: Int, + alternate: Boolean, + expLengthOpt: Option[Int], + number: Double, + useExactDecimal: Boolean): String = { expLengthOpt match { case Some(expLength) => var expNum = @@ -75,9 +84,15 @@ object DecimalFormat { if (alternate) prefix + "." else prefix } else { val denominator = BigDecimal(10).pow(precision) - val bd = BigDecimal(number).abs + val exactDecimal = useExactDecimal || precision > 15 + val bd = + if (exactDecimal) BigDecimal.exact(number).abs + else BigDecimal(number).abs val scaled = - (bd * denominator + BigDecimal("0.5")).setScale(0, BigDecimal.RoundingMode.FLOOR) + if (exactDecimal) + (bd * denominator).setScale(0, BigDecimal.RoundingMode.HALF_EVEN) + else + (bd * denominator + BigDecimal("0.5")).setScale(0, BigDecimal.RoundingMode.FLOOR) val wholeBD = (scaled / denominator).setScale(0, BigDecimal.RoundingMode.FLOOR) val fracBD = (scaled - wholeBD * denominator).abs diff --git a/sjsonnet/src/sjsonnet/Format.scala b/sjsonnet/src/sjsonnet/Format.scala index d878fd2f1..1219a1950 100644 --- a/sjsonnet/src/sjsonnet/Format.scala +++ b/sjsonnet/src/sjsonnet/Format.scala @@ -1222,7 +1222,8 @@ object Format { if (formatted.alternate) 0 else fractionalPrecision, formatted.alternate, None, - math.abs(s) + math.abs(s), + useExactDecimal = precision > 15 ), numeric = true, signedConversion = !isNegative(s) diff --git a/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet b/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet new file mode 100644 index 000000000..e01821d29 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet @@ -0,0 +1,11 @@ +std.assertEqual(std.format("%.16f", 3.14159265358979323), "3.1415926535897931") && +std.assertEqual(std.format("%.17f", 1.0 / 3.0), "0.33333333333333331") && +std.assertEqual(std.format("%.17f", 3.14159265358979323), "3.14159265358979312") && +std.assertEqual(std.format("%.17f", 0.1), "0.10000000000000001") && +std.assertEqual(std.format("%.17g", 3.14159265358979323), "3.1415926535897931") && +std.assertEqual(std.format("%.17g", 31.4159265358979323), "31.415926535897931") && +std.assertEqual(std.format("%.16f", 1.00000762939453125), "1.0000076293945312") && +std.assertEqual(std.format("%.16f", 1.00002288818359375), "1.0000228881835938") && +std.assertEqual(std.format("%.16f", -1.00000762939453125), "-1.0000076293945312") && +std.assertEqual(std.format("%.17g", 1.00000762939453125), "1.0000076293945312") && +true diff --git a/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet.golden new file mode 100644 index 000000000..27ba77dda --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet.golden @@ -0,0 +1 @@ +true From 14e7f03ed505d5998295c443fb1c296b5d7ce7a7 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Wed, 29 Jul 2026 17:47:13 +0800 Subject: [PATCH 2/2] test: add boundary and extreme cases for exact binary64 format Motivation: The original tests lacked coverage at the precision 15/16 threshold (where the code switches from legacy to exact path) and for extreme values like subnormals and the %g fixed-form path. Modification: Add assertions for: precision-15 boundary (pi), %.20f of 0.1 and 1e-10 (exact binary64 trailing digits), subnormal 5e-324 at %.16f, and %.20g exercising the useExactDecimal parameter propagation. Result: The precision threshold and edge cases are now regression-protected. --- .../new_test_suite/format_exact_binary64_precision.jsonnet | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet b/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet index e01821d29..4046cfe8a 100644 --- a/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet +++ b/sjsonnet/test/resources/new_test_suite/format_exact_binary64_precision.jsonnet @@ -8,4 +8,9 @@ std.assertEqual(std.format("%.16f", 1.00000762939453125), "1.0000076293945312") std.assertEqual(std.format("%.16f", 1.00002288818359375), "1.0000228881835938") && std.assertEqual(std.format("%.16f", -1.00000762939453125), "-1.0000076293945312") && std.assertEqual(std.format("%.17g", 1.00000762939453125), "1.0000076293945312") && +std.assertEqual(std.format("%.15f", 3.14159265358979323), "3.141592653589793") && +std.assertEqual(std.format("%.20f", 0.1), "0.10000000000000000555") && +std.assertEqual(std.format("%.20f", 1e-10), "0.00000000010000000000") && +std.assertEqual(std.format("%.16f", 5e-324), "0.0000000000000000") && +std.assertEqual(std.format("%.20g", 0.1), "0.10000000000000000555") && true