Skip to content

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

Open
leongdl wants to merge 2 commits 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 2 commits 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.

Rulings added in review

Review asked for a specific choice on the two cases §7.5 first left unspecified, rather than leaving a reader to infer one. Both follow the split the section already makes, so they landed inside the existing two rules rather than as exceptions:

Case Ruling Rule it joins
Exponent notation '1E+2' renders 1E+2, keeping the case of its marker and the sign inside it 2, with decimal places
Leading + '+2.50' renders 2.50 1, with redundant leading zeros

§7.5 went from 36 lines to 24 in the process. Only the sign of a zero is still open, and that is now one sentence rather than a bulleted list.

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. Header trimmed from 34 comment lines to 10; every other fixture in that directory has at most 4.

Two details worth flagging for reviewers.

The assertions bracket 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.

The exponent elements are spelled with a dot ('2.50E+2', not '1E+2') because the runner re-dumps the template through PyYAML, whose YAML 1.1 resolver needs a dot before an exponent and so leaves 1E+2 unquoted. A YAML 1.2 parser then reads that plain scalar as a number, i.e. a <float> literal, which makes no request about rendering at all. Measured: openjd-rs renders 100.0 for an unquoted 1E+2 and 2.50E+2 for the quoted form. Asserting on '1E+2' here would have tested scalar resolution rather than §7.5.

Verification

Measured against both implementations at their current mainline, after the rulings above:

Implementation Scope Result
openjd-rs d67bfb8 full 2023-09/* 1178 passed, 1 failed
openjd-model 0.11.9 + openjd-cli 0.7.7 2023-09/base/* 656 passed, 1 failed

In both, the single failure is this fixture and nothing else, and in both it fails on the leading + alone: they render W[+4.25] where §7.5 now requires W[4.25]. Every other element passes, including both exponent elements, so the exponent ruling ratifies what both already ship on this surface and only the + ruling needs code.

The Python full suite was not run to completion: it wedges on an unrelated EXPR range-cap fixture (a LIST[INT] range supplied beyond the list cap) that ran for minutes without finishing. Nothing to do with this change, but it is why the Python row is scoped to base.

Per-element, both surfaces:

Source Surface openjd-rs Python §7.5
'2.50' FLOAT range 2.50 2.50 2.50
'03.500' FLOAT range 3.500 3.500 3.500
'0.50' FLOAT range 0.50 0.50 0.50
'2.50E+2' FLOAT range 2.50E+2 2.50E+2 2.50E+2
'1.0e-3' FLOAT range 1.0e-3 1.0e-3 1.0e-3
'+4.25' FLOAT range +4.25 +4.25 4.25
'02', '003', '+7' INT range 2, 3, 7 2, 3, 7 same
'+2.50' FLOAT default +2.50 2.50 2.50
'03.500' FLOAT default 03.500 3.500 3.500
'2.50E+2' FLOAT default 2.50E+2 250 2.50E+2
'1.0e-3' FLOAT default 1.0e-3 0.0010 1.0e-3
'+7' INT default 7 7 7

The fixture pins the range rows only. The default rows are unpinned by any fixture and are filed against the implementations rather than widened into this PR: openjd-rs applies no §7.5 rule 1 at all on that surface (coerce_from_str, create_job/parameters.rs), and Python renders it with str(Decimal(...)) semantics, which keep significant digits but not the written notation.

CI will fail until both implementations release

Both conformance workflows install released versions (pip install openjd-cli, cargo install openjd-cli), so this fixture fails there until the + fix ships in each. Released Python 0.11.6 additionally predates openjd-model-for-python#345 and fails on the leading-zero side too.

One unrelated failure appears in the Python job on Windows, 3.4--path-parameter. It fails on mainline too, so it is a pre-existing baseline failure and not a regression from this PR.

Also left out

The sign of a zero. Both implementations render zero's sign away but differ on whether the decimal places survive it, so §7.5 says the case is unspecified rather than guessing.

Related

@leongdl
leongdl requested a review from a team as a code owner September 1, 2026 02:52
seant-aws
seant-aws previously approved these changes Sep 1, 2026
@leongdl

leongdl commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Pending fixing openjd py and rs.

@leongdl
leongdl force-pushed the fix/numeric-string-zero-handling branch from ae3f433 to 6dce253 Compare September 4, 2026 22:30
wyongzhi
wyongzhi previously approved these changes Sep 8, 2026
`<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 6dce253 to 75c5ddc Compare September 9, 2026 00:05
Comment thread wiki/2023-09-Template-Schemas.md Outdated
Comment thread wiki/2023-09-Template-Schemas.md Outdated
Review asked for a choice on the two cases §7.5 left unspecified, rather than
leaving a reader to infer one. Both follow the split the section already makes,
so state them inside the existing two rules rather than as exceptions:

  An exponent is notation the value was written in, like its decimal places, so
  it is preserved. '1E+2' renders 1E+2, keeping the case of its marker and the
  sign inside it.

  A leading '+' is redundant to the value, like a redundant leading zero, so it
  is removed. '+2.50' renders 2.50.

§7.5 goes from 36 lines to 24 in the process. The "left unspecified" list is
gone; only the sign of a zero is still open, and that is now one sentence.

Extend the fixture to both rulings, and trim its header from 34 comment lines
to 10 -- every other fixture in that directory has at most 4.

The exponent elements are spelled with a dot ('2.50E+2', not '1E+2') because
the runner re-dumps the template through PyYAML, which leaves a dotless
exponent unquoted. A YAML 1.2 parser then reads it as a <float> literal, which
makes no request about rendering at all, so the assertion would have tested
scalar resolution rather than §7.5. Measured: openjd-rs renders 100.0 for an
unquoted 1E+2 and 2.50E+2 for the quoted form -- same input, different YAML
dialect.

Measured against openjd-rs at mainline d67bfb8, full 2023-09 suite: 1178
passed, 1 failed. The one failure is this fixture, on the leading '+' alone --
it renders W[+4.25] where §7.5 now requires W[4.25]. openjd-model 0.11.9 with
openjd-cli 0.7.7 fails the fixture on exactly the same element and nothing
else. So the exponent ruling ratifies what both already ship on this surface,
and only the '+' ruling needs code.

Signed-off-by: David Leong <116610336+leongdl@users.noreply.github.com>
@leongdl
leongdl dismissed stale reviews from seant-aws and wyongzhi via df3508c September 9, 2026 03:07
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.

4 participants