Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions Source/JavaScriptCore/runtime/TemporalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<TemporalInstant>())
return TemporalType::Instant;
if (cell->inherits<TemporalPlainDateTime>())
return TemporalType::PlainDateTime;
if (cell->inherits<TemporalPlainDate>())
return TemporalType::PlainDate;
if (cell->inherits<TemporalPlainTime>())
return TemporalType::PlainTime;
if (cell->inherits<TemporalZonedDateTime>())
return TemporalType::ZonedDateTime;
if (cell->inherits<TemporalPlainYearMonth>())
return TemporalType::PlainYearMonth;
if (cell->inherits<TemporalPlainMonthDay>())
return TemporalType::PlainMonthDay;
if (cell->inherits<TemporalDuration>())
return TemporalType::Duration;
return TemporalType::None;
}

} // namespace JSC

WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
16 changes: 16 additions & 0 deletions Source/JavaScriptCore/runtime/TemporalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,20 @@ void throwTemporalError(JSGlobalObject*, ThrowScope&, const TemporalError&);

std::optional<TimeZone> 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
75 changes: 75 additions & 0 deletions Source/JavaScriptCore/runtime/TemporalZonedDateTime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -42,6 +43,7 @@

#include <wtf/DateMath.h>
#include <wtf/text/MakeString.h>
#include <wtf/text/StringBuilder.h>

namespace JSC {

Expand Down Expand Up @@ -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<Int128>(lengthInNanoseconds(precision.unit)) * static_cast<Int128>(static_cast<int64_t>(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.
Expand Down
7 changes: 7 additions & 0 deletions Source/JavaScriptCore/runtime/TemporalZonedDateTime.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include <JavaScriptCore/JSCTimeZone.h>
#include <JavaScriptCore/JSObject.h>
#include <JavaScriptCore/TemporalEnums.h>
#include <JavaScriptCore/TemporalObject.h>
#include <optional>
#include <wtf/Packed.h>

Expand Down Expand Up @@ -64,6 +65,12 @@ class TemporalZonedDateTime final : public JSNonFinalObject {

std::optional<int64_t> 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<ISO8601::ExactTime> getEpochNanosecondsFor(JSGlobalObject*, const TimeZone&, const ISO8601::PlainDate&, const ISO8601::PlainTime&, TemporalDisambiguation);
Expand Down
79 changes: 2 additions & 77 deletions Source/JavaScriptCore/runtime/TemporalZonedDateTimePrototype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int128>(lengthInNanoseconds(precision.unit)) * static_cast<Int128>(static_cast<int64_t>(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.
Expand All @@ -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)));
}
Expand Down Expand Up @@ -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)));
}
Expand Down
Loading