Skip to content

Commit

Permalink
Polyfill: Refactor TemporalDurationToString to be more like spec text
Browse files Browse the repository at this point in the history
I'm going to make some changes to TemporalDurationToString in order to fix
bug #2563. It will be helpful to have it do everything in the same order
as the spec text, to verify that the changes work.
  • Loading branch information
ptomato committed May 2, 2023
1 parent 71f7b74 commit b017b43
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 54 deletions.
70 changes: 66 additions & 4 deletions polyfill/lib/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,18 @@ export class Duration {

if (typeof __debug__ !== 'undefined' && __debug__) {
Object.defineProperty(this, '_repr_', {
value: `${this[Symbol.toStringTag]} <${ES.TemporalDurationToString(this)}>`,
value: `${this[Symbol.toStringTag]} <${ES.TemporalDurationToString(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds
)}>`,
writable: false,
enumerable: false,
configurable: false
Expand Down Expand Up @@ -402,19 +413,70 @@ export class Duration {
throw new RangeError('smallestUnit must be a time unit other than "hours" or "minutes"');
}
const { precision, unit, increment } = ES.ToSecondsStringPrecisionRecord(smallestUnit, digits);
return ES.TemporalDurationToString(this, precision, { unit, increment, roundingMode });

let { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
ES.RoundDuration(
GetSlot(this, YEARS),
GetSlot(this, MONTHS),
GetSlot(this, WEEKS),
GetSlot(this, DAYS),
GetSlot(this, HOURS),
GetSlot(this, MINUTES),
GetSlot(this, SECONDS),
GetSlot(this, MILLISECONDS),
GetSlot(this, MICROSECONDS),
GetSlot(this, NANOSECONDS),
increment,
unit,
roundingMode
);
return ES.TemporalDurationToString(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
precision
);
}
toJSON() {
if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver');
return ES.TemporalDurationToString(this);
return ES.TemporalDurationToString(
GetSlot(this, YEARS),
GetSlot(this, MONTHS),
GetSlot(this, WEEKS),
GetSlot(this, DAYS),
GetSlot(this, HOURS),
GetSlot(this, MINUTES),
GetSlot(this, SECONDS),
GetSlot(this, MILLISECONDS),
GetSlot(this, MICROSECONDS),
GetSlot(this, NANOSECONDS)
);
}
toLocaleString(locales = undefined, options = undefined) {
if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver');
if (typeof Intl !== 'undefined' && typeof Intl.DurationFormat !== 'undefined') {
return new Intl.DurationFormat(locales, options).format(this);
}
console.warn('Temporal.Duration.prototype.toLocaleString() requires Intl.DurationFormat.');
return ES.TemporalDurationToString(this);
return ES.TemporalDurationToString(
GetSlot(this, YEARS),
GetSlot(this, MONTHS),
GetSlot(this, WEEKS),
GetSlot(this, DAYS),
GetSlot(this, HOURS),
GetSlot(this, MINUTES),
GetSlot(this, SECONDS),
GetSlot(this, MILLISECONDS),
GetSlot(this, MICROSECONDS),
GetSlot(this, NANOSECONDS)
);
}
valueOf() {
throw new TypeError('use compare() to compare Temporal.Duration');
Expand Down
101 changes: 51 additions & 50 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import ToNumber from 'es-abstract/2022/ToNumber.js';
import ToObject from 'es-abstract/2022/ToObject.js';
import ToPrimitive from 'es-abstract/2022/ToPrimitive.js';
import ToString from 'es-abstract/2022/ToString.js';
import ToZeroPaddedDecimalString from 'es-abstract/2022/ToZeroPaddedDecimalString.js';
import Type from 'es-abstract/2022/Type.js';
import HasOwnProperty from 'es-abstract/2022/HasOwnProperty.js';

Expand Down Expand Up @@ -2381,67 +2382,67 @@ export function TemporalInstantToString(instant, timeZone, precision) {
return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`;
}

export function TemporalDurationToString(duration, precision = 'auto', options = undefined) {
function formatNumber(num) {
if (num <= NumberMaxSafeInteger) return num.toString(10);
return bigInt(num).toString();
}
function formatAsDecimalNumber(num) {
if (num <= NumberMaxSafeInteger) return num.toString(10);
return bigInt(num).toString();
}

const years = GetSlot(duration, YEARS);
const months = GetSlot(duration, MONTHS);
const weeks = GetSlot(duration, WEEKS);
const days = GetSlot(duration, DAYS);
const hours = GetSlot(duration, HOURS);
const minutes = GetSlot(duration, MINUTES);
let seconds = GetSlot(duration, SECONDS);
let ms = GetSlot(duration, MILLISECONDS);
let µs = GetSlot(duration, MICROSECONDS);
let ns = GetSlot(duration, NANOSECONDS);
export function TemporalDurationToString(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
ms,
µs,
ns,
precision = 'auto'
) {
const sign = DurationSign(years, months, weeks, days, hours, minutes, seconds, ms, µs, ns);

if (options) {
const { unit, increment, roundingMode } = options;
({
seconds,
milliseconds: ms,
microseconds: µs,
nanoseconds: ns
} = RoundDuration(0, 0, 0, 0, 0, 0, seconds, ms, µs, ns, increment, unit, roundingMode));
}

const dateParts = [];
if (years) dateParts.push(`${formatNumber(MathAbs(years))}Y`);
if (months) dateParts.push(`${formatNumber(MathAbs(months))}M`);
if (weeks) dateParts.push(`${formatNumber(MathAbs(weeks))}W`);
if (days) dateParts.push(`${formatNumber(MathAbs(days))}D`);

const timeParts = [];
if (hours) timeParts.push(`${formatNumber(MathAbs(hours))}H`);
if (minutes) timeParts.push(`${formatNumber(MathAbs(minutes))}M`);

const secondParts = [];
let total = TotalDurationNanoseconds(0, 0, 0, seconds, ms, µs, ns, 0);
({ quotient: total, remainder: ns } = total.divmod(1000));
({ quotient: total, remainder: µs } = total.divmod(1000));
({ quotient: seconds, remainder: ms } = total.divmod(1000));
let fraction = MathAbs(ms.toJSNumber()) * 1e6 + MathAbs(µs.toJSNumber()) * 1e3 + MathAbs(ns.toJSNumber());
let decimalPart;
if (precision === 'auto') {
if (fraction !== 0) {
decimalPart = `${fraction}`.padStart(9, '0');

let datePart = '';
if (years !== 0) datePart += `${formatAsDecimalNumber(MathAbs(years))}Y`;
if (months !== 0) datePart += `${formatAsDecimalNumber(MathAbs(months))}M`;
if (weeks !== 0) datePart += `${formatAsDecimalNumber(MathAbs(weeks))}W`;
if (days !== 0) datePart += `${formatAsDecimalNumber(MathAbs(days))}D`;

let timePart = '';
if (hours !== 0) timePart += `${formatAsDecimalNumber(MathAbs(hours))}H`;
if (minutes !== 0) timePart += `${formatAsDecimalNumber(MathAbs(minutes))}M`;

if (
!seconds.isZero() ||
!ms.isZero() ||
!µs.isZero() ||
!ns.isZero() ||
(years === 0 && months === 0 && weeks === 0 && days === 0 && hours === 0 && minutes === 0) ||
precision !== 'auto'
) {
const fraction = MathAbs(ms.toJSNumber()) * 1e6 + MathAbs(µs.toJSNumber()) * 1e3 + MathAbs(ns.toJSNumber());
let decimalPart = ToZeroPaddedDecimalString(fraction, 9);
if (precision === 'auto') {
while (decimalPart[decimalPart.length - 1] === '0') {
decimalPart = decimalPart.slice(0, -1);
}
} else if (precision === 0) {
decimalPart = '';
} else {
decimalPart = decimalPart.slice(0, precision);
}
} else if (precision !== 0) {
decimalPart = `${fraction}`.padStart(9, '0').slice(0, precision);
}
if (decimalPart) secondParts.unshift('.', decimalPart);
if (!seconds.isZero() || secondParts.length || precision !== 'auto') secondParts.unshift(seconds.abs().toString());
if (secondParts.length) timeParts.push(`${secondParts.join('')}S`);
if (timeParts.length) timeParts.unshift('T');
if (!dateParts.length && !timeParts.length) return 'PT0S';
return `${sign < 0 ? '-' : ''}P${dateParts.join('')}${timeParts.join('')}`;
let secondsPart = seconds.abs().toString();
if (decimalPart) secondsPart += `.${decimalPart}`;
timePart += `${secondsPart}S`;
}
let result = `${sign < 0 ? '-' : ''}P${datePart}`;
if (timePart) result = `${result}T${timePart}`;
return result;
}

export function TemporalDateToString(date, showCalendar = 'auto') {
Expand Down

0 comments on commit b017b43

Please sign in to comment.