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
* only supports the "en" locale.
* @param {Object} durationFormat the duration format object
* @param {Object} duration the duration record
* @param {String} style the duration format style
* @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 = [
"years",
"months",
@ -2528,39 +2589,7 @@ function partitionDurationFormatPattern(duration, style = "short") {
"nanoseconds",
];
function durationToFractionalSeconds(duration) {
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}`;
}
let options = durationFormat.resolvedOptions();
// Only "en" is supported.
const locale = "en";
@ -2568,116 +2597,131 @@ function partitionDurationFormatPattern(duration, style = "short") {
const timeSeparator = ":";
let result = [];
let separated = false;
let needSeparator = false;
let displayNegativeSign = true;
for (let unit of units) {
// Absent units default to zero.
let value = duration[unit] ?? 0;
let display = "auto";
if (style === "digital") {
// Always display numeric units per GetDurationUnitOptions.
if (unit === "hours" || unit === "minutes" || unit === "seconds") {
display = "always";
}
let style = options[unit];
let display = options[unit + "Display"];
// Numeric seconds and sub-seconds are combined into a single value.
if (unit === "seconds") {
value = durationToFractionalSeconds(duration);
// NumberFormat requires singular unit names.
let numberFormatUnit = unit.slice(0, -1);
// 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.
if (value !== 0 || display !== "auto") {
// Map the DurationFormat style to a NumberFormat style.
let unitStyle = style;
if (style === "digital") {
if (unit === "hours") {
unitStyle = "numeric";
} else if (unit === "minutes" || unit === "seconds") {
unitStyle = "2-digit";
} else {
unitStyle = "short";
if (value !== 0 || display !== "auto" || displayRequired) {
// Display only the first negative value.
if (displayNegativeSign) {
displayNegativeSign = false;
// Set to negative zero to ensure the sign is displayed.
if (value === 0) {
let negative = units.some(unit => (duration[unit] ?? 0) < 0);
if (negative) {
value = -0;
}
}
} else {
nfOpts.signDisplay = "never";
}
// NumberFormat requires singular unit names.
let numberFormatUnit = unit.slice(0, -1);
nfOpts.numberingSystem = options.numberingSystem;
// Compute the matching NumberFormat options.
let nfOpts;
if (unitStyle !== "numeric" && unitStyle !== "2-digit") {
// 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;
// If the value is formatted as a 2-digit numeric value.
if (style === "2-digit") {
nfOpts.minimumIntegerDigits = 2;
}
// Numeric seconds include any sub-seconds.
if (style === "digital" && unit === "seconds") {
roundingMode = "trunc";
minimumFractionDigits = 0;
maximumFractionDigits = 9;
}
// The value is formatted as a numeric unit.
nfOpts = {
numberingSystem,
minimumIntegerDigits: (unitStyle === "2-digit" ? 2 : 1),
roundingMode,
minimumFractionDigits,
maximumFractionDigits,
};
// If the value is formatted as a standalone unit.
if (style !== "numeric" && style !== "2-digit") {
nfOpts.style = "unit";
nfOpts.unit = numberFormatUnit;
nfOpts.unitDisplay = style;
}
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.
let list = [];
for (let {value, type} of formatted) {
for (let {value, type} of parts) {
list.push({type, value, unit: numberFormatUnit});
}
if (!separated) {
if (!needSeparator) {
// Prepend the separator before the next numeric unit.
if (unitStyle === "2-digit" || unitStyle === "numeric") {
separated = true;
if (style === "2-digit" || style === "numeric") {
needSeparator = true;
}
// Append the formatted number to |result|.
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 (style === "digital" && unit === "seconds") {
if (done) {
break;
}
}
let listStyle = options.style;
if (listStyle === "digital") {
listStyle = "short";
}
let lf = new Intl.ListFormat(locale, {
type: "unit",
style: (style !== "digital" ? style : "short"),
style: listStyle,
});
// 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.
* @param {Object} durationFormat the duration format object
* @param {Object} duration the duration record
* @param {String} style the duration format style
* @result {String} a string containing the formatted duration
*/
function formatDurationFormatPattern(duration, style) {
return partitionDurationFormatPattern(duration, style).reduce((acc, e) => acc + e.value, "");
function formatDurationFormatPattern(durationFormat, duration) {
let parts = partitionDurationFormatPattern(durationFormat, duration);
return parts.reduce((acc, e) => acc + e.value, "");
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,8 @@ const duration = {
nanoseconds: 9,
};
const expected = formatDurationFormatPattern(duration);
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`);

View File

@ -25,7 +25,8 @@ const duration = {
nanoseconds: 9,
};
const expected = formatDurationFormatPattern(duration, 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`);

View File

@ -24,7 +24,8 @@ const duration = {
nanoseconds: 9,
};
const expected = formatDurationFormatPattern(duration, 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`);

View File

@ -24,7 +24,8 @@ const duration = {
nanoseconds: 9,
};
const expected = formatDurationFormatPattern(duration, 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`);

View File

@ -24,7 +24,8 @@ const duration = {
nanoseconds: 9,
};
const expected = formatDurationFormatPattern(duration, 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`);

View File

@ -38,7 +38,8 @@ const duration = {
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`);

View File

@ -40,7 +40,8 @@ const duration = {
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}`);

View File

@ -40,7 +40,8 @@ const duration = {
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}`);

View File

@ -40,7 +40,8 @@ const duration = {
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}`);

View File

@ -40,7 +40,8 @@ const duration = {
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}`);

View File

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

View File

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

View File

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

View File

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

View File

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