Repository navigation
Specify sequence units - #161
jessealama wants to merge 33 commits into
Conversation
|
eemeli
left a comment
There was a problem hiding this comment.
This PR also refactors
Number.prototype.toExponentialin place. [...][...] To support them,
GetUnsignedRoundingMode, its table of unsigned rounding modes, andApplyUnsignedRoundingModeare 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
left a comment
There was a problem hiding this comment.
Some comments on the approach.
| 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; |
There was a problem hiding this comment.
How about this?
| and no entry may be negative while another is positive; | |
| and all non-zero entries must have the same sign; |
| 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, |
There was a problem hiding this comment.
Why require "without an offset"?
| 1. Return _O_.[[AmountValue]]. | ||
| 1. Let _v_ be _O_.[[AmountValue]]. | ||
| 1. If _v_ is a List, then | ||
| 1. Let _array_ be CreateArrayFromList(_v_). |
There was a problem hiding this comment.
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.
|
Marking this as a draft while we work on splitting this up. |
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.
f654432 to
7905121
Compare
This PR adds spec text for sequence units. The unit identifier grammar gains a
SequenceUnitIdentifierproduction 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 thevaluegetter. 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.toStringrenders each entry with its component unit.convertToneeded 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.toLocaleStringformats through proposal-intl-sequence-units'FormatNumericSequence.This work builds on convert-options (#110) because we need
SelectTargetUnitand the unit category data.This PR also refactors
Number.prototype.toExponentialin place. Its rendering steps move into a sharedRenderInExponentialNotationabstract 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.
RoundToFractionDigitsandRoundToSignificantDigitsround a mathematical value directly. To support them,GetUnsignedRoundingMode, its table of unsigned rounding modes, andApplyUnsignedRoundingModeare 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.CreateFormatterObjectand the detour throughFormatNumericToStringand its formatted string are gone.The README is updated, too.