Skip to content

fix: round formatted doubles ties-to-even - #1116

Merged
stephenamar-db merged 1 commit into
databricks:masterfrom
He-Pin:fix/exact-binary64-format-rounding
Aug 11, 2026
Merged

fix: round formatted doubles ties-to-even#1116
stephenamar-db merged 1 commit into
databricks:masterfrom
He-Pin:fix/exact-binary64-format-rounding

Conversation

@He-Pin

@He-Pin He-Pin commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Motivation

Jsonnet documents std.format as following the same formatting rules as Python. sjsonnet previously mixed decimal sources and rounding mechanisms:

  • Common fixed-point paths rounded Scala's shortest decimal conversion with + 0.5 followed by FLOOR.
  • Scientific and generic low-precision paths used binary64 intermediate arithmetic followed by Math.round, while high-precision paths could switch to exact BigDecimal.

That made the result depend on the conversion path. For example, old sjsonnet produced 1.01 for std.format("%.2f", 1.005), while exact binary64 formatting produces 1.00; exact midpoint cases such as std.format("%.0f", 2.5) also produced 3 instead of Python's ties-to-even result 2.

The target contract in this PR is: format the represented finite IEEE 754 binary64 value, perform exactly one rounding step, and use round-to-nearest, ties-to-even.

Modification

  • Construct the rounding source from the exact binary64 value with BigDecimal.exact(number).
  • Shift the decimal point exactly and apply HALF_EVEN once for positive-precision fixed formatting and all scientific formatting.
  • Use math.rint for zero-precision fixed formatting.
  • Use the same exact rounding model when %g decides whether rounding crosses an exponent boundary.
  • Replace the half-away regression fixture with coverage for exact midpoint ties, non-decimal binary64 values, exponent carry, negative zero, large integers, and %f / %e / %g consistency.

Result

Cross-implementation comparison

The following outputs were measured locally with the same format/input pairs: one shared Jsonnet expression for the Jsonnet implementations and equivalent percent-format expressions in CPython. JSON string quotes are omitted in the table.

  • sjsonnet before: ce6652f0 (the PR base commit)
  • go-jsonnet: v0.21.0
  • jrsonnet: 0.5.0-pre99
  • CPython: 3.14.5
  • sjsonnet after: 00f52495 (this PR)
Format Input sjsonnet before go-jsonnet jrsonnet CPython sjsonnet after
%.0f 0.5 1 1 1 0 0
%.0f 2.5 3 3 3 2 2
%.1f 0.25 0.3 0.3 0.3 0.2 0.2
%.2f 0.015 0.02 0.02 0.02 0.01 0.01
%.2f 1.005 1.01 1.00 1.00 1.00 1.00
%.2f 2.675 2.68 2.68 2.68 2.67 2.67
%.0e 2.5e10 3e+10 3e+10 3e+10 2e+10 2e+10
%.1g 2.5 3 3 3 2 2

The table separates two issues:

  1. 1.005 exposes the old sjsonnet-only shortest-decimal path; go-jsonnet and jrsonnet happen to produce 1.00 after their binary64 intermediate rounds below the decimal midpoint.
  2. The other rows expose the half-away versus ties-to-even policy. The new sjsonnet behavior intentionally follows CPython rather than current go-jsonnet and jrsonnet behavior.

The same eight-case expression produced identical arrays for CPython and the new sjsonnet implementation.

Validation

  • ./mill __.test: 2065/2065 tasks passed across JVM, JS, Wasm, and Native.
  • ./mill __.checkFormat: 36/36 tasks passed.
  • Independent reference comparison against CPython percent formatting: 2,029,200 cases, zero differences.
  • Independent subAgent review: no must-fix findings.
  • Auditable qodercli review: no must-fix findings.

References and risks

References

Risks

  • This is an intentional compatibility change for users relying on current go-jsonnet / jrsonnet midpoint outputs. It affects %f, %e, and %g.
  • Exact BigDecimal / BigInt arithmetic replaces low-precision fast paths and can be slower in formatting-heavy workloads. This PR does not claim a performance-neutral change.
  • Non-finite numbers remain rejected upstream; this PR does not change that input contract.

Motivation: std.format mixed shortest-decimal and binary64 intermediate rounding, producing mathematically incorrect results such as 1.005 -> 1.01 and inconsistent behavior across precisions and conversions.

Modification: round the exact binary64 value once with HALF_EVEN for fixed, scientific, and generic formats, update exponent selection, and expand regression coverage.

Result: %f, %e, and %g now consistently follow Python-style round-to-nearest ties-to-even semantics across tested precisions and platforms.
@He-Pin
He-Pin marked this pull request as draft August 9, 2026 08:32
@He-Pin
He-Pin marked this pull request as ready for review August 9, 2026 09:09
@He-Pin
He-Pin marked this pull request as draft August 9, 2026 09:10
@He-Pin
He-Pin marked this pull request as ready for review August 11, 2026 13:13
@stephenamar-db
stephenamar-db merged commit 17be909 into databricks:master Aug 11, 2026
5 checks passed
@He-Pin
He-Pin deleted the fix/exact-binary64-format-rounding branch August 12, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants