diff --git a/Source/JavaScriptCore/runtime/TemporalObject.cpp b/Source/JavaScriptCore/runtime/TemporalObject.cpp index d865261eb987c..3ade55a6a4bde 100644 --- a/Source/JavaScriptCore/runtime/TemporalObject.cpp +++ b/Source/JavaScriptCore/runtime/TemporalObject.cpp @@ -31,8 +31,10 @@ #include "ObjectPrototype.h" #include "Rounding.h" #include "TemporalCalendar.h" +#include "TemporalDuration.h" #include "TemporalDurationConstructor.h" #include "TemporalDurationPrototype.h" +#include "TemporalInstant.h" #include "TemporalInstantConstructor.h" #include "TemporalInstantPrototype.h" #include "TemporalNow.h" @@ -746,6 +748,31 @@ void throwTemporalError(JSGlobalObject* globalObject, ThrowScope& scope, const T throwError(globalObject, scope, error.kind == TemporalErrorKind::RangeError ? ErrorType::RangeError : ErrorType::TypeError, error.message); } +TemporalType temporalType(JSValue value) +{ + // Every Temporal class uses a plain ObjectType structure, so anything else short-circuits. + if (!value.isCell() || value.asCell()->type() != ObjectType) + return TemporalType::None; + JSCell* cell = value.asCell(); + if (cell->inherits()) + return TemporalType::Instant; + if (cell->inherits()) + return TemporalType::PlainDateTime; + if (cell->inherits()) + return TemporalType::PlainDate; + if (cell->inherits()) + return TemporalType::PlainTime; + if (cell->inherits()) + return TemporalType::ZonedDateTime; + if (cell->inherits()) + return TemporalType::PlainYearMonth; + if (cell->inherits()) + return TemporalType::PlainMonthDay; + if (cell->inherits()) + return TemporalType::Duration; + return TemporalType::None; +} + } // namespace JSC WTF_ALLOW_UNSAFE_BUFFER_USAGE_END diff --git a/Source/JavaScriptCore/runtime/TemporalObject.h b/Source/JavaScriptCore/runtime/TemporalObject.h index 4f1d526ed1504..3dc1103e960af 100644 --- a/Source/JavaScriptCore/runtime/TemporalObject.h +++ b/Source/JavaScriptCore/runtime/TemporalObject.h @@ -137,4 +137,20 @@ void throwTemporalError(JSGlobalObject*, ThrowScope&, const TemporalError&); std::optional toTemporalTimeZoneIdentifier(JSGlobalObject*, JSValue); +// Which [[InitializedTemporal*]] internal slot a value carries. Embedders mirror +// these discriminants, so keep them stable. +enum class TemporalType : uint8_t { + None = 0, + Instant = 1, + PlainDateTime = 2, + PlainDate = 3, + PlainTime = 4, + ZonedDateTime = 5, + PlainYearMonth = 6, + PlainMonthDay = 7, + Duration = 8, +}; + +JS_EXPORT_PRIVATE TemporalType temporalType(JSValue); + } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/TemporalZonedDateTime.cpp b/Source/JavaScriptCore/runtime/TemporalZonedDateTime.cpp index d08bbe454b995..a788785d8e404 100644 --- a/Source/JavaScriptCore/runtime/TemporalZonedDateTime.cpp +++ b/Source/JavaScriptCore/runtime/TemporalZonedDateTime.cpp @@ -31,6 +31,7 @@ #include "ISO8601.h" #include "IntlObject.h" #include "JSCInlines.h" +#include "Rounding.h" #include "TemporalCalendar.h" #include "TemporalCoreTypes.h" #include "TemporalDuration.h" @@ -42,6 +43,7 @@ #include #include +#include namespace JSC { @@ -121,6 +123,79 @@ ISO8601::PlainDateTime TemporalZonedDateTime::getLocalDateTime(JSGlobalObject* g return *result; } +// https://tc39.es/proposal-temporal/#sec-temporal-temporalzoneddatetimetostring +String TemporalZonedDateTime::toString(JSGlobalObject* globalObject, const PrecisionData& precision, RoundingMode roundingMode, StringView showOffset, StringView showTimeZone, StringView showCalendar) const +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + // Steps 1-3: Default increment/unit/roundingMode when not present. + // (Callers already pass concrete values, so these defaults are effectively no-ops here.) + // Step 4: Let epochNs be zonedDateTime.[[EpochNanoseconds]]. + // Step 5: Set epochNs to RoundTemporalInstant(epochNs, increment, unit, roundingMode). + Int128 epochNs = m_exactTime.epochNanoseconds(); + Int128 incrementNs = static_cast(lengthInNanoseconds(precision.unit)) * static_cast(static_cast(precision.increment)); + if (incrementNs > 0) + epochNs = TemporalCore::roundNumberToIncrementAsIfPositive(epochNs, incrementNs, roundingMode); + ISO8601::ExactTime roundedExact(epochNs); + + // Step 6: Let timeZone be zonedDateTime.[[TimeZone]]. + // Step 7: Let offsetNanoseconds be GetOffsetNanosecondsFor(timeZone, epochNs). + auto offsetOpt = TemporalCore::getOffsetNanosecondsFor(m_timeZone, roundedExact); + if (!offsetOpt) [[unlikely]] { + throwRangeError(globalObject, scope, offsetOpt.error().message); + return { }; + } + + // Step 8: Let isoDateTime be GetISODateTimeFor(timeZone, epochNs). + auto [date, time] = TemporalCore::exactTimeToLocalDateAndTime(roundedExact, *offsetOpt); + + // Step 9: Let dateTimeString be ISODateTimeToString(isoDateTime, "iso8601", precision, ~never~). + StringBuilder sb; + sb.append(ISO8601::temporalDateTimeToString(date, time, precision.precision)); + + // Steps 10-11: offsetString = if showOffset is ~never~ then "" else FormatDateTimeUTCOffsetRounded(offsetNs). + if (showOffset != "never"_s) { + int64_t offsetNs = *offsetOpt; + int64_t offsetMinutes = offsetNs / 60'000'000'000; + int64_t remainder = offsetNs % 60'000'000'000; + if (remainder > 30'000'000'000 || (remainder == 30'000'000'000 && offsetNs > 0)) + offsetMinutes++; + else if (remainder < -30'000'000'000 || (remainder == -30'000'000'000 && offsetNs < 0)) + offsetMinutes--; + sb.append(ISO8601::formatTimeZoneOffsetString(offsetMinutes * 60'000'000'000)); + } + + // Steps 12-13: timeZoneString = if showTimeZone is ~never~ then "" else "[" + (critical ? "!" : "") + timeZone + "]". + if (showTimeZone != "never"_s) { + sb.append('['); + if (showTimeZone == "critical"_s) + sb.append('!'); + sb.append(timeZoneId()); + sb.append(']'); + } + + // Step 14: calendarString = FormatCalendarAnnotation(calendar, showCalendar). + bool appendCalendar = showCalendar == "always"_s || showCalendar == "critical"_s + || (showCalendar == "auto"_s && !TemporalCore::calendarIsISO(m_calendarID)); + if (appendCalendar) { + sb.append('['); + if (showCalendar == "critical"_s) + sb.append('!'); + sb.append("u-ca="_s); + sb.append(calendarId()); + sb.append(']'); + } + + // Step 15: Return string-concatenation(dateTimeString, offsetString, timeZoneString, calendarString). + return sb.toString(); +} + +String TemporalZonedDateTime::toString(JSGlobalObject* globalObject) const +{ + return toString(globalObject, { { Precision::Auto, 0 }, TemporalUnit::Nanosecond, 1 }, RoundingMode::Trunc, "auto"_s, "auto"_s, "auto"_s); +} + // Internal helper: extracts the runtime TimeZone handle from an already-parsed TimeZoneRecord. // Bracket annotation takes priority over Z, which takes priority over inline offset. // Returns nullopt if the record has no usable timezone info. diff --git a/Source/JavaScriptCore/runtime/TemporalZonedDateTime.h b/Source/JavaScriptCore/runtime/TemporalZonedDateTime.h index 917c37a07a3b5..d2ade2af545f0 100644 --- a/Source/JavaScriptCore/runtime/TemporalZonedDateTime.h +++ b/Source/JavaScriptCore/runtime/TemporalZonedDateTime.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include @@ -64,6 +65,12 @@ class TemporalZonedDateTime final : public JSNonFinalObject { std::optional getOffsetNanoseconds(JSGlobalObject*) const; + // https://tc39.es/proposal-temporal/#sec-temporal-temporalzoneddatetimetostring + // showOffset: ~auto~ | ~never~; showTimeZone: ~auto~ | ~never~ | ~critical~; showCalendar: ~auto~ | ~always~ | ~never~ | ~critical~. + String toString(JSGlobalObject*, const PrecisionData&, RoundingMode, StringView showOffset, StringView showTimeZone, StringView showCalendar) const; + // Every option ~auto~ (what toJSON and default-argument toString produce). + JS_EXPORT_PRIVATE String toString(JSGlobalObject*) const; + ISO8601::PlainDateTime getLocalDateTime(JSGlobalObject*) const; static std::optional getEpochNanosecondsFor(JSGlobalObject*, const TimeZone&, const ISO8601::PlainDate&, const ISO8601::PlainTime&, TemporalDisambiguation); diff --git a/Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp b/Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp index 13d1a1652cf06..001a03d99e093 100644 --- a/Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp +++ b/Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp @@ -906,79 +906,6 @@ JSC_DEFINE_HOST_FUNCTION(temporalZonedDateTimePrototypeFuncToPlainDateTime, (JSG RELEASE_AND_RETURN(scope, JSValue::encode(TemporalPlainDateTime::create(vm, globalObject->plainDateTimeStructure(), WTF::move(date), WTF::move(time), zdt->calendarID()))); } -// https://tc39.es/proposal-temporal/#sec-temporal-temporalzoneddatetimetostring -static String temporalZonedDateTimeToString(JSGlobalObject* globalObject, const TemporalZonedDateTime* zdt, - const PrecisionData& precision, RoundingMode roundingMode, - StringView showOffset, // ~auto~ | ~never~ - StringView showTimeZone, // ~auto~ | ~never~ | ~critical~ - StringView showCalendar, // ~auto~ | ~always~ | ~never~ | ~critical~ - CalendarID calendarID) -{ - VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - // Steps 1-3: Default increment/unit/roundingMode when not present. - // (Callers already pass concrete values, so these defaults are effectively no-ops here.) - // Step 4: Let epochNs be zonedDateTime.[[EpochNanoseconds]]. - // Step 5: Set epochNs to RoundTemporalInstant(epochNs, increment, unit, roundingMode). - Int128 epochNs = zdt->exactTime().epochNanoseconds(); - Int128 incrementNs = static_cast(lengthInNanoseconds(precision.unit)) * static_cast(static_cast(precision.increment)); - if (incrementNs > 0) - epochNs = TemporalCore::roundNumberToIncrementAsIfPositive(epochNs, incrementNs, roundingMode); - ISO8601::ExactTime roundedExact(epochNs); - - // Step 6: Let timeZone be zonedDateTime.[[TimeZone]]. (already in zdt->timeZone()) - // Step 7: Let offsetNanoseconds be GetOffsetNanosecondsFor(timeZone, epochNs). - auto offsetOpt = TemporalCore::getOffsetNanosecondsFor(zdt->timeZone(), roundedExact); - if (!offsetOpt) [[unlikely]] { - throwRangeError(globalObject, scope, offsetOpt.error().message); - return { }; - } - - // Step 8: Let isoDateTime be GetISODateTimeFor(timeZone, epochNs). - auto [date, time] = TemporalCore::exactTimeToLocalDateAndTime(roundedExact, *offsetOpt); - - // Step 9: Let dateTimeString be ISODateTimeToString(isoDateTime, "iso8601", precision, ~never~). - StringBuilder sb; - sb.append(ISO8601::temporalDateTimeToString(date, time, precision.precision)); - - // Steps 10-11: offsetString = if showOffset is ~never~ then "" else FormatDateTimeUTCOffsetRounded(offsetNs). - if (showOffset != "never"_s) { - int64_t offsetNs = *offsetOpt; - int64_t offsetMinutes = offsetNs / 60'000'000'000; - int64_t remainder = offsetNs % 60'000'000'000; - if (remainder > 30'000'000'000 || (remainder == 30'000'000'000 && offsetNs > 0)) - offsetMinutes++; - else if (remainder < -30'000'000'000 || (remainder == -30'000'000'000 && offsetNs < 0)) - offsetMinutes--; - sb.append(ISO8601::formatTimeZoneOffsetString(offsetMinutes * 60'000'000'000)); - } - - // Steps 12-13: timeZoneString = if showTimeZone is ~never~ then "" else "[" + (critical ? "!" : "") + timeZone + "]". - if (showTimeZone != "never"_s) { - sb.append('['); - if (showTimeZone == "critical"_s) - sb.append('!'); - sb.append(zdt->timeZoneId()); - sb.append(']'); - } - - // Step 14: calendarString = FormatCalendarAnnotation(calendar, showCalendar). - bool appendCalendar = showCalendar == "always"_s || showCalendar == "critical"_s - || (showCalendar == "auto"_s && !TemporalCore::calendarIsISO(calendarID)); - if (appendCalendar) { - sb.append('['); - if (showCalendar == "critical"_s) - sb.append('!'); - sb.append("u-ca="_s); - sb.append(zdt->calendarId()); - sb.append(']'); - } - - // Step 15: Return string-concatenation(dateTimeString, offsetString, timeZoneString, calendarString). - return sb.toString(); -} - // temporal_rs: ZonedDateTime::to_ixdtf_string_with_provider (default precision) // https://tc39.es/proposal-temporal/#sec-temporal.zoneddatetime.prototype.tojson // toJSON always uses default format (auto precision, full string), ignoring any argument. @@ -993,8 +920,7 @@ JSC_DEFINE_HOST_FUNCTION(temporalZonedDateTimePrototypeFuncToJSON, (JSGlobalObje return throwVMTypeError(globalObject, scope, "Temporal.ZonedDateTime.prototype.toJSON called on value that's not a ZonedDateTime"_s); // Step 3: Return TemporalZonedDateTimeToString(zonedDateTime, ~auto~, ~auto~, ~auto~, ~auto~). - PrecisionData precision { { Precision::Auto, 0 }, TemporalUnit::Nanosecond, 1 }; - String result = temporalZonedDateTimeToString(globalObject, zdt, precision, RoundingMode::Trunc, /* showOffset */ "auto"_s, /* showTimeZone */ "auto"_s, /* showCalendar */ "auto"_s, zdt->calendarID()); + String result = zdt->toString(globalObject); RETURN_IF_EXCEPTION(scope, { }); return JSValue::encode(jsString(vm, WTF::move(result))); } @@ -1062,8 +988,7 @@ JSC_DEFINE_HOST_FUNCTION(temporalZonedDateTimePrototypeFuncToString, (JSGlobalOb auto precision = toSecondsStringPrecisionRecord(smallestUnit, digits); // Step 14: Return TemporalZonedDateTimeToString(zonedDateTime, precision, showCalendar, showTimeZone, showOffset, ...). - String result = temporalZonedDateTimeToString(globalObject, zdt, precision, roundingMode, - offsetOpt, tzNameOpt, calendarOpt, zdt->calendarID()); + String result = zdt->toString(globalObject, precision, roundingMode, offsetOpt, tzNameOpt, calendarOpt); RETURN_IF_EXCEPTION(scope, { }); return JSValue::encode(jsString(vm, WTF::move(result))); }