Skip to content

Specify sequence units - #161

Draft
jessealama wants to merge 33 commits into
mainfrom
sequence-units
Draft

jessealama wants to merge 33 commits into
mainfrom
sequence-units

Conversation

@jessealama

@jessealama jessealama commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

This PR adds spec text for sequence units. The unit identifier grammar gains a SequenceUnitIdentifier production and reserves the segment "and", so the components of a unit identifier are found by splitting it at "-and-". Amounts with a sequence unit store one entry per component as a List in [[AmountValue]] and return a fresh frozen Array from the value getter. The constructor accepts either an Array or an object with one property per component; every entry but the last must be an integer, and no entry may be negative while another is positive. toString renders each entry with its component unit.

convertTo needed the biggest update. A sequence source is validated and totalled exactly in its smallest component before converting, and the source is now checked before the options are read for every kind of source. A sequence target is produced by converting to the smallest component, rounding there, and splitting with truncating division so that every entry has the sign of the source, negative zero included. Convertibility of a sequence is decided from the CLDR conversion factors. toLocaleString formats through proposal-intl-sequence-units' FormatNumericSequence.

This work builds on convert-options (#110) because we need SelectTargetUnit and the unit category data.

This PR also refactors Number.prototype.toExponential in place. Its rendering steps move into a shared RenderInExponentialNotation abstract operation, which Amount uses for every value it renders in exponential notation, so the two cannot drift apart. As part of that, a Number value is now rendered with the fewest significant digits that identify it, so an Amount holding the Number 100 renders as "1e+2" rather than "1.00e+2".

Rounding under precision options no longer goes through an Intl formatter. RoundToFractionDigits and RoundToSignificantDigits round a mathematical value directly. To support them, GetUnsignedRoundingMode, its table of unsigned rounding modes, and ApplyUnsignedRoundingMode are promoted unchanged from ECMA-402 to the Mathematical Operations clause of ECMA-262, and the ECMA-402 amendments delete them from their old home. They are pure arithmetic and the canonical definitions of the nine rounding modes. CreateFormatterObject and the detour through FormatNumericToString and its formatted string are gone.

The README is updated, too.

@github-actions

github-actions Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://tc39.github.io/proposal-amount/pr-preview/pr-161/

Built to branch gh-pages at 2026-09-29 23:38 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@jessealama
jessealama requested a review from eemeli September 10, 2026 14:01
@jessealama
jessealama added this pull request to stack #162 September 10, 2026 15:03

@eemeli eemeli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR also refactors Number.prototype.toExponential in place. [...]

[...] To support them, GetUnsignedRoundingMode, its table of unsigned rounding modes, and ApplyUnsignedRoundingMode are promoted unchanged from ECMA-402 to the Mathematical Operations clause of ECMA-262, and the ECMA-402 amendments delete them from their old home.

Could these changes be spun off into a separate PR, with the sequence unit changes building on top of it? That would make review quite a bit easier.

That PR doesn't need to be editorial only, just the parts of this that aren't about sequence units specifically.

@jessealama

Copy link
Copy Markdown
Collaborator Author

This PR also refactors Number.prototype.toExponential in place. [...]
[...] To support them, GetUnsignedRoundingMode, its table of unsigned rounding modes, and ApplyUnsignedRoundingMode are promoted unchanged from ECMA-402 to the Mathematical Operations clause of ECMA-262, and the ECMA-402 amendments delete them from their old home.

Could these changes be spun off into a separate PR, with the sequence unit changes building on top of it? That would make review quite a bit easier.

That PR doesn't need to be editorial only, just the parts of this that aren't about sequence units specifically.

Sounds good. I'll break up the arithmetic stuff into a separate PR.

@gibson042 gibson042 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments on the approach.

Comment thread README.md
and all except for the last Array entry must represent an integer;
otherwise a TypeError is thrown.
All except the last entry must represent an integer,
and no entry may be negative while another is positive;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this?

Suggested change
and no entry may be negative while another is positive;
and all non-zero entries must have the same sign;

Comment thread README.md
In other words, conversion to `foot-and-inch` is valid, but
`inch-and-foot`, `foot-and-centimeter`, and `foot-and-gallon` are not valid conversion targets.
If either the source or the target is a sequence unit,
a TypeError is thrown unless its component units share a CLDR base unit without an offset,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why require "without an offset"?

Comment thread spec.emu
1. Return _O_.[[AmountValue]].
1. Let _v_ be _O_.[[AmountValue]].
1. If _v_ is a List, then
1. Let _array_ be CreateArrayFromList(_v_).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning fresh objects from a getter makes for bad DX—amount.value !== amount.value is just really unpleasant behavior. I'd much prefer always returning the same frozen array.

@jessealama

Copy link
Copy Markdown
Collaborator Author

Marking this as a draft while we work on splitting this up.

Base automatically changed from convert-options to main September 28, 2026 01:16
A SequenceUnitIdentifier joins two or more SingleUnitIdentifiers with
-and-, and the segment and is reserved so the split is unambiguous. A
UnitComponents static semantics and a GetUnitComponents abstract
operation expose the component list to the rest of the specification.
The draft does not yet publish a biblio.json, so this is generated
locally with ecmarkup --write-biblio after adding a location to its
metadata. It lets the ECMA-402 text cross-reference the draft's abstract
operations. Once tc39/proposal-intl-sequence-units#15
lands and a biblio is published, this vendored copy can be dropped in
favour of the published one.
A sequence-unit Amount stores a List in [[AmountValue]], one entry per
component. The constructor accepts an Array of entries or an object with
one property per component, validates that every entry but the last is
an integer and that entries do not mix signs, and rounds only the last
entry under fractionDigits. The value getter returns a fresh frozen
Array for such Amounts. The single-unit rounding steps move into a
RoundAmountValue operation shared by both paths.
Entries are rendered with their component units and joined by a comma
and a space. Entries other than the last are plain integers; the last
entry uses the exponential form of a single-unit value. The single-value
rendering moves into RenderAmountValue so both paths share it.
A sequence unit is convertible when its components share a base unit
with no offset and each is an integer multiple of at least 2 of the
next, checked by GetSequenceUnitData. A sequence source is totalled
exactly in its smallest component and converted once. A sequence target
is produced by converting to the smallest component, rounding there so
carries happen, and splitting with truncating division so every entry
shares the sign of the value. Conversion sources are now described by a
Conversion Source Record so usage-based target selection works for
sequence-unit Amounts and for sequence-unit preferences.
The ECMA-402 toLocaleString hands the stored entries to the Intl
Sequence Units proposal's FormatNumericSequence as Intl mathematical
values, after constructing an Intl.NumberFormat with the sequence unit.
The proposal is recorded as a normative dependency.
Document the plain-object constructor form, use RangeError for
non-integer and mixed-sign entries, describe conversion as a single
conversion of the smallest-unit total, and replace Euclidean division
with truncating division so negative values keep one sign.
The operation was renamed NumericEntryValue when its parameter changed
from an Amount to a single stored value, but the Amount prefix is what
signals that the operation belongs to this specification. Keep the old
name and the new per-entry parameter.
Number::toString renders negative zero as 0, so the sign had to be
re-attached after parsing. Handling negative zero up front, next to NaN
and the infinities, produces the same string without the fixup.
RenderAmountValue obtained a significant-digit count for a Number by
parsing Number::toString output as an Intl mathematical value, which
counts trailing integer zeros and so rendered 30 as 3.0e+1 and 100 as
1.00e+2, implying precision a Number does not carry. Choose the digits
the way Number::toString and Number.prototype.toExponential do instead,
so 30 renders as 3e+1. String and BigInt values are unchanged.
Move RenderInExponentialNotation out of the Amount abstract operations
into a new Amendments to ECMA-262 section, and amend
Number.prototype.toExponential so that, once it has chosen the integer
significand and exponent, it renders them by calling the shared
operation instead of assembling the string inline. The zero case sets
the integer significand to 0 so the same call covers it. This resolves
the editor's note asking for a shared operation and guarantees that
Amount's exponential notation and toExponential cannot drift apart.
Mirror the ECMA-262 clause hierarchy (Numbers and Dates, Number
Objects, Properties of the Number Prototype Object) at the top of the
specification instead of a separate amendments section, add
RenderInExponentialNotation there directly before
Number.prototype.toExponential, and mark the changed steps of
toExponential with ins and del against the current ECMA-262 text.
In the constructor note, mark the object-form example { foot: 5,
inch: 11 } as a spec value. In the convertTo note, format the worked
example as code and say that totalling a sequence source in its
smallest component happens without an observable intermediate Amount;
the [5, 12] rebalancing example is dropped from that note.
GetUnitComponents asserts that its argument parses as a UnitIdentifier.
Units reached through options are validated on entry, but units taken
from CLDR unit preference data were relied on without saying so. Say in
the Unit Preference Data clause that every preferred unit is a unit
identifier, and assert it in GetPreferredUnits where the string enters
the specification.
The reserved and segment guarantees that splitting at -and- yields
exactly the components, so the grammar-walking UnitComponents
syntax-directed operation is not needed.
RoundToDecimalDigitString takes the rounded value from
[[RoundedNumber]], so negative values keep their sign under
precision options, and reads only the digit count from the unsigned
[[FormattedString]]. Parsing that string directly instead of going
through ToIntlMathematicalValue avoids overflowing to positive
infinity for values beyond Number range. RoundAmountValue,
SplitIntoSequence, and convertTo all round through it, and
RenderAmountValue renders BigInts exactly.
SequenceTotal previously rounded a String or BigInt last entry to a
Number before adding it to the exact sum of the other entries, so the
result could be rounded twice.
toLocaleString let the caller override the style or unit options
for a sequence-unit Amount, which would hand FormatNumericSequence a
formatter that is not a sequence-unit formatter. Those options now
throw a TypeError for sequence Amounts. Both paths now pass an Intl
MV Record to the formatting operations, as their signatures require,
which also keeps BigInt values exact.
…urce

SplitIntoSequence produced negative zero only for a zero last entry;
zero entries in earlier positions became positive zero.
convertTo checked the components of a sequence-unit source before
reading any option, but let a single-unit source with no conversion
data through until ConvertUnitValue ran. GetAmountConversionSource
now checks both kinds of source, so every conversion validates the
source first, then the options, then the target.
Any object with the component properties is accepted, not only a
plain object. foot-and-centimeter is not an example of a sequence
whose ratio is fractional, because centimeter has no conversion data
of its own. Sequence totals are now computed exactly.
The notes keep only what the algorithms do not already say.
The rendered spec opened with the diff against ECMA-262 before its
own introduction. The amendment now follows the Amount object and
the introduction lists it as a third part.
RenderSequenceEntries walks the stored List once, rendering every
entry but the last as a plain integer and the last as a single-unit
value, and pairs each with its component. It replaces the per-entry
RenderIntegerEntry, which had a single caller inside toString.
RenderInteger only spelled out the decimal representation of an
integer, which the spec can state directly. Negative zero is the one
case that needs a step of its own.
The record is resolved by the time it is passed in, so the name
need not say so.
RoundToFractionDigits and RoundToSignificantDigits each take the
digit count and rounding mode they need. RoundAmountValue dispatches
to one or the other, and SplitIntoSequence, which only ever rounds
to fraction digits, calls the first directly.
SelectTargetUnit, RoundAmountValue, and SplitIntoSequence are handed
an already resolved Amount Options Record. Only GetAmountOptions,
which builds that record, keeps the resolvedOpts name.
RoundToFractionDigits and RoundToSignificantDigits now bracket the
value between its two nearest candidates and choose one with
ECMA-402's ApplyUnsignedRoundingMode, which is pure arithmetic. They
return the rounded mathematical value. RenderWithFractionDigits
turns a fraction-digit rounding into a decimal digit string with the
same digit count the formatted string used to carry, and significant
digit roundings render with their digit count directly.

CreateFormatterObject, the detour through FormatNumericToString, and
the re-parse of its formatted string are gone.
…-262

Both operations, and the unsigned rounding mode table between them,
move unchanged from ECMA-402 into the Mathematical Operations clause
of ECMA-262, where the Amount rounding operations call them. The
ECMA-402 amendments delete them from their old home.
RoundAmountValue now takes a Number, a BigInt, or a String, matching
the type of [[AmountValue]] and the sibling operations
GetAmountNumericValue and RenderAmountValue, and parses a String
itself. ReadSequenceEntries returns Strings too, and normalises
non-final String entries to plain integer strings while it has the
parse in hand, so the constructor's sequence loop reduces to
rounding the last entry.

This branch has not been deployed

No deployments
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.

3 participants