Skip to content

fix: Specify how a numeric string's leading and trailing zeros render - #180

Open
leongdl wants to merge 1 commit into
OpenJobDescription:mainlinefrom
leongdl:fix/numeric-string-zero-handling
Open

fix: Specify how a numeric string's leading and trailing zeros render#180
leongdl wants to merge 1 commit into
OpenJobDescription:mainlinefrom
leongdl:fix/numeric-string-zero-handling

Conversation

@leongdl

@leongdl leongdl commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What

Adds §7.5 Numeric strings, which says what an <intstring>/<floatstring> renders as, and one conformance fixture that pins it for range elements.

The rule, in one line: leading zeros go, trailing zeros stay.

- name: Weight
  type: FLOAT
  range: ['2.50', '03.500', '0.50']   # -> 2.50   3.500   0.50

- name: Frame
  type: INT
  range: ['02', '003']                # -> 2      3

Why the spec needs to say this

<intstring> and <floatstring> are defined only as "a string whose value is the string representation of" a number. That says what the element denotes; it does not say what it renders as. Implementations read it both ways, and two already-landed fixtures pull in opposite directions:

Fixture Asserts Reading
base/jobs/2.3--int-param-intstring-default-resolves COUNT:7 from INT default: '007' text is not preserved
EXPR/jobs/expr1.3.4--float-passthrough PARAM:3.500 from FLOAT default: "3.500" text is preserved

Both are right, and the reason is that the two kinds of zero are not the same thing:

  • Leading zeros are redundant to the value. '02' and '2' ask for the same thing, and forwarding the text invokes a renderer with --frame 02.
  • Trailing zeros are not redundant to the request. A fixed number of decimal places is something renderers commonly require, and the string form is the only way a Template can ask for one — a <float> literal cannot, because 2.50 and 2.5 are the same literal after parsing.

This came out of review on openjd-model-for-python#342, which normalized range elements to the number they denote and so dropped the trailing zeros. openjd-model-for-python#345 reverts that half; this PR is the spec statement behind it.

base/jobs/3.4.1.2--float-range-floatstring-elements-normalized in #179 states the fully-normalizing reading for a FLOAT range element. Its own comment flagged that it and expr1.3.4--float-passthrough could not both be right and that a ruling was needed. This is the ruling, and 3.4.1.2 is the one that moves — see the note on #179 below.

What is deliberately left unspecified

Exponent notation and an explicit leading + are called out in §7.5 as unspecified in this revision, rather than left for a reader to infer. Measured across both implementations and two surfaces (job parameter default, task parameter range element), after the fixes in openjd-model-for-python#345 and openjd-rs#354:

Source Py default Py range rs default rs range
'1E+2' 1E+2 1E+2 1E+2 1E+2
'1e-3' 0.001 1e-3 1e-3 1e-3
'+2.50' 2.50 +2.50 +2.50 +2.50

The range surface now agrees; the default surface does not, and nothing pins it. So §7.5 says so and tells a Template author to write the value they want.

The fixture

base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml, covering INT and FLOAT range elements in one template. '0.50' is in there because its leading zero is not redundant — it is the whole integer part — so an implementation that strips leading zeros by text has to leave one digit behind.

One detail worth flagging for reviewers: the fixture brackets each value (W[2.50]) rather than delimiting with a colon. The runner matches expected output as a substring, so an assertion of W:2.5 is satisfied by a line reading W:2.50. The closing bracket is what makes the forbidden entries forbid anything. base/jobs/3.4.1.2 in #179 has exactly that defect and reports a pass against either behaviour — that is how the contradiction went unnoticed.

Verification

The fixture discriminates. Full 2023-09/* suite (1162 fixtures) run against both implementations, each from source, before and after their matching fix:

Implementation Baseline With the fix
openjd-model-for-python (e7a17b3 → #345) 1161 passed, 1 failed 1162 passed, 0 failed
openjd-rs (9a9998d → #354) 1161 passed, 1 failed 1162 passed, 0 failed

In both baselines the single failure is this fixture and nothing else, so it pins the new rule without disturbing any existing assertion. The Python runs used openjd-cli at mainline 7c7ece4 with openjd-sessions 0.10.14, the newest combination that CLI supports.

⚠️ CI will fail until both implementations release

Both conformance workflows install released versions — pip install openjd-cli and cargo install openjd-cli — and neither release has the fix:

  • Python: released 0.11.6 predates openjd-model-for-python#342 and forwards the text verbatim, so it fails on the leading-zero side, on the INT elements too.
  • Rust: released openjd-cli normalizes a FLOAT range through f64, so it fails on the trailing-zero side.

That is what CI now shows, and it is worth reading because the two implementations fail differently:

Job Result Missing
Conformance (Python), ubuntu 1161 passed, 1 failed W[3.500]
Conformance (Rust), ubuntu / windows / macos 1161 / 1116 / 1161 passed, 1 failed W[2.50]

In every job the only failure is this fixture, it parsed cleanly, and the job ran — the diagnostic is a missing expected line, not a syntax or schema error. Released Python drops the leading zero from '03.500' and so misses W[3.500]; released Rust normalizes '2.50' to 2.5 and so misses W[2.50]. The fixture discriminates in both directions.

One unrelated failure appears in the Python job on Windows, 3.4--path-parameter. It fails on mainline too — that workflow has been red on Windows since at least 2026-07-15 — so it is a pre-existing baseline failure, not a regression from this PR.

Verified passing against both fixes built from source, as above. Happy to split the fixture into a follow-up PR if the convention is to keep the spec change green, in the manner of #162#166.

Also left out

Negative zero written with decimal places ('-0.00') is not asserted. Both implementations render zero's sign away, but they differ on whether the decimal places survive it, and §7.5 has nothing to say about a sign on zero.

Related

@leongdl
leongdl requested a review from a team as a code owner September 1, 2026 02:52
@leongdl

leongdl commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Pending fixing openjd py and rs.

`<intstring>` and `<floatstring>` are defined only as "a string whose value is
the string representation of" a number, which says what such an element denotes
but not what it renders as. Implementations read it both ways, and both readings
were already pinned by landed conformance fixtures pulling in opposite
directions:

  base/jobs/2.3--int-param-intstring-default-resolves asserts COUNT:7 from an
  INT default of '007' -- the text is not preserved.

  EXPR/jobs/expr1.3.4--float-passthrough asserts PARAM:3.500 from a FLOAT
  default of "3.500" -- the text is preserved.

Both are right, because the two kinds of zero are not the same thing. Add §7.5
Numeric strings to say so, and cross-reference it from the four places
`<intstring>`/`<floatstring>` are defined.

Leading zeros are redundant to the value and are removed: '02' and '2' ask for
the same thing, and forwarding the text invokes a renderer with `--frame 02`.
The decimal places a <floatstring> is written with are preserved: '2.50' renders
2.50, because a fixed number of decimal places is a thing renderers require and
the string form is the only way a Template can ask for one -- a <float> literal
cannot, since 2.50 and 2.5 are the same literal after parsing.

Exponent notation and an explicit leading '+' are called out as unspecified in
this revision rather than left to be inferred. Implementations disagree on both
and no fixture pins either: measured across two implementations and two
surfaces, '1E+2' renders as 1E+2 in three cells and 100.0 in one, and '+2.50'
renders as +2.50 in two and 2.50 in two.

The new fixture pins both rules on the range-element surface, for INT and FLOAT.
'0.50' is included because its leading zero is not redundant -- it is the whole
integer part -- so a text-based strip must still leave one digit behind.

The fixture brackets each value (`W[2.50]`) instead of delimiting it with a
colon. The runner matches expected output as a substring, so an assertion of
`W:2.5` is satisfied by a line reading `W:2.50`; the closing bracket is what
makes the `forbidden` entries forbid anything. base/jobs/3.4.1.2 in OpenJobDescription#179 has
exactly that defect and reports a pass against either behaviour.

Verification. The fixture passes against openjd-model-for-python#345 and fails
against openjd-rs at 1b58c03, which renders W[2.5], W[3.5] and W[0.5] -- so it
discriminates rather than just describing one implementation. Both CI
conformance jobs install released versions (`pip install openjd-cli`,
`cargo install openjd-cli`) and will fail this fixture until both
implementations ship the fix; released Python 0.11.6 fails it on the INT side
too.

Zero-valued <floatstring>s ('0.00') are left out: openjd-rs collapses the value
to 0.0 and loses the written scale, which is a separate pre-existing defect that
also affects FLOAT parameter defaults.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
@leongdl
leongdl force-pushed the fix/numeric-string-zero-handling branch from ae3f433 to 6dce253 Compare September 4, 2026 22:30
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