Skip to content

Commit

Permalink
Editorial: DRY refactor of time formatting
Browse files Browse the repository at this point in the history
This editorial commit removes redundant spec text dealing with time
formatting:
* Adds a new AO FormatTimeString, and calls it in TemporalTimeToString,
  TemporalDateTimeToString, and TimeString (the legacy date AO in 262).
* Removes FormatSecondsStringPart and replaces it with a new AO
  FormatFractionalSeconds, and call it in FormatTimeString and
  TemporalDurationToString. The text of this new AO is aligned with
  similar text in GetOffsetStringFor in #2607.
* Replaces sub-second formatting text in TemporalDurationToString
  with a call to FormatFractionalSeconds.
* Aligns polyfill code to these spec changes.
* Adjusts polyfill code in a few places to better match the spec.

Note that this commit doesn't touch spec text for formatting time zone
offsets because there are several in-flight PRs dealing with offsets
and I wanted to keep this PR merge-conflict-free. But once those
PRs land and the dust settles, then I'll send another editorial PR to
DRY offset string formatting too, by using FormatTimeString to replace
bespoke formatting text for offsets.
  • Loading branch information
justingrant committed Jul 17, 2023
1 parent 8b80732 commit c429eed
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 109 deletions.
108 changes: 41 additions & 67 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const StringFromCharCode = String.fromCharCode;
const StringPrototypeCharCodeAt = String.prototype.charCodeAt;
const StringPrototypeMatchAll = String.prototype.matchAll;
const StringPrototypeReplace = String.prototype.replace;
const StringPrototypeSlice = String.prototype.slice;

import bigInt from 'big-integer';
import callBound from 'call-bind/callBound';
Expand Down Expand Up @@ -2354,56 +2355,52 @@ export function ISOYearString(year) {
if (year < 0 || year > 9999) {
let sign = year < 0 ? '-' : '+';
let yearNumber = MathAbs(year);
yearString = sign + `000000${yearNumber}`.slice(-6);
yearString = sign + ToZeroPaddedDecimalString(yearNumber, 6);
} else {
yearString = `0000${year}`.slice(-4);
yearString = ToZeroPaddedDecimalString(year, 4);
}
return yearString;
}

export function ISODateTimePartString(part) {
return `00${part}`.slice(-2);
return ToZeroPaddedDecimalString(part, 2);
}

export function FormatSecondsStringPart(second, millisecond, microsecond, nanosecond, precision) {
if (precision === 'minute') return '';

const secs = `:${ISODateTimePartString(second)}`;
let fraction = millisecond * 1e6 + microsecond * 1e3 + nanosecond;

export function FormatFractionalSeconds(subSecondNanoseconds, precision) {
let fraction;
if (precision === 'auto') {
if (fraction === 0) return secs;
fraction = `${fraction}`.padStart(9, '0');
while (fraction[fraction.length - 1] === '0') fraction = fraction.slice(0, -1);
if (subSecondNanoseconds === 0) return '';
const fractionFullPrecision = ToZeroPaddedDecimalString(subSecondNanoseconds, 9);
// now remove any trailing zeroes
fraction = Call(StringPrototypeReplace, fractionFullPrecision, [/0+$/, '']);
} else {
if (precision === 0) return secs;
fraction = `${fraction}`.padStart(9, '0').slice(0, precision);
if (precision === 0) return '';
const fractionFullPrecision = ToZeroPaddedDecimalString(subSecondNanoseconds, 9);
fraction = Call(StringPrototypeSlice, fractionFullPrecision, [0, precision]);
}
return `${secs}.${fraction}`;
return `.${fraction}`;
}

export function FormatTimeString(hour, minute, second, subSecondNanoseconds, precision) {
let result = `${ISODateTimePartString(hour)}:${ISODateTimePartString(minute)}`;
if (precision === 'minute') return result;

result += `:${ISODateTimePartString(second)}`;
result += FormatFractionalSeconds(subSecondNanoseconds, precision);
return result;
}

export function TemporalInstantToString(instant, timeZone, precision) {
let outputTimeZone = timeZone;
if (outputTimeZone === undefined) outputTimeZone = 'UTC';
const dateTime = GetPlainDateTimeFor(outputTimeZone, instant, 'iso8601');
const year = ISOYearString(GetSlot(dateTime, ISO_YEAR));
const month = ISODateTimePartString(GetSlot(dateTime, ISO_MONTH));
const day = ISODateTimePartString(GetSlot(dateTime, ISO_DAY));
const hour = ISODateTimePartString(GetSlot(dateTime, ISO_HOUR));
const minute = ISODateTimePartString(GetSlot(dateTime, ISO_MINUTE));
const seconds = FormatSecondsStringPart(
GetSlot(dateTime, ISO_SECOND),
GetSlot(dateTime, ISO_MILLISECOND),
GetSlot(dateTime, ISO_MICROSECOND),
GetSlot(dateTime, ISO_NANOSECOND),
precision
);
const dateTimeString = TemporalDateTimeToString(dateTime, precision, 'never');
let timeZoneString = 'Z';
if (timeZone !== undefined) {
const offsetNs = GetOffsetNanosecondsFor(outputTimeZone, instant);
timeZoneString = FormatDateTimeUTCOffsetRounded(offsetNs);
}
return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`;
return `${dateTimeString}${timeZoneString}`;
}

function formatAsDecimalNumber(num) {
Expand Down Expand Up @@ -2449,20 +2446,11 @@ export function TemporalDurationToString(
(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);
}
let secondsPart = seconds.abs().toString();
if (decimalPart) secondsPart += `.${decimalPart}`;
timePart += `${secondsPart}S`;
const secondsPart = formatAsDecimalNumber(seconds.abs());
const subSecondNanoseconds =
MathAbs(ms.toJSNumber()) * 1e6 + MathAbs(µs.toJSNumber()) * 1e3 + MathAbs(ns.toJSNumber());
const subSecondsPart = FormatFractionalSeconds(subSecondNanoseconds, precision);
timePart += `${secondsPart}${subSecondsPart}S`;
}
let result = `${sign < 0 ? '-' : ''}P${datePart}`;
if (timePart) result = `${result}T${timePart}`;
Expand Down Expand Up @@ -2506,14 +2494,13 @@ export function TemporalDateTimeToString(dateTime, precision, showCalendar = 'au
));
}

year = ISOYearString(year);
month = ISODateTimePartString(month);
day = ISODateTimePartString(day);
hour = ISODateTimePartString(hour);
minute = ISODateTimePartString(minute);
const seconds = FormatSecondsStringPart(second, millisecond, microsecond, nanosecond, precision);
const yearString = ISOYearString(year);
const monthString = ISODateTimePartString(month);
const dayString = ISODateTimePartString(day);
const subSecondNanoseconds = millisecond * 1e6 + microsecond * 1e3 + nanosecond;
const timeString = FormatTimeString(hour, minute, second, subSecondNanoseconds, precision);
const calendar = MaybeFormatCalendarAnnotation(GetSlot(dateTime, CALENDAR), showCalendar);
return `${year}-${month}-${day}T${hour}:${minute}${seconds}${calendar}`;
return `${yearString}-${monthString}-${dayString}T${timeString}${calendar}`;
}

export function TemporalMonthDayToString(monthDay, showCalendar = 'auto') {
Expand Down Expand Up @@ -2565,31 +2552,18 @@ export function TemporalZonedDateTimeToString(

const tz = GetSlot(zdt, TIME_ZONE);
const dateTime = GetPlainDateTimeFor(tz, instant, 'iso8601');

const year = ISOYearString(GetSlot(dateTime, ISO_YEAR));
const month = ISODateTimePartString(GetSlot(dateTime, ISO_MONTH));
const day = ISODateTimePartString(GetSlot(dateTime, ISO_DAY));
const hour = ISODateTimePartString(GetSlot(dateTime, ISO_HOUR));
const minute = ISODateTimePartString(GetSlot(dateTime, ISO_MINUTE));
const seconds = FormatSecondsStringPart(
GetSlot(dateTime, ISO_SECOND),
GetSlot(dateTime, ISO_MILLISECOND),
GetSlot(dateTime, ISO_MICROSECOND),
GetSlot(dateTime, ISO_NANOSECOND),
precision
);
let result = `${year}-${month}-${day}T${hour}:${minute}${seconds}`;
let dateTimeString = TemporalDateTimeToString(dateTime, precision, 'never');
if (showOffset !== 'never') {
const offsetNs = GetOffsetNanosecondsFor(tz, instant);
result += FormatDateTimeUTCOffsetRounded(offsetNs);
dateTimeString += FormatDateTimeUTCOffsetRounded(offsetNs);
}
if (showTimeZone !== 'never') {
const identifier = ToTemporalTimeZoneIdentifier(tz);
const flag = showTimeZone === 'critical' ? '!' : '';
result += `[${flag}${identifier}]`;
dateTimeString += `[${flag}${identifier}]`;
}
result += MaybeFormatCalendarAnnotation(GetSlot(zdt, CALENDAR), showCalendar);
return result;
dateTimeString += MaybeFormatCalendarAnnotation(GetSlot(zdt, CALENDAR), showCalendar);
return dateTimeString;
}

export function IsOffsetTimeZoneIdentifier(string) {
Expand Down
6 changes: 2 additions & 4 deletions polyfill/lib/plaintime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,8 @@ function TemporalTimeToString(time, precision, options = undefined) {
));
}

hour = ES.ISODateTimePartString(hour);
minute = ES.ISODateTimePartString(minute);
const seconds = ES.FormatSecondsStringPart(second, millisecond, microsecond, nanosecond, precision);
return `${hour}:${minute}${seconds}`;
const subSecondNanoseconds = millisecond * 1e6 + microsecond * 1e3 + nanosecond;
return ES.FormatTimeString(hour, minute, second, subSecondNanoseconds, precision);
}

export class PlainTime {
Expand Down
64 changes: 51 additions & 13 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -680,22 +680,60 @@ <h1>
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-formatsecondsstringpart" aoid="FormatSecondsStringPart">
<h1>FormatSecondsStringPart ( _second_, _millisecond_, _microsecond_, _nanosecond_, _precision_ )</h1>
<emu-clause id="sec-temporal-formatfractionalseconds" type="abstract operation">
<h1>
FormatFractionalSeconds (
_subSecondNanoseconds_: an integer,
_precision_: either an integer in the inclusive range 0 to 9 or *"auto"*
): a String
</h1>
<dl class="header">
<dt>description</dt>
<dd>
If _precision_ is zero, or if _precision_ is *"auto"* and _subSecondNanoseconds_ is zero, then an empty String will be returned.
Otherwise, the output will be a decimal point followed by a sequence of fractional seconds digits, truncated to _precision_ digits or (if _precision_ is *"auto"*) to the last non-zero digit.
</dd>
</dl>
<emu-alg>
1. Assert: _second_, _millisecond_, _microsecond_, and _nanosecond_ are integers.
1. If _precision_ is *"minute"*, return *""*.
1. Let _secondsString_ be the string-concatenation of the code unit 0x003A (COLON) and ToZeroPaddedDecimalString(_second_, 2).
1. Let _fraction_ be _millisecond_ &times; 10<sup>6</sup> + _microsecond_ &times; 10<sup>3</sup> + _nanosecond_.
1. If _precision_ is *"auto"*, then
1. If _fraction_ is 0, return _secondsString_.
1. Set _fraction_ to ToZeroPaddedDecimalString(_fraction_, 9).
1. Set _fraction_ to the longest possible substring of _fraction_ starting at position 0 and not ending with the code unit 0x0030 (DIGIT ZERO).
1. If _subSecondNanoseconds_ is 0, return the empty String.
1. Let _fractionString_ be ToZeroPaddedDecimalString(_subSecondNanoseconds_, 9).
1. Set _fractionString_ to the longest prefix of _fractionString_ ending with a code unit other than 0x0030 (DIGIT ZERO).
1. Else,
1. If _precision_ is 0, return _secondsString_.
1. Set _fraction_ to ToZeroPaddedDecimalString(_fraction_, 9).
1. Set _fraction_ to the substring of _fraction_ from 0 to _precision_.
1. Return the string-concatenation of _secondsString_, the code unit 0x002E (FULL STOP), and _fraction_.
1. If _precision_ is 0, return the empty String.
1. Let _fractionString_ be ToZeroPaddedDecimalString(_subSecondNanoseconds_, 9).
1. Set _fractionString_ to the substring of _fractionString_ from 0 to _precision_.
1. Return the string-concatenation of the code unit 0x002E (FULL STOP) and _fractionString_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-formattimestring" type="abstract operation">
<h1>
FormatTimeString (
_hour_: an integer,
_minute_: an integer,
_second_: an integer,
_subSecondNanoseconds_: an integer,
_precision_: an integer in the inclusive range 0 to 9, *"minute"*, or *"auto"*
): a String
</h1>
<dl class="header">
<dt>description</dt>
<dd>
The output will be formatted like ±HH:MM if _precision_ is *"minute"*.
Otherwise, the output will be formatted like ±HH:MM:SS if _precision_ is zero, or if _subSecondNanoseconds_ is zero and _precision is *"auto"*.
Otherwise, the output will be formatted like ±HH:MM:SS.fff where "fff" is a sequence of fractional seconds digits, truncated to _precision_ digits or (if _precision_ is *"auto"*) to the last non-zero digit.
</dd>
</dl>
<emu-alg>
1. Let _hh_ be ToZeroPaddedDecimalString(_hour_, 2).
1. Let _mm_ be ToZeroPaddedDecimalString(_minute_, 2).
1. Let _result_ be the string-concatenation of _hh_, the code unit 0x003A (COLON), and _mm_.
1. If _precision_ is *"minute"*, return _result_.
1. Let _ss_ be ToZeroPaddedDecimalString(_second_, 2).
1. Let _subSecondsPart_ be FormatFractionalSeconds(_subSecondNanoseconds_, _precision_).
1. Set _result_ to the string-concatenation of _result_, the code unit 0x003A (COLON), _ss_, and _subSecondsPart_.
1. Return _result_.
</emu-alg>
</emu-clause>

Expand Down
14 changes: 3 additions & 11 deletions spec/duration.html
Original file line number Diff line number Diff line change
Expand Up @@ -1936,18 +1936,10 @@ <h1>
1. Let _zeroMinutesAndHigher_ be *false*.
1. If _years_ = 0, and _months_ = 0, and _weeks_ = 0, and _days_ = 0, and _hours_ = 0, and _minutes_ = 0, set _zeroMinutesAndHigher_ to *true*.
1. If _nonzeroSecondsAndLower_ is *true*, or _zeroMinutesAndHigher_ is *true*, or _precision_ is not *"auto"*, then
1. Let _fraction_ be abs(_milliseconds_) &times; 10<sup>6</sup> + abs(_microseconds_) &times; 10<sup>3</sup> + abs(_nanoseconds_).
1. Let _decimalPart_ be ToZeroPaddedDecimalString(_fraction_, 9).
1. If _precision_ is *"auto"*, then
1. Set _decimalPart_ to the longest possible substring of _decimalPart_ starting at position 0 and not ending with the code unit 0x0030 (DIGIT ZERO).
1. Else if _precision_ = 0, then
1. Set _decimalPart_ to *""*.
1. Else,
1. Set _decimalPart_ to the substring of _decimalPart_ from 0 to _precision_.
1. Let _secondsPart_ be abs(_seconds_) formatted as a decimal number.
1. If _decimalPart_ is not *""*, then
1. Set _secondsPart_ to the string-concatenation of _secondsPart_, the code unit 0x002E (FULL STOP), and _decimalPart_.
1. Set _timePart_ to the string concatenation of _timePart_, _secondsPart_, and the code unit 0x0053 (LATIN CAPITAL LETTER S).
1. Let _subSecondNanoseconds_ be abs(_milliseconds_) &times; 10<sup>6</sup> + abs(_microseconds_) &times; 10<sup>3</sup> + abs(_nanoseconds_).
1. Let _subSecondsPart_ be FormatFractionalSeconds(_subSecondNanoseconds_, _precision_).
1. Set _timePart_ to the string concatenation of _timePart_, _secondsPart_, _subSecondsPart_, and the code unit 0x0053 (LATIN CAPITAL LETTER S).
1. Let _signPart_ be the code unit 0x002D (HYPHEN-MINUS) if _sign_ &lt; 0, and otherwise the empty String.
1. Let _result_ be the string concatenation of _signPart_, the code unit 0x0050 (LATIN CAPITAL LETTER P) and _datePart_.
1. If _timePart_ is not *""*, then
Expand Down
19 changes: 19 additions & 0 deletions spec/mainadditions.html
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,25 @@ <h1>

<p>[...]</p>

<emu-clause id="sec-timestring" type="abstract operation">
<h1>
TimeString (
_tv_: a Number, but not *NaN*,
): a String
</h1>
<dl class="header">
</dl>
<emu-alg>
1. <del>Let _hour_ be ToZeroPaddedDecimalString(ℝ(HourFromTime(_tv_)), 2).</del>
1. <del>Let _minute_ be ToZeroPaddedDecimalString(ℝ(MinFromTime(_tv_)), 2).</del>
1. <del>Let _second_ be ToZeroPaddedDecimalString(ℝ(SecFromTime(_tv_)), 2).</del>
1. <ins>Let _timeString_ be FormatTimeString(ℝ(HourFromTime(_tv_)), ℝ(MinFromTime(_tv_)), ℝ(SecFromTime(_tv_)), 0, 0).</ins>
1. Return the string-concatenation of _timeString_, the code unit 0x0020 (SPACE), and *"GMT"*.
</emu-alg>
</emu-clause>

<p>[...]</p>

<emu-clause id="sec-timezoneestring" type="abstract operation">
<h1>
TimeZoneString (
Expand Down
2 changes: 1 addition & 1 deletion spec/plaindate.html
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ <h1>TemporalDateToString ( _temporalDate_, _showCalendar_ )</h1>
<emu-alg>
1. Assert: Type(_temporalDate_) is Object.
1. Assert: _temporalDate_ has an [[InitializedTemporalDate]] internal slot.
1. Let _year_ be ! PadISOYear(_temporalDate_.[[ISOYear]]).
1. Let _year_ be PadISOYear(_temporalDate_.[[ISOYear]]).
1. Let _month_ be ToZeroPaddedDecimalString(_temporalDate_.[[ISOMonth]], 2).
1. Let _day_ be ToZeroPaddedDecimalString(_temporalDate_.[[ISODay]], 2).
1. Let _calendar_ be ? MaybeFormatCalendarAnnotation(_temporalDate_.[[Calendar]], _showCalendar_).
Expand Down
13 changes: 6 additions & 7 deletions spec/plaindatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -993,14 +993,13 @@ <h1>
<h1>TemporalDateTimeToString ( _isoYear_, _isoMonth_, _isoDay_, _hour_, _minute_, _second_, _millisecond_, _microsecond_, _nanosecond_, _calendar_, _precision_, _showCalendar_ )</h1>
<emu-alg>
1. Assert: _isoYear_, _isoMonth_, _isoDay_, _hour_, _minute_, _second_, _millisecond_, _microsecond_, and _nanosecond_ are integers.
1. Let _year_ be ! PadISOYear(_isoYear_).
1. Let _month_ be ToZeroPaddedDecimalString(_isoMonth_, 2).
1. Let _day_ be ToZeroPaddedDecimalString(_isoDay_, 2).
1. Let _hour_ be ToZeroPaddedDecimalString(_hour_, 2).
1. Let _minute_ be ToZeroPaddedDecimalString(_minute_, 2).
1. Let _seconds_ be ! FormatSecondsStringPart(_second_, _millisecond_, _microsecond_, _nanosecond_, _precision_).
1. Let _yearString_ be PadISOYear(_isoYear_).
1. Let _monthString_ be ToZeroPaddedDecimalString(_isoMonth_, 2).
1. Let _dayString_ be ToZeroPaddedDecimalString(_isoDay_, 2).
1. Let _subSecondNanoseconds_ be _millisecond_ &times; 10<sup>6</sup> + _microsecond_ &times; 10<sup>3</sup> + _nanosecond_.
1. Let _timeString_ be FormatTimeString(_hour_, _minute_, _second_, _subSecondNanoseconds_, _precision_).
1. Let _calendarString_ be ? MaybeFormatCalendarAnnotation(_calendar_, _showCalendar_).
1. Return the string-concatenation of _year_, the code unit 0x002D (HYPHEN-MINUS), _month_, the code unit 0x002D (HYPHEN-MINUS), _day_, 0x0054 (LATIN CAPITAL LETTER T), _hour_, the code unit 0x003A (COLON), _minute_, _seconds_, and _calendarString_.
1. Return the string-concatenation of _yearString_, the code unit 0x002D (HYPHEN-MINUS), _monthString_, the code unit 0x002D (HYPHEN-MINUS), _dayString_, 0x0054 (LATIN CAPITAL LETTER T), _timeString_, and _calendarString_.
</emu-alg>
</emu-clause>

Expand Down
2 changes: 1 addition & 1 deletion spec/plainmonthday.html
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ <h1>TemporalMonthDayToString ( _monthDay_, _showCalendar_ )</h1>
1. Let _result_ be the string-concatenation of _month_, the code unit 0x002D (HYPHEN-MINUS), and _day_.
1. Let _calendarIdentifier_ be ? ToTemporalCalendarIdentifier(_monthDay_.[[Calendar]]).
1. If _showCalendar_ is one of *"always"* or *"critical"*, or if _calendarIdentifier_ is not *"iso8601"*, then
1. Let _year_ be ! PadISOYear(_monthDay_.[[ISOYear]]).
1. Let _year_ be PadISOYear(_monthDay_.[[ISOYear]]).
1. Set _result_ to the string-concatenation of _year_, the code unit 0x002D (HYPHEN-MINUS), and _result_.
1. Let _calendarString_ be FormatCalendarAnnotation(_calendarIdentifier_, _showCalendar_).
1. Set _result_ to the string-concatenation of _result_ and _calendarString_.
Expand Down
6 changes: 2 additions & 4 deletions spec/plaintime.html
Original file line number Diff line number Diff line change
Expand Up @@ -813,10 +813,8 @@ <h1>
<h1>TemporalTimeToString ( _hour_, _minute_, _second_, _millisecond_, _microsecond_, _nanosecond_, _precision_ )</h1>
<emu-alg>
1. Assert: _hour_, _minute_, _second_, _millisecond_, _microsecond_ and _nanosecond_ are integers.
1. Let _hour_ be ToZeroPaddedDecimalString(_hour_, 2).
1. Let _minute_ be ToZeroPaddedDecimalString(_minute_, 2).
1. Let _seconds_ be ! FormatSecondsStringPart(_second_, _millisecond_, _microsecond_, _nanosecond_, _precision_).
1. Return the string-concatenation of _hour_, the code unit 0x003A (COLON), _minute_, and _seconds_.
1. Let _subSecondNanoseconds_ be _millisecond_ &times; 10<sup>6</sup> + _microsecond_ &times; 10<sup>3</sup> + _nanosecond_.
1. Return FormatTimeString(_hour_, _minute_, _second_, _subSecondNanoseconds_, _precision_).
</emu-alg>
</emu-clause>

Expand Down
Loading

0 comments on commit c429eed

Please sign in to comment.