1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * vim: set ts=8 sts=2 et sw=2 tw=80:
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "builtin/temporal/ToString.h"
9 #include "mozilla/Assertions.h"
14 #include <type_traits>
17 #include "builtin/temporal/Calendar.h"
18 #include "builtin/temporal/Instant.h"
19 #include "builtin/temporal/PlainDate.h"
20 #include "builtin/temporal/PlainMonthDay.h"
21 #include "builtin/temporal/PlainYearMonth.h"
22 #include "builtin/temporal/Temporal.h"
23 #include "builtin/temporal/TemporalRoundingMode.h"
24 #include "builtin/temporal/TemporalTypes.h"
25 #include "builtin/temporal/TemporalUnit.h"
26 #include "builtin/temporal/TimeZone.h"
27 #include "builtin/temporal/ZonedDateTime.h"
28 #include "gc/Policy.h"
29 #include "js/RootingAPI.h"
30 #include "util/StringBuffer.h"
31 #include "vm/StringType.h"
34 using namespace js::temporal
;
36 enum class TemporalStringFormat
{
47 class TemporalStringBuilder
{
50 TemporalStringFormat kind_
= TemporalStringFormat::None
;
53 bool reserved_
= false;
56 static constexpr size_t reserveAmount(TemporalStringFormat format
) {
57 // Note: This doesn't reserve too much space, because the string builder
58 // already internally reserves space for 64 characters.
60 constexpr size_t datePart
= 1 + 6 + 1 + 2 + 1 + 2; // 13
61 constexpr size_t timePart
= 2 + 1 + 2 + 1 + 2 + 1 + 9; // 18
62 constexpr size_t dateTimePart
= datePart
+ 1 + timePart
; // including 'T'
63 constexpr size_t timeZoneOffsetPart
= 1 + 2 + 1 + 2; // 6
66 case TemporalStringFormat::Date
:
67 case TemporalStringFormat::YearMonth
:
68 case TemporalStringFormat::MonthDay
:
70 case TemporalStringFormat::Time
:
72 case TemporalStringFormat::DateTime
:
74 case TemporalStringFormat::ZonedDateTime
:
75 return dateTimePart
+ timeZoneOffsetPart
;
76 case TemporalStringFormat::Instant
:
77 return dateTimePart
+ timeZoneOffsetPart
;
78 case TemporalStringFormat::None
:
81 MOZ_CRASH("invalid reserve amount");
85 TemporalStringBuilder(JSContext
* cx
, TemporalStringFormat kind
)
86 : sb_(cx
), kind_(kind
) {
87 MOZ_ASSERT(kind
!= TemporalStringFormat::None
);
91 MOZ_ASSERT(!reserved_
);
93 if (!sb_
.reserve(reserveAmount(kind_
))) {
103 void append(char value
) {
104 MOZ_ASSERT(reserved_
);
105 sb_
.infallibleAppend(value
);
108 void appendTwoDigit(int32_t value
) {
109 MOZ_ASSERT(0 <= value
&& value
<= 99);
110 MOZ_ASSERT(reserved_
);
112 sb_
.infallibleAppend(char('0' + (value
/ 10)));
113 sb_
.infallibleAppend(char('0' + (value
% 10)));
116 void appendFourDigit(int32_t value
) {
117 MOZ_ASSERT(0 <= value
&& value
<= 9999);
118 MOZ_ASSERT(reserved_
);
120 sb_
.infallibleAppend(char('0' + (value
/ 1000)));
121 sb_
.infallibleAppend(char('0' + (value
% 1000) / 100));
122 sb_
.infallibleAppend(char('0' + (value
% 100) / 10));
123 sb_
.infallibleAppend(char('0' + (value
% 10)));
126 void appendSixDigit(int32_t value
) {
127 MOZ_ASSERT(0 <= value
&& value
<= 999999);
128 MOZ_ASSERT(reserved_
);
130 sb_
.infallibleAppend(char('0' + (value
/ 100000)));
131 sb_
.infallibleAppend(char('0' + (value
% 100000) / 10000));
132 sb_
.infallibleAppend(char('0' + (value
% 10000) / 1000));
133 sb_
.infallibleAppend(char('0' + (value
% 1000) / 100));
134 sb_
.infallibleAppend(char('0' + (value
% 100) / 10));
135 sb_
.infallibleAppend(char('0' + (value
% 10)));
138 void appendYear(int32_t year
) {
139 if (0 <= year
&& year
<= 9999) {
140 appendFourDigit(year
);
142 append(year
< 0 ? '-' : '+');
143 appendSixDigit(std::abs(year
));
147 auto* finishString() { return sb_
.finishString(); }
149 auto& builder() { return sb_
; }
153 * FormatFractionalSeconds ( subSecondNanoseconds, precision )
155 static void FormatFractionalSeconds(TemporalStringBuilder
& result
,
156 int32_t subSecondNanoseconds
,
157 Precision precision
) {
158 MOZ_ASSERT(0 <= subSecondNanoseconds
&& subSecondNanoseconds
< 1'000'000'000);
159 MOZ_ASSERT(precision
!= Precision::Minute());
162 if (precision
== Precision::Auto()) {
164 if (subSecondNanoseconds
== 0) {
168 // Step 3. (Reordered)
172 uint32_t k
= 100'000'000;
174 result
.append(char('0' + (subSecondNanoseconds
/ k
)));
175 subSecondNanoseconds
%= k
;
177 } while (subSecondNanoseconds
);
180 uint8_t p
= precision
.value();
185 // Step 3. (Reordered)
189 uint32_t k
= 100'000'000;
190 for (uint8_t i
= 0; i
< p
; i
++) {
191 result
.append(char('0' + (subSecondNanoseconds
/ k
)));
192 subSecondNanoseconds
%= k
;
199 * FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision )
201 static void FormatTimeString(TemporalStringBuilder
& result
,
202 const PlainTime
& time
, Precision precision
) {
204 result
.appendTwoDigit(time
.hour
);
208 result
.appendTwoDigit(time
.minute
);
211 if (precision
!= Precision::Minute()) {
213 result
.appendTwoDigit(time
.second
);
215 int32_t subSecondNanoseconds
= time
.millisecond
* 1'000'000 +
216 time
.microsecond
* 1'000 + time
.nanosecond
;
217 FormatFractionalSeconds(result
, subSecondNanoseconds
, precision
);
221 static void FormatDateString(TemporalStringBuilder
& result
,
222 const PlainDate
& date
) {
223 result
.appendYear(date
.year
);
225 result
.appendTwoDigit(date
.month
);
227 result
.appendTwoDigit(date
.day
);
230 static void FormatDateTimeString(TemporalStringBuilder
& result
,
231 const PlainDateTime
& dateTime
,
232 Precision precision
) {
233 FormatDateString(result
, dateTime
.date
);
235 FormatTimeString(result
, dateTime
.time
, precision
);
239 * FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] )
241 static void FormatOffsetTimeZoneIdentifier(TemporalStringBuilder
& result
,
242 int32_t offsetMinutes
) {
243 MOZ_ASSERT(std::abs(offsetMinutes
) < UnitsPerDay(TemporalUnit::Minute
),
244 "time zone offset mustn't exceed 24-hours");
247 char sign
= offsetMinutes
>= 0 ? '+' : '-';
250 int32_t absoluteMinutes
= std::abs(offsetMinutes
);
253 int32_t hours
= absoluteMinutes
/ 60;
256 int32_t minutes
= absoluteMinutes
% 60;
258 // Steps 5-6. (Inlined FormatTimeString)
260 result
.appendTwoDigit(hours
);
262 result
.appendTwoDigit(minutes
);
265 // Returns |RoundNumberToIncrement(offsetNanoseconds, 60 × 10^9, "halfExpand")|
266 // divided by |60 × 10^9|.
267 static int32_t RoundNanosecondsToMinutes(int64_t offsetNanoseconds
) {
268 MOZ_ASSERT(std::abs(offsetNanoseconds
) < ToNanoseconds(TemporalUnit::Day
));
270 constexpr int64_t increment
= ToNanoseconds(TemporalUnit::Minute
);
272 int64_t quotient
= offsetNanoseconds
/ increment
;
273 int64_t remainder
= offsetNanoseconds
% increment
;
274 if (std::abs(remainder
* 2) >= increment
) {
275 quotient
+= (offsetNanoseconds
> 0 ? 1 : -1);
281 * FormatDateTimeUTCOffsetRounded ( offsetNanoseconds )
283 static void FormatDateTimeUTCOffsetRounded(TemporalStringBuilder
& result
,
284 int64_t offsetNanoseconds
) {
285 MOZ_ASSERT(std::abs(offsetNanoseconds
) < ToNanoseconds(TemporalUnit::Day
));
288 int32_t offsetMinutes
= RoundNanosecondsToMinutes(offsetNanoseconds
);
291 FormatOffsetTimeZoneIdentifier(result
, offsetMinutes
);
295 * FormatCalendarAnnotation ( id, showCalendar )
297 static bool FormatCalendarAnnotation(TemporalStringBuilder
& result
,
299 CalendarOption showCalendar
) {
300 switch (showCalendar
) {
301 case CalendarOption::Never
:
304 case CalendarOption::Auto
: {
305 if (StringEqualsLiteral(id
, "iso8601")) {
311 case CalendarOption::Always
: {
312 auto& sb
= result
.builder();
313 return sb
.append("[u-ca=") && sb
.append(id
) && sb
.append(']');
316 case CalendarOption::Critical
: {
317 auto& sb
= result
.builder();
318 return sb
.append("[!u-ca=") && sb
.append(id
) && sb
.append(']');
321 MOZ_CRASH("bad calendar option");
325 * MaybeFormatCalendarAnnotation ( calendar, showCalendar )
327 static bool MaybeFormatCalendarAnnotation(JSContext
* cx
,
328 TemporalStringBuilder
& result
,
329 Handle
<CalendarValue
> calendar
,
330 CalendarOption showCalendar
) {
332 if (showCalendar
== CalendarOption::Never
) {
337 JSString
* calendarIdentifier
= ToTemporalCalendarIdentifier(cx
, calendar
);
338 if (!calendarIdentifier
) {
342 JSLinearString
* linearCalendarId
= calendarIdentifier
->ensureLinear(cx
);
343 if (!linearCalendarId
) {
348 return FormatCalendarAnnotation(result
, linearCalendarId
, showCalendar
);
351 static bool FormatTimeZoneAnnotation(TemporalStringBuilder
& result
,
353 TimeZoneNameOption showTimeZone
) {
354 switch (showTimeZone
) {
355 case TimeZoneNameOption::Never
:
358 case TimeZoneNameOption::Auto
: {
359 auto& sb
= result
.builder();
360 return sb
.append("[") && sb
.append(id
) && sb
.append(']');
363 case TimeZoneNameOption::Critical
: {
364 auto& sb
= result
.builder();
365 return sb
.append("[!") && sb
.append(id
) && sb
.append(']');
368 MOZ_CRASH("bad time zone option");
371 static bool MaybeFormatTimeZoneAnnotation(JSContext
* cx
,
372 TemporalStringBuilder
& result
,
373 Handle
<TimeZoneValue
> timeZone
,
374 TimeZoneNameOption showTimeZone
) {
375 if (showTimeZone
== TimeZoneNameOption::Never
) {
379 JSString
* timeZoneIdentifier
= ToTemporalTimeZoneIdentifier(cx
, timeZone
);
380 if (!timeZoneIdentifier
) {
384 JSLinearString
* linearTimeZoneId
= timeZoneIdentifier
->ensureLinear(cx
);
385 if (!linearTimeZoneId
) {
389 return FormatTimeZoneAnnotation(result
, linearTimeZoneId
, showTimeZone
);
393 * TemporalInstantToString ( instant, timeZone, precision )
395 JSString
* js::temporal::TemporalInstantToString(JSContext
* cx
,
396 Handle
<InstantObject
*> instant
,
397 Handle
<TimeZoneValue
> timeZone
,
398 Precision precision
) {
399 TemporalStringBuilder
result(cx
, TemporalStringFormat::Instant
);
400 if (!result
.reserve()) {
404 // Steps 1-2. (Not applicable in our implementation.)
407 int64_t offsetNanoseconds
= 0;
409 // Steps 3-4. (Not applicable)
412 if (!GetOffsetNanosecondsFor(cx
, timeZone
, instant
, &offsetNanoseconds
)) {
415 MOZ_ASSERT(std::abs(offsetNanoseconds
) < ToNanoseconds(TemporalUnit::Day
));
419 auto dateTime
= GetPlainDateTimeFor(ToInstant(instant
), offsetNanoseconds
);
421 // Step 8. (Inlined TemporalDateTimeToString)
422 FormatDateTimeString(result
, dateTime
, precision
);
425 Rooted
<JSString
*> timeZoneString(cx
);
431 FormatDateTimeUTCOffsetRounded(result
, offsetNanoseconds
);
435 return result
.finishString();
439 * TemporalDateToString ( temporalDate, showCalendar )
441 JSString
* js::temporal::TemporalDateToString(
442 JSContext
* cx
, Handle
<PlainDateObject
*> temporalDate
,
443 CalendarOption showCalendar
) {
444 auto date
= ToPlainDate(temporalDate
);
446 // Steps 1-2. (Not applicable in our implementation.)
448 TemporalStringBuilder
result(cx
, TemporalStringFormat::Date
);
449 if (!result
.reserve()) {
454 FormatDateString(result
, date
);
457 Rooted
<CalendarValue
> calendar(cx
, temporalDate
->calendar());
458 if (!MaybeFormatCalendarAnnotation(cx
, result
, calendar
, showCalendar
)) {
463 return result
.finishString();
467 * TemporalDateTimeToString ( isoYear, isoMonth, isoDay, hour, minute, second,
468 * millisecond, microsecond, nanosecond, calendar, precision, showCalendar )
470 JSString
* js::temporal::TemporalDateTimeToString(JSContext
* cx
,
471 const PlainDateTime
& dateTime
,
472 Handle
<CalendarValue
> calendar
,
474 CalendarOption showCalendar
) {
475 TemporalStringBuilder
result(cx
, TemporalStringFormat::DateTime
);
476 if (!result
.reserve()) {
480 // Step 1. (Not applicable in our implementation.)
483 FormatDateTimeString(result
, dateTime
, precision
);
486 if (!MaybeFormatCalendarAnnotation(cx
, result
, calendar
, showCalendar
)) {
491 return result
.finishString();
495 * TemporalTimeToString ( hour, minute, second, millisecond, microsecond,
496 * nanosecond, precision )
498 JSString
* js::temporal::TemporalTimeToString(JSContext
* cx
,
499 const PlainTime
& time
,
500 Precision precision
) {
501 // Step 1. (Not applicable in our implementation.)
503 TemporalStringBuilder
result(cx
, TemporalStringFormat::Time
);
504 if (!result
.reserve()) {
509 FormatTimeString(result
, time
, precision
);
511 return result
.finishString();
515 * TemporalMonthDayToString ( monthDay, showCalendar )
517 JSString
* js::temporal::TemporalMonthDayToString(
518 JSContext
* cx
, Handle
<PlainMonthDayObject
*> monthDay
,
519 CalendarOption showCalendar
) {
520 // Steps 1-2. (Not applicable in our implementation.)
522 TemporalStringBuilder
result(cx
, TemporalStringFormat::MonthDay
);
523 if (!result
.reserve()) {
527 // Step 6. (Reordered)
528 Rooted
<CalendarValue
> calendar(cx
, monthDay
->calendar());
529 JSString
* str
= ToTemporalCalendarIdentifier(cx
, calendar
);
534 Rooted
<JSLinearString
*> calendarIdentifier(cx
, str
->ensureLinear(cx
));
535 if (!calendarIdentifier
) {
540 auto date
= ToPlainDate(monthDay
);
541 if (showCalendar
== CalendarOption::Always
||
542 showCalendar
== CalendarOption::Critical
||
543 !StringEqualsLiteral(calendarIdentifier
, "iso8601")) {
544 // FIXME: spec issue - don't print "year" part when showCalendar is "never".
547 // let cal = new Proxy({id: "cal"}, {has(t, pk) { return true; }});
548 // let pmd = new Temporal.PlainMonthDay(8, 1, cal);
549 // pmd.toString({calendarName: "never"})
552 FormatDateString(result
, date
);
554 result
.appendTwoDigit(date
.month
);
556 result
.appendTwoDigit(date
.day
);
560 if (!FormatCalendarAnnotation(result
, calendarIdentifier
, showCalendar
)) {
565 return result
.finishString();
569 * TemporalYearMonthToString ( yearMonth, showCalendar )
571 JSString
* js::temporal::TemporalYearMonthToString(
572 JSContext
* cx
, Handle
<PlainYearMonthObject
*> yearMonth
,
573 CalendarOption showCalendar
) {
574 // Steps 1-2. (Not applicable in our implementation.)
576 TemporalStringBuilder
result(cx
, TemporalStringFormat::YearMonth
);
577 if (!result
.reserve()) {
581 // Step 6. (Reordered)
582 Rooted
<CalendarValue
> calendar(cx
, yearMonth
->calendar());
583 JSString
* str
= ToTemporalCalendarIdentifier(cx
, calendar
);
588 Rooted
<JSLinearString
*> calendarIdentifier(cx
, str
->ensureLinear(cx
));
589 if (!calendarIdentifier
) {
594 auto date
= ToPlainDate(yearMonth
);
595 if (showCalendar
== CalendarOption::Always
||
596 showCalendar
== CalendarOption::Critical
||
597 !StringEqualsLiteral(calendarIdentifier
, "iso8601")) {
598 // FIXME: spec issue - don't print "day" part when showCalendar is "never".
601 // let cal = new Proxy({id: "cal"}, {has(t, pk) { return true; }});
602 // let pym = new Temporal.PlainYearMonth(2023, 8, cal);
603 // pym.toString({calendarName: "never"})
606 FormatDateString(result
, date
);
608 result
.appendYear(date
.year
);
610 result
.appendTwoDigit(date
.month
);
614 if (!FormatCalendarAnnotation(result
, calendarIdentifier
, showCalendar
)) {
619 return result
.finishString();
623 * TemporalZonedDateTimeToString ( zonedDateTime, precision, showCalendar,
624 * showTimeZone, showOffset [ , increment, unit, roundingMode ] )
626 JSString
* js::temporal::TemporalZonedDateTimeToString(
627 JSContext
* cx
, Handle
<ZonedDateTime
> zonedDateTime
, Precision precision
,
628 CalendarOption showCalendar
, TimeZoneNameOption showTimeZone
,
629 ShowOffsetOption showOffset
, Increment increment
, TemporalUnit unit
,
630 TemporalRoundingMode roundingMode
) {
631 TemporalStringBuilder
result(cx
, TemporalStringFormat::ZonedDateTime
);
632 if (!result
.reserve()) {
636 // Steps 1-3. (Not applicable in our implementation.)
639 auto ns
= RoundTemporalInstant(zonedDateTime
.instant(), increment
, unit
,
643 auto timeZone
= zonedDateTime
.timeZone();
646 int64_t offsetNanoseconds
;
647 if (!GetOffsetNanosecondsFor(cx
, timeZone
, ns
, &offsetNanoseconds
)) {
650 MOZ_ASSERT(std::abs(offsetNanoseconds
) < ToNanoseconds(TemporalUnit::Day
));
653 auto temporalDateTime
= GetPlainDateTimeFor(ns
, offsetNanoseconds
);
655 // Step 10. (Inlined TemporalDateTimeToString)
656 FormatDateTimeString(result
, temporalDateTime
, precision
);
659 if (showOffset
!= ShowOffsetOption::Never
) {
660 FormatDateTimeUTCOffsetRounded(result
, offsetNanoseconds
);
664 if (!MaybeFormatTimeZoneAnnotation(cx
, result
, timeZone
, showTimeZone
)) {
669 if (!MaybeFormatCalendarAnnotation(cx
, result
, zonedDateTime
.calendar(),
675 return result
.finishString();