summaryrefslogtreecommitdiffstats
path: root/js/src/builtin/temporal/ToString.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/builtin/temporal/ToString.cpp')
-rw-r--r--js/src/builtin/temporal/ToString.cpp679
1 files changed, 679 insertions, 0 deletions
diff --git a/js/src/builtin/temporal/ToString.cpp b/js/src/builtin/temporal/ToString.cpp
new file mode 100644
index 0000000000..c789c5e95c
--- /dev/null
+++ b/js/src/builtin/temporal/ToString.cpp
@@ -0,0 +1,679 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: set ts=8 sts=2 et sw=2 tw=80:
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "builtin/temporal/ToString.h"
+
+#include "mozilla/Assertions.h"
+
+#include <cstdlib>
+#include <stddef.h>
+#include <stdint.h>
+#include <type_traits>
+#include <utility>
+
+#include "builtin/temporal/Calendar.h"
+#include "builtin/temporal/Instant.h"
+#include "builtin/temporal/PlainDate.h"
+#include "builtin/temporal/PlainMonthDay.h"
+#include "builtin/temporal/PlainYearMonth.h"
+#include "builtin/temporal/Temporal.h"
+#include "builtin/temporal/TemporalRoundingMode.h"
+#include "builtin/temporal/TemporalTypes.h"
+#include "builtin/temporal/TemporalUnit.h"
+#include "builtin/temporal/TimeZone.h"
+#include "builtin/temporal/ZonedDateTime.h"
+#include "gc/Policy.h"
+#include "js/RootingAPI.h"
+#include "util/StringBuffer.h"
+#include "vm/StringType.h"
+
+using namespace js;
+using namespace js::temporal;
+
+enum class TemporalStringFormat {
+ None,
+ Date,
+ Time,
+ DateTime,
+ YearMonth,
+ MonthDay,
+ ZonedDateTime,
+ Instant,
+};
+
+class TemporalStringBuilder {
+ JSStringBuilder sb_;
+
+ TemporalStringFormat kind_ = TemporalStringFormat::None;
+
+#ifdef DEBUG
+ bool reserved_ = false;
+#endif
+
+ static constexpr size_t reserveAmount(TemporalStringFormat format) {
+ // Note: This doesn't reserve too much space, because the string builder
+ // already internally reserves space for 64 characters.
+
+ constexpr size_t datePart = 1 + 6 + 1 + 2 + 1 + 2; // 13
+ constexpr size_t timePart = 2 + 1 + 2 + 1 + 2 + 1 + 9; // 18
+ constexpr size_t dateTimePart = datePart + 1 + timePart; // including 'T'
+ constexpr size_t timeZoneOffsetPart = 1 + 2 + 1 + 2; // 6
+
+ switch (format) {
+ case TemporalStringFormat::Date:
+ case TemporalStringFormat::YearMonth:
+ case TemporalStringFormat::MonthDay:
+ return datePart;
+ case TemporalStringFormat::Time:
+ return timePart;
+ case TemporalStringFormat::DateTime:
+ return dateTimePart;
+ case TemporalStringFormat::ZonedDateTime:
+ return dateTimePart + timeZoneOffsetPart;
+ case TemporalStringFormat::Instant:
+ return dateTimePart + timeZoneOffsetPart;
+ case TemporalStringFormat::None:
+ break;
+ }
+ MOZ_CRASH("invalid reserve amount");
+ }
+
+ public:
+ TemporalStringBuilder(JSContext* cx, TemporalStringFormat kind)
+ : sb_(cx), kind_(kind) {
+ MOZ_ASSERT(kind != TemporalStringFormat::None);
+ }
+
+ bool reserve() {
+ MOZ_ASSERT(!reserved_);
+
+ if (!sb_.reserve(reserveAmount(kind_))) {
+ return false;
+ }
+
+#ifdef DEBUG
+ reserved_ = true;
+#endif
+ return true;
+ }
+
+ void append(char value) {
+ MOZ_ASSERT(reserved_);
+ sb_.infallibleAppend(value);
+ }
+
+ void appendTwoDigit(int32_t value) {
+ MOZ_ASSERT(0 <= value && value <= 99);
+ MOZ_ASSERT(reserved_);
+
+ sb_.infallibleAppend(char('0' + (value / 10)));
+ sb_.infallibleAppend(char('0' + (value % 10)));
+ }
+
+ void appendFourDigit(int32_t value) {
+ MOZ_ASSERT(0 <= value && value <= 9999);
+ MOZ_ASSERT(reserved_);
+
+ sb_.infallibleAppend(char('0' + (value / 1000)));
+ sb_.infallibleAppend(char('0' + (value % 1000) / 100));
+ sb_.infallibleAppend(char('0' + (value % 100) / 10));
+ sb_.infallibleAppend(char('0' + (value % 10)));
+ }
+
+ void appendSixDigit(int32_t value) {
+ MOZ_ASSERT(0 <= value && value <= 999999);
+ MOZ_ASSERT(reserved_);
+
+ sb_.infallibleAppend(char('0' + (value / 100000)));
+ sb_.infallibleAppend(char('0' + (value % 100000) / 10000));
+ sb_.infallibleAppend(char('0' + (value % 10000) / 1000));
+ sb_.infallibleAppend(char('0' + (value % 1000) / 100));
+ sb_.infallibleAppend(char('0' + (value % 100) / 10));
+ sb_.infallibleAppend(char('0' + (value % 10)));
+ }
+
+ void appendYear(int32_t year) {
+ if (0 <= year && year <= 9999) {
+ appendFourDigit(year);
+ } else {
+ append(year < 0 ? '-' : '+');
+ appendSixDigit(std::abs(year));
+ }
+ }
+
+ auto* finishString() { return sb_.finishString(); }
+
+ auto& builder() { return sb_; }
+};
+
+/**
+ * FormatFractionalSeconds ( subSecondNanoseconds, precision )
+ */
+static void FormatFractionalSeconds(TemporalStringBuilder& result,
+ int32_t subSecondNanoseconds,
+ Precision precision) {
+ MOZ_ASSERT(0 <= subSecondNanoseconds && subSecondNanoseconds < 1'000'000'000);
+ MOZ_ASSERT(precision != Precision::Minute());
+
+ // Steps 1-2.
+ if (precision == Precision::Auto()) {
+ // Step 1.a.
+ if (subSecondNanoseconds == 0) {
+ return;
+ }
+
+ // Step 3. (Reordered)
+ result.append('.');
+
+ // Steps 1.b-c.
+ uint32_t k = 100'000'000;
+ do {
+ result.append(char('0' + (subSecondNanoseconds / k)));
+ subSecondNanoseconds %= k;
+ k /= 10;
+ } while (subSecondNanoseconds);
+ } else {
+ // Step 2.a.
+ uint8_t p = precision.value();
+ if (p == 0) {
+ return;
+ }
+
+ // Step 3. (Reordered)
+ result.append('.');
+
+ // Steps 2.b-c.
+ uint32_t k = 100'000'000;
+ for (uint8_t i = 0; i < p; i++) {
+ result.append(char('0' + (subSecondNanoseconds / k)));
+ subSecondNanoseconds %= k;
+ k /= 10;
+ }
+ }
+}
+
+/**
+ * FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision )
+ */
+static void FormatTimeString(TemporalStringBuilder& result,
+ const PlainTime& time, Precision precision) {
+ // Step 1.
+ result.appendTwoDigit(time.hour);
+
+ // Step 2.
+ result.append(':');
+ result.appendTwoDigit(time.minute);
+
+ // Steps 4-7.
+ if (precision != Precision::Minute()) {
+ result.append(':');
+ result.appendTwoDigit(time.second);
+
+ int32_t subSecondNanoseconds = time.millisecond * 1'000'000 +
+ time.microsecond * 1'000 + time.nanosecond;
+ FormatFractionalSeconds(result, subSecondNanoseconds, precision);
+ }
+}
+
+static void FormatDateString(TemporalStringBuilder& result,
+ const PlainDate& date) {
+ result.appendYear(date.year);
+ result.append('-');
+ result.appendTwoDigit(date.month);
+ result.append('-');
+ result.appendTwoDigit(date.day);
+}
+
+static void FormatDateTimeString(TemporalStringBuilder& result,
+ const PlainDateTime& dateTime,
+ Precision precision) {
+ FormatDateString(result, dateTime.date);
+ result.append('T');
+ FormatTimeString(result, dateTime.time, precision);
+}
+
+/**
+ * FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] )
+ */
+static void FormatOffsetTimeZoneIdentifier(TemporalStringBuilder& result,
+ int32_t offsetMinutes) {
+ MOZ_ASSERT(std::abs(offsetMinutes) < UnitsPerDay(TemporalUnit::Minute),
+ "time zone offset mustn't exceed 24-hours");
+
+ // Step 1.
+ char sign = offsetMinutes >= 0 ? '+' : '-';
+
+ // Step 2.
+ int32_t absoluteMinutes = std::abs(offsetMinutes);
+
+ // Step 3.
+ int32_t hours = absoluteMinutes / 60;
+
+ // Step 4.
+ int32_t minutes = absoluteMinutes % 60;
+
+ // Steps 5-6. (Inlined FormatTimeString)
+ result.append(sign);
+ result.appendTwoDigit(hours);
+ result.append(':');
+ result.appendTwoDigit(minutes);
+}
+
+// Returns |RoundNumberToIncrement(offsetNanoseconds, 60 × 10^9, "halfExpand")|
+// divided by |60 × 10^9|.
+static int32_t RoundNanosecondsToMinutes(int64_t offsetNanoseconds) {
+ MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day));
+
+ constexpr int64_t increment = ToNanoseconds(TemporalUnit::Minute);
+
+ int64_t quotient = offsetNanoseconds / increment;
+ int64_t remainder = offsetNanoseconds % increment;
+ if (std::abs(remainder * 2) >= increment) {
+ quotient += (offsetNanoseconds > 0 ? 1 : -1);
+ }
+ return quotient;
+}
+
+/**
+ * FormatDateTimeUTCOffsetRounded ( offsetNanoseconds )
+ */
+static void FormatDateTimeUTCOffsetRounded(TemporalStringBuilder& result,
+ int64_t offsetNanoseconds) {
+ MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day));
+
+ // Steps 1-3.
+ int32_t offsetMinutes = RoundNanosecondsToMinutes(offsetNanoseconds);
+
+ // Step 4.
+ FormatOffsetTimeZoneIdentifier(result, offsetMinutes);
+}
+
+/**
+ * FormatCalendarAnnotation ( id, showCalendar )
+ */
+static bool FormatCalendarAnnotation(TemporalStringBuilder& result,
+ JSLinearString* id,
+ CalendarOption showCalendar) {
+ switch (showCalendar) {
+ case CalendarOption::Never:
+ return true;
+
+ case CalendarOption::Auto: {
+ if (StringEqualsLiteral(id, "iso8601")) {
+ return true;
+ }
+ [[fallthrough]];
+ }
+
+ case CalendarOption::Always: {
+ auto& sb = result.builder();
+ return sb.append("[u-ca=") && sb.append(id) && sb.append(']');
+ }
+
+ case CalendarOption::Critical: {
+ auto& sb = result.builder();
+ return sb.append("[!u-ca=") && sb.append(id) && sb.append(']');
+ }
+ }
+ MOZ_CRASH("bad calendar option");
+}
+
+/**
+ * MaybeFormatCalendarAnnotation ( calendar, showCalendar )
+ */
+static bool MaybeFormatCalendarAnnotation(JSContext* cx,
+ TemporalStringBuilder& result,
+ Handle<CalendarValue> calendar,
+ CalendarOption showCalendar) {
+ // Step 1.
+ if (showCalendar == CalendarOption::Never) {
+ return true;
+ }
+
+ // Step 2.
+ JSString* calendarIdentifier = ToTemporalCalendarIdentifier(cx, calendar);
+ if (!calendarIdentifier) {
+ return false;
+ }
+
+ JSLinearString* linearCalendarId = calendarIdentifier->ensureLinear(cx);
+ if (!linearCalendarId) {
+ return false;
+ }
+
+ // Step 3.
+ return FormatCalendarAnnotation(result, linearCalendarId, showCalendar);
+}
+
+static bool FormatTimeZoneAnnotation(TemporalStringBuilder& result,
+ JSLinearString* id,
+ TimeZoneNameOption showTimeZone) {
+ switch (showTimeZone) {
+ case TimeZoneNameOption::Never:
+ return true;
+
+ case TimeZoneNameOption::Auto: {
+ auto& sb = result.builder();
+ return sb.append("[") && sb.append(id) && sb.append(']');
+ }
+
+ case TimeZoneNameOption::Critical: {
+ auto& sb = result.builder();
+ return sb.append("[!") && sb.append(id) && sb.append(']');
+ }
+ }
+ MOZ_CRASH("bad time zone option");
+}
+
+static bool MaybeFormatTimeZoneAnnotation(JSContext* cx,
+ TemporalStringBuilder& result,
+ Handle<TimeZoneValue> timeZone,
+ TimeZoneNameOption showTimeZone) {
+ if (showTimeZone == TimeZoneNameOption::Never) {
+ return true;
+ }
+
+ JSString* timeZoneIdentifier = ToTemporalTimeZoneIdentifier(cx, timeZone);
+ if (!timeZoneIdentifier) {
+ return false;
+ }
+
+ JSLinearString* linearTimeZoneId = timeZoneIdentifier->ensureLinear(cx);
+ if (!linearTimeZoneId) {
+ return false;
+ }
+
+ return FormatTimeZoneAnnotation(result, linearTimeZoneId, showTimeZone);
+}
+
+/**
+ * TemporalInstantToString ( instant, timeZone, precision )
+ */
+JSString* js::temporal::TemporalInstantToString(JSContext* cx,
+ Handle<InstantObject*> instant,
+ Handle<TimeZoneValue> timeZone,
+ Precision precision) {
+ TemporalStringBuilder result(cx, TemporalStringFormat::Instant);
+ if (!result.reserve()) {
+ return nullptr;
+ }
+
+ // Steps 1-2. (Not applicable in our implementation.)
+
+ // Steps 3-6.
+ int64_t offsetNanoseconds = 0;
+ if (timeZone) {
+ // Steps 3-4. (Not applicable)
+
+ // Steps 5-6.
+ if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) {
+ return nullptr;
+ }
+ MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day));
+ }
+
+ // Step 7.
+ auto dateTime = GetPlainDateTimeFor(ToInstant(instant), offsetNanoseconds);
+
+ // Step 8. (Inlined TemporalDateTimeToString)
+ FormatDateTimeString(result, dateTime, precision);
+
+ // Steps 9-10.
+ Rooted<JSString*> timeZoneString(cx);
+ if (!timeZone) {
+ // Step 9.a.
+ result.append('Z');
+ } else {
+ // Step 10.a.
+ FormatDateTimeUTCOffsetRounded(result, offsetNanoseconds);
+ }
+
+ // Step 11.
+ return result.finishString();
+}
+
+/**
+ * TemporalDateToString ( temporalDate, showCalendar )
+ */
+JSString* js::temporal::TemporalDateToString(
+ JSContext* cx, Handle<PlainDateObject*> temporalDate,
+ CalendarOption showCalendar) {
+ auto date = ToPlainDate(temporalDate);
+
+ // Steps 1-2. (Not applicable in our implementation.)
+
+ TemporalStringBuilder result(cx, TemporalStringFormat::Date);
+ if (!result.reserve()) {
+ return nullptr;
+ }
+
+ // Steps 3-5.
+ FormatDateString(result, date);
+
+ // Step 6.
+ Rooted<CalendarValue> calendar(cx, temporalDate->calendar());
+ if (!MaybeFormatCalendarAnnotation(cx, result, calendar, showCalendar)) {
+ return nullptr;
+ }
+
+ // Step 7.
+ return result.finishString();
+}
+
+/**
+ * TemporalDateTimeToString ( isoYear, isoMonth, isoDay, hour, minute, second,
+ * millisecond, microsecond, nanosecond, calendar, precision, showCalendar )
+ */
+JSString* js::temporal::TemporalDateTimeToString(JSContext* cx,
+ const PlainDateTime& dateTime,
+ Handle<CalendarValue> calendar,
+ Precision precision,
+ CalendarOption showCalendar) {
+ TemporalStringBuilder result(cx, TemporalStringFormat::DateTime);
+ if (!result.reserve()) {
+ return nullptr;
+ }
+
+ // Step 1. (Not applicable in our implementation.)
+
+ // Steps 2-6.
+ FormatDateTimeString(result, dateTime, precision);
+
+ // Step 7.
+ if (!MaybeFormatCalendarAnnotation(cx, result, calendar, showCalendar)) {
+ return nullptr;
+ }
+
+ // Step 8.
+ return result.finishString();
+}
+
+/**
+ * TemporalTimeToString ( hour, minute, second, millisecond, microsecond,
+ * nanosecond, precision )
+ */
+JSString* js::temporal::TemporalTimeToString(JSContext* cx,
+ const PlainTime& time,
+ Precision precision) {
+ // Step 1. (Not applicable in our implementation.)
+
+ TemporalStringBuilder result(cx, TemporalStringFormat::Time);
+ if (!result.reserve()) {
+ return nullptr;
+ }
+
+ // Steps 2-3.
+ FormatTimeString(result, time, precision);
+
+ return result.finishString();
+}
+
+/**
+ * TemporalMonthDayToString ( monthDay, showCalendar )
+ */
+JSString* js::temporal::TemporalMonthDayToString(
+ JSContext* cx, Handle<PlainMonthDayObject*> monthDay,
+ CalendarOption showCalendar) {
+ // Steps 1-2. (Not applicable in our implementation.)
+
+ TemporalStringBuilder result(cx, TemporalStringFormat::MonthDay);
+ if (!result.reserve()) {
+ return nullptr;
+ }
+
+ // Step 6. (Reordered)
+ Rooted<CalendarValue> calendar(cx, monthDay->calendar());
+ JSString* str = ToTemporalCalendarIdentifier(cx, calendar);
+ if (!str) {
+ return nullptr;
+ }
+
+ Rooted<JSLinearString*> calendarIdentifier(cx, str->ensureLinear(cx));
+ if (!calendarIdentifier) {
+ return nullptr;
+ }
+
+ // Steps 3-5 and 7.
+ auto date = ToPlainDate(monthDay);
+ if (showCalendar == CalendarOption::Always ||
+ showCalendar == CalendarOption::Critical ||
+ !StringEqualsLiteral(calendarIdentifier, "iso8601")) {
+ // FIXME: spec issue - don't print "year" part when showCalendar is "never".
+ //
+ // ```js
+ // let cal = new Proxy({id: "cal"}, {has(t, pk) { return true; }});
+ // let pmd = new Temporal.PlainMonthDay(8, 1, cal);
+ // pmd.toString({calendarName: "never"})
+ // ```
+
+ FormatDateString(result, date);
+ } else {
+ result.appendTwoDigit(date.month);
+ result.append('-');
+ result.appendTwoDigit(date.day);
+ }
+
+ // Steps 8-9.
+ if (!FormatCalendarAnnotation(result, calendarIdentifier, showCalendar)) {
+ return nullptr;
+ }
+
+ // Step 10.
+ return result.finishString();
+}
+
+/**
+ * TemporalYearMonthToString ( yearMonth, showCalendar )
+ */
+JSString* js::temporal::TemporalYearMonthToString(
+ JSContext* cx, Handle<PlainYearMonthObject*> yearMonth,
+ CalendarOption showCalendar) {
+ // Steps 1-2. (Not applicable in our implementation.)
+
+ TemporalStringBuilder result(cx, TemporalStringFormat::YearMonth);
+ if (!result.reserve()) {
+ return nullptr;
+ }
+
+ // Step 6. (Reordered)
+ Rooted<CalendarValue> calendar(cx, yearMonth->calendar());
+ JSString* str = ToTemporalCalendarIdentifier(cx, calendar);
+ if (!str) {
+ return nullptr;
+ }
+
+ Rooted<JSLinearString*> calendarIdentifier(cx, str->ensureLinear(cx));
+ if (!calendarIdentifier) {
+ return nullptr;
+ }
+
+ // Steps 3-5 and 7.
+ auto date = ToPlainDate(yearMonth);
+ if (showCalendar == CalendarOption::Always ||
+ showCalendar == CalendarOption::Critical ||
+ !StringEqualsLiteral(calendarIdentifier, "iso8601")) {
+ // FIXME: spec issue - don't print "day" part when showCalendar is "never".
+ //
+ // ```js
+ // let cal = new Proxy({id: "cal"}, {has(t, pk) { return true; }});
+ // let pym = new Temporal.PlainYearMonth(2023, 8, cal);
+ // pym.toString({calendarName: "never"})
+ // ```
+
+ FormatDateString(result, date);
+ } else {
+ result.appendYear(date.year);
+ result.append('-');
+ result.appendTwoDigit(date.month);
+ }
+
+ // Steps 8-9.
+ if (!FormatCalendarAnnotation(result, calendarIdentifier, showCalendar)) {
+ return nullptr;
+ }
+
+ // Step 10.
+ return result.finishString();
+}
+
+/**
+ * TemporalZonedDateTimeToString ( zonedDateTime, precision, showCalendar,
+ * showTimeZone, showOffset [ , increment, unit, roundingMode ] )
+ */
+JSString* js::temporal::TemporalZonedDateTimeToString(
+ JSContext* cx, Handle<ZonedDateTime> zonedDateTime, Precision precision,
+ CalendarOption showCalendar, TimeZoneNameOption showTimeZone,
+ ShowOffsetOption showOffset, Increment increment, TemporalUnit unit,
+ TemporalRoundingMode roundingMode) {
+ TemporalStringBuilder result(cx, TemporalStringFormat::ZonedDateTime);
+ if (!result.reserve()) {
+ return nullptr;
+ }
+
+ // Steps 1-3. (Not applicable in our implementation.)
+
+ // Step 4.
+ Instant ns;
+ if (!RoundTemporalInstant(cx, zonedDateTime.instant(), increment, unit,
+ roundingMode, &ns)) {
+ return nullptr;
+ }
+
+ // Step 5.
+ auto timeZone = zonedDateTime.timeZone();
+
+ // Steps 6-8.
+ int64_t offsetNanoseconds;
+ if (!GetOffsetNanosecondsFor(cx, timeZone, ns, &offsetNanoseconds)) {
+ return nullptr;
+ }
+ MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day));
+
+ // Step 9.
+ auto temporalDateTime = GetPlainDateTimeFor(ns, offsetNanoseconds);
+
+ // Step 10. (Inlined TemporalDateTimeToString)
+ FormatDateTimeString(result, temporalDateTime, precision);
+
+ // Steps 11-12.
+ if (showOffset != ShowOffsetOption::Never) {
+ FormatDateTimeUTCOffsetRounded(result, offsetNanoseconds);
+ }
+
+ // Steps 13-14.
+ if (!MaybeFormatTimeZoneAnnotation(cx, result, timeZone, showTimeZone)) {
+ return nullptr;
+ }
+
+ // Step 15.
+ if (!MaybeFormatCalendarAnnotation(cx, result, zonedDateTime.calendar(),
+ showCalendar)) {
+ return nullptr;
+ }
+
+ // Step 16.
+ return result.finishString();
+}