Sync partitionDurationFormatPattern with latest spec draft

Sync `partitionDurationFormatPattern` with the latest spec draft and
change it to use an `Intl.DurationFormat` object as the input, so it's
easier to compare it against the spec text and because it allows to test
more inputs.

Includes the fixes for:
- https://github.com/tc39/proposal-intl-duration-format/pull/183
- https://github.com/tc39/proposal-intl-duration-format/pull/184
This commit is contained in:
André Bargull 2024-01-09 11:13:29 +01:00 committed by Ms2ger
parent b37947f3a4
commit 330ecdd016
22 changed files with 217 additions and 152 deletions

View File

@ -2509,12 +2509,73 @@ function isCanonicalizedStructurallyValidTimeZoneName(timeZone) {
/** /**
* @description Simplified PartitionDurationFormatPattern implementation which * @description Simplified PartitionDurationFormatPattern implementation which
* only supports the "en" locale. * only supports the "en" locale.
* @param {Object} durationFormat the duration format object
* @param {Object} duration the duration record * @param {Object} duration the duration record
* @param {String} style the duration format style
* @result {Array} an array with formatted duration parts * @result {Array} an array with formatted duration parts
*/ */
function partitionDurationFormatPattern(duration, style = "short") { function partitionDurationFormatPattern(durationFormat, duration) {
function durationToFractional(duration, exponent) {
let {
seconds = 0,
milliseconds = 0,
microseconds = 0,
nanoseconds = 0,
} = duration;
// Directly return the duration amount when no sub-seconds are present.
switch (exponent) {
case 9: {
if (milliseconds === 0 && microseconds === 0 && nanoseconds === 0) {
return seconds;
}
break;
}
case 6: {
if (microseconds === 0 && nanoseconds === 0) {
return milliseconds;
}
break;
}
case 3: {
if (nanoseconds === 0) {
return microseconds;
}
break;
}
}
// Otherwise compute the overall amount of nanoseconds using BigInt to avoid
// loss of precision.
let ns = BigInt(nanoseconds);
switch (exponent) {
case 9:
ns += BigInt(seconds) * 1_000_000_000n;
// fallthrough
case 6:
ns += BigInt(milliseconds) * 1_000_000n;
// fallthrough
case 3:
ns += BigInt(microseconds) * 1_000n;
// fallthrough
}
let e = BigInt(10 ** exponent);
// Split the nanoseconds amount into an integer and its fractional part.
let q = ns / e;
let r = ns % e;
// Pad fractional part, without any leading negative sign, to |exponent| digits.
if (r < 0) {
r = -r;
}
r = String(r).padStart(exponent, "0");
// Return the result as a decimal string.
return `${q}.${r}`;
}
const units = [ const units = [
"years", "years",
"months", "months",
@ -2528,39 +2589,7 @@ function partitionDurationFormatPattern(duration, style = "short") {
"nanoseconds", "nanoseconds",
]; ];
function durationToFractionalSeconds(duration) { let options = durationFormat.resolvedOptions();
let {
seconds = 0,
milliseconds = 0,
microseconds = 0,
nanoseconds = 0,
} = duration;
// Directly return seconds when no sub-seconds are present.
if (milliseconds === 0 && microseconds === 0 && nanoseconds === 0) {
return seconds;
}
// Otherwise compute the overall amount of nanoseconds using BigInt to avoid
// loss of precision.
let ns_sec = BigInt(seconds) * 1_000_000_000n;
let ns_ms = BigInt(milliseconds) * 1_000_000n;
let ns_us = BigInt(microseconds) * 1_000n;
let ns = ns_sec + ns_ms + ns_us + BigInt(nanoseconds);
// Split the nanoseconds amount into seconds and sub-seconds.
let q = ns / 1_000_000_000n;
let r = ns % 1_000_000_000n;
// Pad sub-seconds, without any leading negative sign, to nine digits.
if (r < 0) {
r = -r;
}
r = String(r).padStart(9, "0");
// Return seconds with fractional part as a decimal string.
return `${q}.${r}`;
}
// Only "en" is supported. // Only "en" is supported.
const locale = "en"; const locale = "en";
@ -2568,116 +2597,131 @@ function partitionDurationFormatPattern(duration, style = "short") {
const timeSeparator = ":"; const timeSeparator = ":";
let result = []; let result = [];
let separated = false; let needSeparator = false;
let displayNegativeSign = true;
for (let unit of units) { for (let unit of units) {
// Absent units default to zero. // Absent units default to zero.
let value = duration[unit] ?? 0; let value = duration[unit] ?? 0;
let display = "auto"; let style = options[unit];
if (style === "digital") { let display = options[unit + "Display"];
// Always display numeric units per GetDurationUnitOptions.
if (unit === "hours" || unit === "minutes" || unit === "seconds") {
display = "always";
}
// Numeric seconds and sub-seconds are combined into a single value. // NumberFormat requires singular unit names.
if (unit === "seconds") { let numberFormatUnit = unit.slice(0, -1);
value = durationToFractionalSeconds(duration);
// Compute the matching NumberFormat options.
let nfOpts = Object.create(null);
// Numeric seconds and sub-seconds are combined into a single value.
let done = false;
if (unit === "seconds" || unit === "milliseconds" || unit === "microseconds") {
let nextStyle = options[units[units.indexOf(unit) + 1]];
if (nextStyle === "numeric") {
if (unit === "seconds") {
value = durationToFractional(duration, 9);
} else if (unit === "milliseconds") {
value = durationToFractional(duration, 6);
} else {
value = durationToFractional(duration, 3);
}
nfOpts.maximumFractionDigits = options.fractionalDigits ?? 9;
nfOpts.minimumFractionDigits = options.fractionalDigits ?? 0;
nfOpts.roundingMode = "trunc";
done = true;
} }
} }
// Display zero numeric minutes when seconds will be displayed.
let displayRequired = false;
if (unit === "minutes" && needSeparator) {
displayRequired = options.secondsDisplay === "always" ||
(duration.seconds ?? 0) !== 0 ||
(duration.milliseconds ?? 0) !== 0 ||
(duration.microseconds ?? 0) !== 0 ||
(duration.nanoseconds ?? 0) !== 0;
}
// "auto" display omits zero units. // "auto" display omits zero units.
if (value !== 0 || display !== "auto") { if (value !== 0 || display !== "auto" || displayRequired) {
// Map the DurationFormat style to a NumberFormat style. // Display only the first negative value.
let unitStyle = style; if (displayNegativeSign) {
if (style === "digital") { displayNegativeSign = false;
if (unit === "hours") {
unitStyle = "numeric"; // Set to negative zero to ensure the sign is displayed.
} else if (unit === "minutes" || unit === "seconds") { if (value === 0) {
unitStyle = "2-digit"; let negative = units.some(unit => (duration[unit] ?? 0) < 0);
} else { if (negative) {
unitStyle = "short"; value = -0;
}
} }
} else {
nfOpts.signDisplay = "never";
} }
// NumberFormat requires singular unit names. nfOpts.numberingSystem = options.numberingSystem;
let numberFormatUnit = unit.slice(0, -1);
// Compute the matching NumberFormat options. // If the value is formatted as a 2-digit numeric value.
let nfOpts; if (style === "2-digit") {
if (unitStyle !== "numeric" && unitStyle !== "2-digit") { nfOpts.minimumIntegerDigits = 2;
// The value is formatted as a standalone unit. }
nfOpts = {
numberingSystem,
style: "unit",
unit: numberFormatUnit,
unitDisplay: unitStyle,
};
} else {
let roundingMode = undefined;
let minimumFractionDigits = undefined;
let maximumFractionDigits = undefined;
// Numeric seconds include any sub-seconds. // If the value is formatted as a standalone unit.
if (style === "digital" && unit === "seconds") { if (style !== "numeric" && style !== "2-digit") {
roundingMode = "trunc"; nfOpts.style = "unit";
minimumFractionDigits = 0; nfOpts.unit = numberFormatUnit;
maximumFractionDigits = 9; nfOpts.unitDisplay = style;
}
// The value is formatted as a numeric unit.
nfOpts = {
numberingSystem,
minimumIntegerDigits: (unitStyle === "2-digit" ? 2 : 1),
roundingMode,
minimumFractionDigits,
maximumFractionDigits,
};
} }
let nf = new Intl.NumberFormat(locale, nfOpts); let nf = new Intl.NumberFormat(locale, nfOpts);
let formatted = nf.formatToParts(value);
let list;
if (!needSeparator) {
list = [];
} else {
list = result[result.length - 1];
// Prepend the time separator before the formatted number.
list.push({
type: "literal",
value: timeSeparator,
});
}
// Format the numeric value.
let parts = nf.formatToParts(value);
// Add |numberFormatUnit| to the formatted number. // Add |numberFormatUnit| to the formatted number.
let list = []; for (let {value, type} of parts) {
for (let {value, type} of formatted) {
list.push({type, value, unit: numberFormatUnit}); list.push({type, value, unit: numberFormatUnit});
} }
if (!separated) { if (!needSeparator) {
// Prepend the separator before the next numeric unit. // Prepend the separator before the next numeric unit.
if (unitStyle === "2-digit" || unitStyle === "numeric") { if (style === "2-digit" || style === "numeric") {
separated = true; needSeparator = true;
} }
// Append the formatted number to |result|. // Append the formatted number to |result|.
result.push(list); result.push(list);
} else {
let last = result[result.length - 1];
// Prepend the time separator before the formatted number.
last.push({
type: "literal",
value: timeSeparator,
});
// Concatenate |last| and the formatted number.
last.push(...list);
} }
} else {
separated = false;
} }
// No further units possible after "seconds" when style is "digital". if (done) {
if (style === "digital" && unit === "seconds") {
break; break;
} }
} }
let listStyle = options.style;
if (listStyle === "digital") {
listStyle = "short";
}
let lf = new Intl.ListFormat(locale, { let lf = new Intl.ListFormat(locale, {
type: "unit", type: "unit",
style: (style !== "digital" ? style : "short"), style: listStyle,
}); });
// Collect all formatted units into a list of strings. // Collect all formatted units into a list of strings.
@ -2705,11 +2749,12 @@ function partitionDurationFormatPattern(duration, style = "short") {
/** /**
* @description Return the formatted string from partitionDurationFormatPattern. * @description Return the formatted string from partitionDurationFormatPattern.
* @param {Object} durationFormat the duration format object
* @param {Object} duration the duration record * @param {Object} duration the duration record
* @param {String} style the duration format style
* @result {String} a string containing the formatted duration * @result {String} a string containing the formatted duration
*/ */
function formatDurationFormatPattern(duration, style) { function formatDurationFormatPattern(durationFormat, duration) {
return partitionDurationFormatPattern(duration, style).reduce((acc, e) => acc + e.value, ""); let parts = partitionDurationFormatPattern(durationFormat, duration);
return parts.reduce((acc, e) => acc + e.value, "");
} }

View File

@ -23,9 +23,10 @@ const duration = {
nanoseconds: -9, nanoseconds: -9,
}; };
const expected = formatDurationFormatPattern(duration);
const df = new Intl.DurationFormat("en"); const df = new Intl.DurationFormat("en");
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue( assert.sameValue(
df.format(duration), df.format(duration),
expected, expected,

View File

@ -25,9 +25,10 @@ const duration = {
nanoseconds: -9, nanoseconds: -9,
}; };
const expected = formatDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", {style}); const df = new Intl.DurationFormat("en", {style});
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue( assert.sameValue(
df.format(duration), df.format(duration),
expected, expected,

View File

@ -25,9 +25,10 @@ const duration = {
nanoseconds: -9, nanoseconds: -9,
}; };
const expected = formatDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", {style}); const df = new Intl.DurationFormat("en", {style});
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue( assert.sameValue(
df.format(duration), df.format(duration),
expected, expected,

View File

@ -25,9 +25,10 @@ const duration = {
nanoseconds: -9, nanoseconds: -9,
}; };
const expected = formatDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", {style}); const df = new Intl.DurationFormat("en", {style});
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue( assert.sameValue(
df.format(duration), df.format(duration),
expected, expected,

View File

@ -25,9 +25,10 @@ const duration = {
nanoseconds: -9, nanoseconds: -9,
}; };
const expected = formatDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", {style}); const df = new Intl.DurationFormat("en", {style});
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue( assert.sameValue(
df.format(duration), df.format(duration),
expected, expected,

View File

@ -84,7 +84,7 @@ const durations = [
const df = new Intl.DurationFormat("en", {style: "digital"}); const df = new Intl.DurationFormat("en", {style: "digital"});
for (let duration of durations) { for (let duration of durations) {
let expected = formatDurationFormatPattern(duration, "digital"); let expected = formatDurationFormatPattern(df, duration);
assert.sameValue( assert.sameValue(
df.format(duration), df.format(duration),
expected, expected,

View File

@ -22,7 +22,8 @@ const duration = {
nanoseconds: 9, nanoseconds: 9,
}; };
const expected = formatDurationFormatPattern(duration);
const df = new Intl.DurationFormat("en"); const df = new Intl.DurationFormat("en");
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using default style option`); assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using default style option`);

View File

@ -25,7 +25,8 @@ const duration = {
nanoseconds: 9, nanoseconds: 9,
}; };
const expected = formatDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", {style}); const df = new Intl.DurationFormat("en", {style});
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using ${style} style option`); assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using ${style} style option`);

View File

@ -24,7 +24,8 @@ const duration = {
nanoseconds: 9, nanoseconds: 9,
}; };
const expected = formatDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", {style}); const df = new Intl.DurationFormat("en", {style});
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using ${style} style option`); assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using ${style} style option`);

View File

@ -24,7 +24,8 @@ const duration = {
nanoseconds: 9, nanoseconds: 9,
}; };
const expected = formatDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", {style}); const df = new Intl.DurationFormat("en", {style});
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using ${style} style option`); assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using ${style} style option`);

View File

@ -24,7 +24,8 @@ const duration = {
nanoseconds: 9, nanoseconds: 9,
}; };
const expected = formatDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", {style}); const df = new Intl.DurationFormat("en", {style});
const expected = formatDurationFormatPattern(df, duration);
assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using ${style} style option`); assert.sameValue(df.format(duration), expected, `Assert DurationFormat format output using ${style} style option`);

View File

@ -38,7 +38,8 @@ const duration = {
nanoseconds: 789, nanoseconds: 789,
}; };
const expected = partitionDurationFormatPattern(duration); const df = new Intl.DurationFormat('en');
const expected = partitionDurationFormatPattern(df, duration);
let df = new Intl.DurationFormat('en');
compare(df.formatToParts(duration), expected, `Using style : default`); compare(df.formatToParts(duration), expected, `Using style : default`);

View File

@ -40,7 +40,8 @@ const duration = {
const style = "digital"; const style = "digital";
const expected = partitionDurationFormatPattern(duration, style); const df = new Intl.DurationFormat('en', { style });
const expected = partitionDurationFormatPattern(df, duration);
let df = new Intl.DurationFormat('en', { style });
compare(df.formatToParts(duration), expected, `Using style : ${style}`); compare(df.formatToParts(duration), expected, `Using style : ${style}`);

View File

@ -40,7 +40,8 @@ const duration = {
const style = "long"; const style = "long";
const expected = partitionDurationFormatPattern(duration, style); const df = new Intl.DurationFormat('en', { style });
const expected = partitionDurationFormatPattern(df, duration);
let df = new Intl.DurationFormat('en', { style });
compare(df.formatToParts(duration), expected, `Using style : ${style}`); compare(df.formatToParts(duration), expected, `Using style : ${style}`);

View File

@ -40,7 +40,8 @@ const duration = {
const style = "narrow"; const style = "narrow";
const expected = partitionDurationFormatPattern(duration, style); const df = new Intl.DurationFormat('en', { style });
const expected = partitionDurationFormatPattern(df, duration);
let df = new Intl.DurationFormat('en', { style });
compare(df.formatToParts(duration), expected, `Using style : ${style}`); compare(df.formatToParts(duration), expected, `Using style : ${style}`);

View File

@ -40,7 +40,8 @@ const duration = {
const style = "short"; const style = "short";
const expected = partitionDurationFormatPattern(duration, style); const df = new Intl.DurationFormat('en', { style });
const expected = partitionDurationFormatPattern(df, duration);
let df = new Intl.DurationFormat('en', { style });
compare(df.formatToParts(duration), expected, `Using style : ${style}`); compare(df.formatToParts(duration), expected, `Using style : ${style}`);

View File

@ -41,7 +41,8 @@ const duration = {
nanoseconds: -789, nanoseconds: -789,
}; };
const expected = partitionDurationFormatPattern(duration);
const df = new Intl.DurationFormat("en"); const df = new Intl.DurationFormat("en");
const expected = partitionDurationFormatPattern(df, duration);
compare(df.formatToParts(duration), expected, `Using style : default`); compare(df.formatToParts(duration), expected, `Using style : default`);

View File

@ -43,7 +43,8 @@ const duration = {
nanoseconds: -789, nanoseconds: -789,
}; };
const expected = partitionDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", { style }); const df = new Intl.DurationFormat("en", { style });
const expected = partitionDurationFormatPattern(df, duration);
compare(df.formatToParts(duration), expected, `Using style : ${style}`); compare(df.formatToParts(duration), expected, `Using style : ${style}`);

View File

@ -43,7 +43,8 @@ const duration = {
nanoseconds: -789, nanoseconds: -789,
}; };
const expected = partitionDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", { style }); const df = new Intl.DurationFormat("en", { style });
const expected = partitionDurationFormatPattern(df, duration);
compare(df.formatToParts(duration), expected, `Using style : ${style}`); compare(df.formatToParts(duration), expected, `Using style : ${style}`);

View File

@ -43,7 +43,8 @@ const duration = {
nanoseconds: -789, nanoseconds: -789,
}; };
const expected = partitionDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", { style }); const df = new Intl.DurationFormat("en", { style });
const expected = partitionDurationFormatPattern(df, duration);
compare(df.formatToParts(duration), expected, `Using style : ${style}`); compare(df.formatToParts(duration), expected, `Using style : ${style}`);

View File

@ -43,7 +43,8 @@ const duration = {
nanoseconds: -789, nanoseconds: -789,
}; };
const expected = partitionDurationFormatPattern(duration, style);
const df = new Intl.DurationFormat("en", { style }); const df = new Intl.DurationFormat("en", { style });
const expected = partitionDurationFormatPattern(df, duration);
compare(df.formatToParts(duration), expected, `Using style : ${style}`); compare(df.formatToParts(duration), expected, `Using style : ${style}`);