Bug 1874684 - Part 10: Replace BigInt with Int128 in RoundNumberToIncrement. r=mgaudet
[gecko.git] / js / src / builtin / temporal / ToString.cpp
blob959a3c3d880d757dc1804e612d2fba9567f02ccb
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"
11 #include <cstdlib>
12 #include <stddef.h>
13 #include <stdint.h>
14 #include <type_traits>
15 #include <utility>
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"
33 using namespace js;
34 using namespace js::temporal;
36 enum class TemporalStringFormat {
37 None,
38 Date,
39 Time,
40 DateTime,
41 YearMonth,
42 MonthDay,
43 ZonedDateTime,
44 Instant,
47 class TemporalStringBuilder {
48 JSStringBuilder sb_;
50 TemporalStringFormat kind_ = TemporalStringFormat::None;
52 #ifdef DEBUG
53 bool reserved_ = false;
54 #endif
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
65 switch (format) {
66 case TemporalStringFormat::Date:
67 case TemporalStringFormat::YearMonth:
68 case TemporalStringFormat::MonthDay:
69 return datePart;
70 case TemporalStringFormat::Time:
71 return timePart;
72 case TemporalStringFormat::DateTime:
73 return dateTimePart;
74 case TemporalStringFormat::ZonedDateTime:
75 return dateTimePart + timeZoneOffsetPart;
76 case TemporalStringFormat::Instant:
77 return dateTimePart + timeZoneOffsetPart;
78 case TemporalStringFormat::None:
79 break;
81 MOZ_CRASH("invalid reserve amount");
84 public:
85 TemporalStringBuilder(JSContext* cx, TemporalStringFormat kind)
86 : sb_(cx), kind_(kind) {
87 MOZ_ASSERT(kind != TemporalStringFormat::None);
90 bool reserve() {
91 MOZ_ASSERT(!reserved_);
93 if (!sb_.reserve(reserveAmount(kind_))) {
94 return false;
97 #ifdef DEBUG
98 reserved_ = true;
99 #endif
100 return true;
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);
141 } else {
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());
161 // Steps 1-2.
162 if (precision == Precision::Auto()) {
163 // Step 1.a.
164 if (subSecondNanoseconds == 0) {
165 return;
168 // Step 3. (Reordered)
169 result.append('.');
171 // Steps 1.b-c.
172 uint32_t k = 100'000'000;
173 do {
174 result.append(char('0' + (subSecondNanoseconds / k)));
175 subSecondNanoseconds %= k;
176 k /= 10;
177 } while (subSecondNanoseconds);
178 } else {
179 // Step 2.a.
180 uint8_t p = precision.value();
181 if (p == 0) {
182 return;
185 // Step 3. (Reordered)
186 result.append('.');
188 // Steps 2.b-c.
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;
193 k /= 10;
199 * FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision )
201 static void FormatTimeString(TemporalStringBuilder& result,
202 const PlainTime& time, Precision precision) {
203 // Step 1.
204 result.appendTwoDigit(time.hour);
206 // Step 2.
207 result.append(':');
208 result.appendTwoDigit(time.minute);
210 // Steps 4-7.
211 if (precision != Precision::Minute()) {
212 result.append(':');
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);
224 result.append('-');
225 result.appendTwoDigit(date.month);
226 result.append('-');
227 result.appendTwoDigit(date.day);
230 static void FormatDateTimeString(TemporalStringBuilder& result,
231 const PlainDateTime& dateTime,
232 Precision precision) {
233 FormatDateString(result, dateTime.date);
234 result.append('T');
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");
246 // Step 1.
247 char sign = offsetMinutes >= 0 ? '+' : '-';
249 // Step 2.
250 int32_t absoluteMinutes = std::abs(offsetMinutes);
252 // Step 3.
253 int32_t hours = absoluteMinutes / 60;
255 // Step 4.
256 int32_t minutes = absoluteMinutes % 60;
258 // Steps 5-6. (Inlined FormatTimeString)
259 result.append(sign);
260 result.appendTwoDigit(hours);
261 result.append(':');
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);
277 return quotient;
281 * FormatDateTimeUTCOffsetRounded ( offsetNanoseconds )
283 static void FormatDateTimeUTCOffsetRounded(TemporalStringBuilder& result,
284 int64_t offsetNanoseconds) {
285 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day));
287 // Steps 1-3.
288 int32_t offsetMinutes = RoundNanosecondsToMinutes(offsetNanoseconds);
290 // Step 4.
291 FormatOffsetTimeZoneIdentifier(result, offsetMinutes);
295 * FormatCalendarAnnotation ( id, showCalendar )
297 static bool FormatCalendarAnnotation(TemporalStringBuilder& result,
298 JSLinearString* id,
299 CalendarOption showCalendar) {
300 switch (showCalendar) {
301 case CalendarOption::Never:
302 return true;
304 case CalendarOption::Auto: {
305 if (StringEqualsLiteral(id, "iso8601")) {
306 return true;
308 [[fallthrough]];
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) {
331 // Step 1.
332 if (showCalendar == CalendarOption::Never) {
333 return true;
336 // Step 2.
337 JSString* calendarIdentifier = ToTemporalCalendarIdentifier(cx, calendar);
338 if (!calendarIdentifier) {
339 return false;
342 JSLinearString* linearCalendarId = calendarIdentifier->ensureLinear(cx);
343 if (!linearCalendarId) {
344 return false;
347 // Step 3.
348 return FormatCalendarAnnotation(result, linearCalendarId, showCalendar);
351 static bool FormatTimeZoneAnnotation(TemporalStringBuilder& result,
352 JSLinearString* id,
353 TimeZoneNameOption showTimeZone) {
354 switch (showTimeZone) {
355 case TimeZoneNameOption::Never:
356 return true;
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) {
376 return true;
379 JSString* timeZoneIdentifier = ToTemporalTimeZoneIdentifier(cx, timeZone);
380 if (!timeZoneIdentifier) {
381 return false;
384 JSLinearString* linearTimeZoneId = timeZoneIdentifier->ensureLinear(cx);
385 if (!linearTimeZoneId) {
386 return false;
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()) {
401 return nullptr;
404 // Steps 1-2. (Not applicable in our implementation.)
406 // Steps 3-6.
407 int64_t offsetNanoseconds = 0;
408 if (timeZone) {
409 // Steps 3-4. (Not applicable)
411 // Steps 5-6.
412 if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) {
413 return nullptr;
415 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day));
418 // Step 7.
419 auto dateTime = GetPlainDateTimeFor(ToInstant(instant), offsetNanoseconds);
421 // Step 8. (Inlined TemporalDateTimeToString)
422 FormatDateTimeString(result, dateTime, precision);
424 // Steps 9-10.
425 Rooted<JSString*> timeZoneString(cx);
426 if (!timeZone) {
427 // Step 9.a.
428 result.append('Z');
429 } else {
430 // Step 10.a.
431 FormatDateTimeUTCOffsetRounded(result, offsetNanoseconds);
434 // Step 11.
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()) {
450 return nullptr;
453 // Steps 3-5.
454 FormatDateString(result, date);
456 // Step 6.
457 Rooted<CalendarValue> calendar(cx, temporalDate->calendar());
458 if (!MaybeFormatCalendarAnnotation(cx, result, calendar, showCalendar)) {
459 return nullptr;
462 // Step 7.
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,
473 Precision precision,
474 CalendarOption showCalendar) {
475 TemporalStringBuilder result(cx, TemporalStringFormat::DateTime);
476 if (!result.reserve()) {
477 return nullptr;
480 // Step 1. (Not applicable in our implementation.)
482 // Steps 2-6.
483 FormatDateTimeString(result, dateTime, precision);
485 // Step 7.
486 if (!MaybeFormatCalendarAnnotation(cx, result, calendar, showCalendar)) {
487 return nullptr;
490 // Step 8.
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()) {
505 return nullptr;
508 // Steps 2-3.
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()) {
524 return nullptr;
527 // Step 6. (Reordered)
528 Rooted<CalendarValue> calendar(cx, monthDay->calendar());
529 JSString* str = ToTemporalCalendarIdentifier(cx, calendar);
530 if (!str) {
531 return nullptr;
534 Rooted<JSLinearString*> calendarIdentifier(cx, str->ensureLinear(cx));
535 if (!calendarIdentifier) {
536 return nullptr;
539 // Steps 3-5 and 7.
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".
546 // ```js
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"})
550 // ```
552 FormatDateString(result, date);
553 } else {
554 result.appendTwoDigit(date.month);
555 result.append('-');
556 result.appendTwoDigit(date.day);
559 // Steps 8-9.
560 if (!FormatCalendarAnnotation(result, calendarIdentifier, showCalendar)) {
561 return nullptr;
564 // Step 10.
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()) {
578 return nullptr;
581 // Step 6. (Reordered)
582 Rooted<CalendarValue> calendar(cx, yearMonth->calendar());
583 JSString* str = ToTemporalCalendarIdentifier(cx, calendar);
584 if (!str) {
585 return nullptr;
588 Rooted<JSLinearString*> calendarIdentifier(cx, str->ensureLinear(cx));
589 if (!calendarIdentifier) {
590 return nullptr;
593 // Steps 3-5 and 7.
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".
600 // ```js
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"})
604 // ```
606 FormatDateString(result, date);
607 } else {
608 result.appendYear(date.year);
609 result.append('-');
610 result.appendTwoDigit(date.month);
613 // Steps 8-9.
614 if (!FormatCalendarAnnotation(result, calendarIdentifier, showCalendar)) {
615 return nullptr;
618 // Step 10.
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()) {
633 return nullptr;
636 // Steps 1-3. (Not applicable in our implementation.)
638 // Step 4.
639 auto ns = RoundTemporalInstant(zonedDateTime.instant(), increment, unit,
640 roundingMode);
642 // Step 5.
643 auto timeZone = zonedDateTime.timeZone();
645 // Steps 6-8.
646 int64_t offsetNanoseconds;
647 if (!GetOffsetNanosecondsFor(cx, timeZone, ns, &offsetNanoseconds)) {
648 return nullptr;
650 MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day));
652 // Step 9.
653 auto temporalDateTime = GetPlainDateTimeFor(ns, offsetNanoseconds);
655 // Step 10. (Inlined TemporalDateTimeToString)
656 FormatDateTimeString(result, temporalDateTime, precision);
658 // Steps 11-12.
659 if (showOffset != ShowOffsetOption::Never) {
660 FormatDateTimeUTCOffsetRounded(result, offsetNanoseconds);
663 // Steps 13-14.
664 if (!MaybeFormatTimeZoneAnnotation(cx, result, timeZone, showTimeZone)) {
665 return nullptr;
668 // Step 15.
669 if (!MaybeFormatCalendarAnnotation(cx, result, zonedDateTime.calendar(),
670 showCalendar)) {
671 return nullptr;
674 // Step 16.
675 return result.finishString();