From 75c5ddcab35693c77f298eb0c80c16ba01ec012f Mon Sep 17 00:00:00 2001 From: David Leong <116610336+leongdl@users.noreply.github.com> Date: Mon, 31 Aug 2026 19:52:09 -0700 Subject: [PATCH 1/2] fix: Specify how a numeric string's leading and trailing zeros render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `` and `` 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 ``/`` 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 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 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 #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 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> --- ...c-string-zeros-in-range-elements.test.yaml | 86 +++++++++++++++++++ wiki/2023-09-Template-Schemas.md | 52 ++++++++++- 2 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 conformance-tests/2023-09/base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml diff --git a/conformance-tests/2023-09/base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml b/conformance-tests/2023-09/base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml new file mode 100644 index 00000000..1b9b46b4 --- /dev/null +++ b/conformance-tests/2023-09/base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml @@ -0,0 +1,86 @@ +# The two kinds of zero in an / range element are not the same thing, and +# this pins both directions at once (Template Schemas §7.5, added alongside this fixture). +# +# Leading zeros are not part of the value and are removed. '02' on an INT range is the task +# value 2; forwarding the text invokes a renderer with `--frame 02`. Already pinned for a +# parameter default by base/jobs/2.3--int-param-intstring-default-resolves.test.yaml, which +# asserts COUNT:7 from default '007'. +# +# The decimal places a is written with are preserved. '2.50' renders 2.50, not +# 2.5. This is the reason the string form exists — a literal cannot ask for a fixed +# number of decimal places, because 2.50 and 2.5 are the same literal after parsing, and +# renderers commonly require a fixed number. Already pinned for a parameter default by +# EXPR/jobs/expr1.3.4--float-passthrough.test.yaml, which asserts PARAM:3.500 from default +# "3.500". This fixture extends the same rule to a range element. +# +# '0.50' is included because its leading zero is NOT redundant — it is the whole integer part — +# so an implementation that strips leading zeros by text must still leave one digit behind. +# +# Note for reviewers: the assertions bracket the value (`W[2.50]`) rather than delimiting it with +# a colon. The test runner matches expected output as a substring, so a bare `W:2.5` matches a +# line reading `W:2.50` and the fixture would pass against either behaviour. The closing bracket +# is what makes the `forbidden` entries below actually forbid anything. +# +# Deliberately not covered, because §7.5 leaves them unspecified and implementations differ: +# exponent notation ('1E+2'), an explicit leading '+' ('+2.50'), and zero-valued s +# ('0.00'), where one implementation collapses the value to 0.0 and loses the written scale. +# +# Spec references, Template Schemas 2023-09: +# §7.5 Numeric strings — how a numeric string's value is rendered, and the two zero rules +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L2034 +# §3.4.1.1 L1112 `` elements are ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1112 +# §3.4.1.2 L1187 `` elements are ` | ` +# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1187 +template: + specificationVersion: jobtemplate-2023-09 + name: TestJob + steps: + - name: FloatRange + parameterSpace: + taskParameterDefinitions: + - name: Weight + type: FLOAT + range: ['2.50', '03.500', '0.50', '1.5'] + script: + actions: + onRun: + command: python + args: + - -c + - print(r'W[{{Task.Param.Weight}}]') + - name: IntRange + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: INT + range: ['1', '02', '003'] + script: + actions: + onRun: + command: python + args: + - -c + - print(r'F[{{Task.Param.Frame}}]') +expected: + output: + # A keeps the decimal places it was written with, and loses only + # the leading zero of '03.500'. + - 'W[2.50]' + - 'W[3.500]' + - 'W[0.50]' + - 'W[1.5]' + # An renders as the integer it denotes. + - 'F[1]' + - 'F[2]' + - 'F[3]' + forbidden: + # Normalizing a to the number it denotes: the decimal places are + # part of what the element asked for. + - 'W[2.5]' + - 'W[3.5]' + - 'W[0.5]' + # Forwarding the source text unchanged: the leading zeros are not. + - 'W[03.500]' + - 'F[02]' + - 'F[003]' diff --git a/wiki/2023-09-Template-Schemas.md b/wiki/2023-09-Template-Schemas.md index 2cd3441d..9ff9f69c 100644 --- a/wiki/2023-09-Template-Schemas.md +++ b/wiki/2023-09-Template-Schemas.md @@ -315,7 +315,8 @@ userInterface: # @optional ``` -Where `` is a string whose value is the string representation of an integer value in base-10, and: +Where `` is a string whose value is the string representation of an integer value in base-10 +(see [Numeric strings](#75-numeric-strings) for how such a value is rendered), and: 1. *name* — The name by which the parameter is referenced. See: [<Identifier>](#71-identifier). 2. *description* — A description to apply to the parameter. It has no functional purpose, but may appear in UI elements. @@ -373,7 +374,8 @@ userInterface: # @optional ``` Where `` is a string whose value is the string representation of a floating point or integer value in -base-10, and: +base-10 (see [Numeric strings](#75-numeric-strings) for how such a value is rendered, including the decimal places it +was written with), and: 1. *name* — The name by which the parameter is referenced. See: [<Identifier>](#71-identifier). 2. *description* — A description to apply to the parameter. It has no functional purpose, but may appear in UI elements. @@ -1112,7 +1114,8 @@ With: Where `` is a string whose value is the string representation of an integer value in base-10, `` (See [<TaskParameterStringValue>](#342-taskparameterstringvalue)) must resolve to the -string representation of an integer value in base-10, and: +string representation of an integer value in base-10, both are rendered as described in +[Numeric strings](#75-numeric-strings), and: 1. *name* — The name of the parameter. 2. *type* — The literal "INT", defining this parameter as integer valued. @@ -1186,7 +1189,9 @@ With: Where `` is a string whose value is the string representation of a floating point value in base-10, `` (See [<TaskParameterStringValue>](#342-taskparameterstringvalue)) must resolve to the -string representation of a floating point value in base-10, and: +string representation of a floating point value in base-10, both are rendered as described in +[Numeric strings](#75-numeric-strings) — which preserves the decimal places the element was written with, so a range +element of `'2.50'` gives a Task the value `2.50` — and: 1. *name* — The name of the parameter. 2. *type* — The literal "FLOAT", defining this parameter as floating point valued. @@ -2026,6 +2031,45 @@ With the `EXPR` extension enabled, expressions that reference values not yet kno stage are type-checked using the declared types of those values, catching type errors as early as possible. See the [Expression Language](2026-02-Expression-Language) specification for details. +### 7.5. Numeric strings + +``, ``, and `` are the string forms of a number. A field +that accepts one of them accepts the number written as text wherever it would otherwise accept an +``, ``, or `` literal. + +Such a value is eventually rendered back into text — into a format string result, and from there +onto a Task's command line — so what it renders as is part of what the Template asked for. Two +rules govern the zeros in it, and they differ: + +1. **Redundant leading zeros are not part of the value, and are removed.** An `` of + `'007'` is the value 7, and renders `7`. A leading zero that is not redundant is kept: the `0` + in a `` of `'0.50'` is the whole of its integer part, and `'000'` denotes 0. So a + `` of `'02.50'` renders `2.50`. + +2. **The decimal places a `` is written with are part of what it asks for, and are + preserved.** A `` of `'2.50'` renders `2.50`, not `2.5`, and one of `'3.500'` + renders `3.500`. Renderers and other Task commands commonly require a fixed number of decimal + places, and writing the value as a string is how a Template asks for one. A `` literal + cannot do this, because the number of decimal places a literal was written with is not + preserved through parsing — a `` of `2.50` is the same value as `2.5`. + +An `` and a `` have no decimal places, so rule 1 is the whole of their +behaviour: each renders as the integer it denotes. + +Rule 2 is the reason the two rules are not one rule. Both kinds of zero are redundant to the +*value*, but only the leading ones are redundant to the *request*: `'02'` and `'2'` ask for the +same thing, while `'2.50'` and `'2.5'` do not. + +Two things are deliberately left unspecified in this revision, because no conformance fixture +pins them and implementations differ: + +* Whether a numeric string written in exponent notation renders in exponent notation. A + `` of `'1E+2'` may render `1E+2` or `100`. +* Whether an explicit leading `+` is preserved. A `` of `'+2.50'` may render + `+2.50` or `2.50`. + +A Template that needs a specific answer to either should write the value it wants. + ## 8. `` This object is only available in the extension `FEATURE_BUNDLE_1` From df3508cef69a28a01df004c27c3917ffcbec1f99 Mon Sep 17 00:00:00 2001 From: David Leong <116610336+leongdl@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:07:08 -0700 Subject: [PATCH 2/2] fix: Rule on a numeric string's exponent and leading sign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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> --- ...c-string-zeros-in-range-elements.test.yaml | 61 +++++++------------ wiki/2023-09-Template-Schemas.md | 51 ++++++---------- 2 files changed, 40 insertions(+), 72 deletions(-) diff --git a/conformance-tests/2023-09/base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml b/conformance-tests/2023-09/base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml index 1b9b46b4..9f591ccd 100644 --- a/conformance-tests/2023-09/base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml +++ b/conformance-tests/2023-09/base/jobs/7.5--numeric-string-zeros-in-range-elements.test.yaml @@ -1,37 +1,13 @@ -# The two kinds of zero in an / range element are not the same thing, and -# this pins both directions at once (Template Schemas §7.5, added alongside this fixture). +# Template Schemas §7.5: a numeric string loses notation redundant to its value (leading zeros, a +# leading '+') and keeps notation it was written in (decimal places, an exponent). # -# Leading zeros are not part of the value and are removed. '02' on an INT range is the task -# value 2; forwarding the text invokes a renderer with `--frame 02`. Already pinned for a -# parameter default by base/jobs/2.3--int-param-intstring-default-resolves.test.yaml, which -# asserts COUNT:7 from default '007'. +# Both halves over FLOAT and INT range elements. '0.50' is here because its leading zero is the +# whole integer part. The exponent elements carry a dot ('2.50E+2', not '1E+2') because the runner +# re-dumps the template through PyYAML, which leaves a dotless exponent unquoted, and a YAML 1.2 +# parser then reads it as a literal that makes no request at all. Assertions bracket the +# value because the runner substring-matches, so a bare 'W:2.5' is satisfied by 'W:2.50'. # -# The decimal places a is written with are preserved. '2.50' renders 2.50, not -# 2.5. This is the reason the string form exists — a literal cannot ask for a fixed -# number of decimal places, because 2.50 and 2.5 are the same literal after parsing, and -# renderers commonly require a fixed number. Already pinned for a parameter default by -# EXPR/jobs/expr1.3.4--float-passthrough.test.yaml, which asserts PARAM:3.500 from default -# "3.500". This fixture extends the same rule to a range element. -# -# '0.50' is included because its leading zero is NOT redundant — it is the whole integer part — -# so an implementation that strips leading zeros by text must still leave one digit behind. -# -# Note for reviewers: the assertions bracket the value (`W[2.50]`) rather than delimiting it with -# a colon. The test runner matches expected output as a substring, so a bare `W:2.5` matches a -# line reading `W:2.50` and the fixture would pass against either behaviour. The closing bracket -# is what makes the `forbidden` entries below actually forbid anything. -# -# Deliberately not covered, because §7.5 leaves them unspecified and implementations differ: -# exponent notation ('1E+2'), an explicit leading '+' ('+2.50'), and zero-valued s -# ('0.00'), where one implementation collapses the value to 0.0 and loses the written scale. -# -# Spec references, Template Schemas 2023-09: -# §7.5 Numeric strings — how a numeric string's value is rendered, and the two zero rules -# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L2034 -# §3.4.1.1 L1112 `` elements are ` | ` -# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1112 -# §3.4.1.2 L1187 `` elements are ` | ` -# https://github.com/OpenJobDescription/openjd-specifications/blob/mainline/wiki/2023-09-Template-Schemas.md?plain=1#L1187 +# A normalizing implementation misses W[2.50] and W[2.50E+2]; one forwarding text misses F[2]. template: specificationVersion: jobtemplate-2023-09 name: TestJob @@ -41,7 +17,7 @@ template: taskParameterDefinitions: - name: Weight type: FLOAT - range: ['2.50', '03.500', '0.50', '1.5'] + range: ['2.50', '03.500', '0.50', '1.5', '2.50E+2', '1.0e-3', '+4.25'] script: actions: onRun: @@ -54,7 +30,7 @@ template: taskParameterDefinitions: - name: Frame type: INT - range: ['1', '02', '003'] + range: ['1', '02', '003', '+7'] script: actions: onRun: @@ -64,23 +40,28 @@ template: - print(r'F[{{Task.Param.Frame}}]') expected: output: - # A keeps the decimal places it was written with, and loses only - # the leading zero of '03.500'. - 'W[2.50]' - 'W[3.500]' - 'W[0.50]' - 'W[1.5]' - # An renders as the integer it denotes. + - 'W[2.50E+2]' + - 'W[1.0e-3]' + - 'W[4.25]' - 'F[1]' - 'F[2]' - 'F[3]' + - 'F[7]' forbidden: - # Normalizing a to the number it denotes: the decimal places are - # part of what the element asked for. + # Normalizing to the number the element denotes. - 'W[2.5]' - 'W[3.5]' - 'W[0.5]' - # Forwarding the source text unchanged: the leading zeros are not. + - 'W[250]' + - 'W[250.0]' + - 'W[0.001]' + # Forwarding the source text unchanged. - 'W[03.500]' + - 'W[+4.25]' - 'F[02]' - 'F[003]' + - 'F[+7]' diff --git a/wiki/2023-09-Template-Schemas.md b/wiki/2023-09-Template-Schemas.md index 9ff9f69c..97c0a573 100644 --- a/wiki/2023-09-Template-Schemas.md +++ b/wiki/2023-09-Template-Schemas.md @@ -2037,38 +2037,25 @@ as possible. See the [Expression Language](2026-02-Expression-Language) specific that accepts one of them accepts the number written as text wherever it would otherwise accept an ``, ``, or `` literal. -Such a value is eventually rendered back into text — into a format string result, and from there -onto a Task's command line — so what it renders as is part of what the Template asked for. Two -rules govern the zeros in it, and they differ: - -1. **Redundant leading zeros are not part of the value, and are removed.** An `` of - `'007'` is the value 7, and renders `7`. A leading zero that is not redundant is kept: the `0` - in a `` of `'0.50'` is the whole of its integer part, and `'000'` denotes 0. So a - `` of `'02.50'` renders `2.50`. - -2. **The decimal places a `` is written with are part of what it asks for, and are - preserved.** A `` of `'2.50'` renders `2.50`, not `2.5`, and one of `'3.500'` - renders `3.500`. Renderers and other Task commands commonly require a fixed number of decimal - places, and writing the value as a string is how a Template asks for one. A `` literal - cannot do this, because the number of decimal places a literal was written with is not - preserved through parsing — a `` of `2.50` is the same value as `2.5`. - -An `` and a `` have no decimal places, so rule 1 is the whole of their -behaviour: each renders as the integer it denotes. - -Rule 2 is the reason the two rules are not one rule. Both kinds of zero are redundant to the -*value*, but only the leading ones are redundant to the *request*: `'02'` and `'2'` ask for the -same thing, while `'2.50'` and `'2.5'` do not. - -Two things are deliberately left unspecified in this revision, because no conformance fixture -pins them and implementations differ: - -* Whether a numeric string written in exponent notation renders in exponent notation. A - `` of `'1E+2'` may render `1E+2` or `100`. -* Whether an explicit leading `+` is preserved. A `` of `'+2.50'` may render - `+2.50` or `2.50`. - -A Template that needs a specific answer to either should write the value it wants. +Such a value is rendered back into text — into a format string result, and from there onto a Task's +command line — so how it was written is part of what the Template asked for. Two rules govern that: + +1. **Notation redundant to the value is removed.** Redundant leading zeros and a leading `+` both + are: an `` of `'007'` renders `7`, and a `` of `'+2.50'` renders `2.50`. + A leading zero that is not redundant is kept, so `'0.50'` renders `0.50` and `'02.50'` renders + `2.50`. + +2. **Notation the value was written in is preserved.** Decimal places and an exponent both are: + `'2.50'` renders `2.50` and not `2.5`, and `'1E+2'` renders `1E+2` and not `100`. An exponent + keeps the case of its marker and the sign inside it. Writing the value as a string is the only + way a Template can ask for a fixed number of decimal places, which renderers commonly require; + a `` literal cannot, because `2.50` and `2.5` are the same literal after parsing. + +An `` and a `` have no decimal places and no exponent, so rule 1 is the +whole of their behaviour: each renders as the integer it denotes. + +The sign of a zero is left unspecified in this revision: a `` of `'-0.00'` may render +`-0.00` or `0.00`. ## 8. ``