Skip to content

Commit c5677c3

Browse files
committed
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: - #1097 - https://jsonnet.org/ref/stdlib.html#std-format
1 parent a638dc4 commit c5677c3

4 files changed

Lines changed: 32 additions & 4 deletions

File tree

sjsonnet/src/sjsonnet/DecimalFormat.scala

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,16 @@ object DecimalFormat {
1919
hashes: Int,
2020
alternate: Boolean,
2121
expLengthOpt: Option[Int],
22-
number: Double): String = {
22+
number: Double): String =
23+
format(zeroes, hashes, alternate, expLengthOpt, number, useExactDecimal = false)
24+
25+
private[sjsonnet] def format(
26+
zeroes: Int,
27+
hashes: Int,
28+
alternate: Boolean,
29+
expLengthOpt: Option[Int],
30+
number: Double,
31+
useExactDecimal: Boolean): String = {
2332
expLengthOpt match {
2433
case Some(expLength) =>
2534
var expNum =
@@ -75,9 +84,15 @@ object DecimalFormat {
7584
if (alternate) prefix + "." else prefix
7685
} else {
7786
val denominator = BigDecimal(10).pow(precision)
78-
val bd = BigDecimal(number).abs
87+
val exactDecimal = useExactDecimal || precision > 15
88+
val bd =
89+
if (exactDecimal) BigDecimal.exact(number).abs
90+
else BigDecimal(number).abs
7991
val scaled =
80-
(bd * denominator + BigDecimal("0.5")).setScale(0, BigDecimal.RoundingMode.FLOOR)
92+
if (exactDecimal)
93+
(bd * denominator).setScale(0, BigDecimal.RoundingMode.HALF_EVEN)
94+
else
95+
(bd * denominator + BigDecimal("0.5")).setScale(0, BigDecimal.RoundingMode.FLOOR)
8196
val wholeBD = (scaled / denominator).setScale(0, BigDecimal.RoundingMode.FLOOR)
8297
val fracBD = (scaled - wholeBD * denominator).abs
8398

sjsonnet/src/sjsonnet/Format.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1222,7 +1222,8 @@ object Format {
12221222
if (formatted.alternate) 0 else fractionalPrecision,
12231223
formatted.alternate,
12241224
None,
1225-
math.abs(s)
1225+
math.abs(s),
1226+
useExactDecimal = precision > 15
12261227
),
12271228
numeric = true,
12281229
signedConversion = !isNegative(s)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
std.assertEqual(std.format("%.16f", 3.14159265358979323), "3.1415926535897931") &&
2+
std.assertEqual(std.format("%.17f", 1.0 / 3.0), "0.33333333333333331") &&
3+
std.assertEqual(std.format("%.17f", 3.14159265358979323), "3.14159265358979312") &&
4+
std.assertEqual(std.format("%.17f", 0.1), "0.10000000000000001") &&
5+
std.assertEqual(std.format("%.17g", 3.14159265358979323), "3.1415926535897931") &&
6+
std.assertEqual(std.format("%.17g", 31.4159265358979323), "31.415926535897931") &&
7+
std.assertEqual(std.format("%.16f", 1.00000762939453125), "1.0000076293945312") &&
8+
std.assertEqual(std.format("%.16f", 1.00002288818359375), "1.0000228881835938") &&
9+
std.assertEqual(std.format("%.16f", -1.00000762939453125), "-1.0000076293945312") &&
10+
std.assertEqual(std.format("%.17g", 1.00000762939453125), "1.0000076293945312") &&
11+
true
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
true

0 commit comments

Comments
 (0)