Implement Temporal.Instant.{ toLocalString, toJSON, round }

Signed-off-by: Seonghyun Kim <sh8281.kim@samsung.com>
This commit is contained in:
Seonghyun Kim 2025-09-03 19:10:13 +09:00 committed by MuHong Byun
commit 5711241b99
8 changed files with 146 additions and 69 deletions

View file

@ -177,12 +177,30 @@ static Value builtinTemporalInstantEquals(ExecutionState& state, Value thisValue
return Value(instant->epochNanoseconds() == other->epochNanoseconds());
}
static Value builtinTemporalInstantToJSON(ExecutionState& state, Value thisValue, size_t argc, Value* argv, Optional<Object*> newTarget)
{
RESOLVE_THIS_BINDING_TO_INSTANT2(instant, toJSON);
return instant->toString(state, Value());
}
static Value builtinTemporalInstantToLocaleString(ExecutionState& state, Value thisValue, size_t argc, Value* argv, Optional<Object*> newTarget)
{
RESOLVE_THIS_BINDING_TO_INSTANT2(instant, toLocaleString);
return instant->toString(state, Value());
}
static Value builtinTemporalInstantToString(ExecutionState& state, Value thisValue, size_t argc, Value* argv, Optional<Object*> newTarget)
{
RESOLVE_THIS_BINDING_TO_INSTANT2(instant, toString);
return instant->toString(state, argc ? argv[0] : Value());
}
static Value builtinTemporalInstantRound(ExecutionState& state, Value thisValue, size_t argc, Value* argv, Optional<Object*> newTarget)
{
RESOLVE_THIS_BINDING_TO_INSTANT2(instant, round);
return instant->round(state, argv[0]);
}
#define RESOLVE_THIS_BINDING_TO_PLAINDATE(NAME, BUILT_IN_METHOD) \
if (!thisValue.isObject() || !thisValue.asObject()->isTemporalPlainDateObject()) { \
ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, state.context()->staticStrings().lazyCapitalPlainDate().string(), true, state.context()->staticStrings().lazy##BUILT_IN_METHOD().string(), ErrorObject::Messages::GlobalObject_CalledOnIncompatibleReceiver); \
@ -424,6 +442,9 @@ void GlobalObject::installTemporal(ExecutionState& state)
m_temporalInstantPrototype->directDefineOwnProperty(state, ObjectPropertyName(strings->toString), ObjectPropertyDescriptor(new NativeFunctionObject(state, NativeFunctionInfo(strings->toString, builtinTemporalInstantToString, 0, NativeFunctionInfo::Strict)), (ObjectPropertyDescriptor::PresentAttribute)(ObjectPropertyDescriptor::WritablePresent | ObjectPropertyDescriptor::ConfigurablePresent)));
m_temporalInstantPrototype->directDefineOwnProperty(state, ObjectPropertyName(strings->valueOf), ObjectPropertyDescriptor(new NativeFunctionObject(state, NativeFunctionInfo(strings->valueOf, builtinTemporalInstantValueOf, 0, NativeFunctionInfo::Strict)), (ObjectPropertyDescriptor::PresentAttribute)(ObjectPropertyDescriptor::WritablePresent | ObjectPropertyDescriptor::ConfigurablePresent)));
m_temporalInstantPrototype->directDefineOwnProperty(state, ObjectPropertyName(strings->lazyEquals()), ObjectPropertyDescriptor(new NativeFunctionObject(state, NativeFunctionInfo(strings->lazyEquals(), builtinTemporalInstantEquals, 1, NativeFunctionInfo::Strict)), (ObjectPropertyDescriptor::PresentAttribute)(ObjectPropertyDescriptor::WritablePresent | ObjectPropertyDescriptor::ConfigurablePresent)));
m_temporalInstantPrototype->directDefineOwnProperty(state, ObjectPropertyName(strings->toJSON), ObjectPropertyDescriptor(new NativeFunctionObject(state, NativeFunctionInfo(strings->toJSON, builtinTemporalInstantToJSON, 0, NativeFunctionInfo::Strict)), (ObjectPropertyDescriptor::PresentAttribute)(ObjectPropertyDescriptor::WritablePresent | ObjectPropertyDescriptor::ConfigurablePresent)));
m_temporalInstantPrototype->directDefineOwnProperty(state, ObjectPropertyName(strings->toLocaleString), ObjectPropertyDescriptor(new NativeFunctionObject(state, NativeFunctionInfo(strings->toLocaleString, builtinTemporalInstantToLocaleString, 0, NativeFunctionInfo::Strict)), (ObjectPropertyDescriptor::PresentAttribute)(ObjectPropertyDescriptor::WritablePresent | ObjectPropertyDescriptor::ConfigurablePresent)));
m_temporalInstantPrototype->directDefineOwnProperty(state, ObjectPropertyName(strings->round), ObjectPropertyDescriptor(new NativeFunctionObject(state, NativeFunctionInfo(strings->round, builtinTemporalInstantRound, 1, NativeFunctionInfo::Strict)), (ObjectPropertyDescriptor::PresentAttribute)(ObjectPropertyDescriptor::WritablePresent | ObjectPropertyDescriptor::ConfigurablePresent)));
{
auto getter = new NativeFunctionObject(state, NativeFunctionInfo(strings->lazyGetEpochMilliseconds(), builtinTemporalInstantGetEpochMilliseconds, 0, NativeFunctionInfo::Strict));

View file

@ -38,6 +38,32 @@ Value TemporalInstantObject::epochMilliseconds() const
return Value(static_cast<int64_t>(s));
}
static Int128 resolveNanosecondsValueByUnit(String* unit)
{
Int128 maximum = 0;
constexpr int64_t hoursPerDay = 24;
constexpr int64_t minutesPerHour = 60;
constexpr int64_t secondsPerMinute = 60;
constexpr int64_t msPerDay = hoursPerDay * minutesPerHour * secondsPerMinute * 1000;
if (unit->equals("hour")) {
maximum = static_cast<Int128>(hoursPerDay);
} else if (unit->equals("minute")) {
maximum = static_cast<Int128>(minutesPerHour * hoursPerDay);
} else if (unit->equals("second")) {
maximum = static_cast<Int128>(secondsPerMinute * minutesPerHour * hoursPerDay);
} else if (unit->equals("millisecond")) {
maximum = static_cast<Int128>(msPerDay);
} else if (unit->equals("microsecond")) {
maximum = static_cast<Int128>(msPerDay * 1000);
} else if (unit->equals("nanosecond")) {
maximum = ISO8601::ExactTime::nsPerDay;
}
return maximum;
}
String* TemporalInstantObject::toString(ExecutionState& state, Value options)
{
const char* msg = "Invalid options value";
@ -77,26 +103,7 @@ String* TemporalInstantObject::toString(ExecutionState& state, Value options)
auto precision = Temporal::toSecondsStringPrecisionRecord(state, smallestUnit, digits);
// Let roundedNs be RoundTemporalInstant(instant.[[EpochNanoseconds]], precision.[[Increment]], precision.[[Unit]], roundingMode).
// Let roundedInstant be ! CreateTemporalInstant(roundedNs).
Int128 maximum = 0;
constexpr int64_t hoursPerDay = 24;
constexpr int64_t minutesPerHour = 60;
constexpr int64_t secondsPerMinute = 60;
constexpr int64_t msPerDay = hoursPerDay * minutesPerHour * secondsPerMinute * 1000;
String* unit = precision.unit;
if (unit->equals("hour")) {
maximum = static_cast<Int128>(hoursPerDay);
} else if (unit->equals("minute")) {
maximum = static_cast<Int128>(minutesPerHour * hoursPerDay);
} else if (unit->equals("second")) {
maximum = static_cast<Int128>(secondsPerMinute * minutesPerHour * hoursPerDay);
} else if (unit->equals("millisecond")) {
maximum = static_cast<Int128>(msPerDay);
} else if (unit->equals("microsecond")) {
maximum = static_cast<Int128>(msPerDay * 1000);
} else if (unit->equals("nanosecond")) {
maximum = ISO8601::ExactTime::nsPerDay;
}
Int128 maximum = resolveNanosecondsValueByUnit(precision.unit);
Temporal::validateTemporalRoundingIncrement(state, precision.increment, maximum, true);
@ -237,6 +244,62 @@ String* TemporalInstantObject::toString(ExecutionState& state, Int128 epochNanos
return builder.finalize();
}
TemporalInstantObject* TemporalInstantObject::round(ExecutionState& state, Value roundTo)
{
// Let instant be the this value.
// Perform ? RequireInternalSlot(instant, [[InitializedTemporalInstant]]).
const auto* msg = "Temporal.Instant.round needs object or string parameter";
// If roundTo is undefined, then
if (roundTo.isUndefined()) {
// Throw a TypeError exception.
ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, msg);
} else if (roundTo.isString()) {
// If roundTo is a String, then
// Let paramString be roundTo.
auto paramString = roundTo.asString();
// Set roundTo to OrdinaryObjectCreate(null).
roundTo = new Object(state, Object::PrototypeIsNull);
// Perform ! CreateDataPropertyOrThrow(roundTo, "smallestUnit", paramString).
roundTo.asObject()->directDefineOwnProperty(state, state.context()->staticStrings().lazySmallestUnit(),
ObjectPropertyDescriptor(paramString, ObjectPropertyDescriptor::AllPresent));
} else {
// Else,
// Set roundTo to ? GetOptionsObject(roundTo).
if (!roundTo.isObject()) {
ErrorObject::throwBuiltinError(state, ErrorCode::TypeError, msg);
}
}
// NOTE: The following steps read options and perform independent validation in alphabetical order (GetRoundingIncrementOption reads "roundingIncrement" and GetRoundingModeOption reads "roundingMode").
// Let roundingIncrement be ? GetRoundingIncrementOption(roundTo).
auto roundingIncrement = Temporal::getRoundingIncrementOption(state, roundTo.asObject());
// Let roundingMode be ? GetRoundingModeOption(roundTo, half-expand).
auto roundingMode = Temporal::getRoundingModeOption(state, roundTo.asObject(), state.context()->staticStrings().lazyHalfExpand().string());
// Let smallestUnit be ? GetTemporalUnitValuedOption(roundTo, "smallestUnit", required).
auto smallestUnit = Temporal::getTemporalUnitValuedOption(state, roundTo.asObject(), state.context()->staticStrings().lazySmallestUnit().string(), Value(Value::EmptyValue)).value();
// Perform ? ValidateTemporalUnitValue(smallestUnit, time).
Temporal::validateTemporalUnitValue(state, smallestUnit, ISO8601::DateTimeUnitCategory::Time, nullptr, 0);
// If smallestUnit is hour, then
// Let maximum be HoursPerDay.
// Else if smallestUnit is minute, then
// Let maximum be MinutesPerHour × HoursPerDay.
// Else if smallestUnit is second, then
// Let maximum be SecondsPerMinute × MinutesPerHour × HoursPerDay.
// Else if smallestUnit is millisecond, then
// Let maximum be (msPerDay).
// Let maximum be 10**3 × (msPerDay).
// Else,
// Assert: smallestUnit is nanosecond.
// Let maximum be nsPerDay.
Int128 maximum = resolveNanosecondsValueByUnit(smallestUnit);
// Perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, true).
Temporal::validateTemporalRoundingIncrement(state, roundingIncrement, maximum, true);
// Let roundedNs be RoundTemporalInstant(instant.[[EpochNanoseconds]], roundingIncrement, smallestUnit, roundingMode).
// Return ! CreateTemporalInstant(roundedNs).
auto roundedInstant = ISO8601::ExactTime(*m_nanoseconds).round(state, roundingIncrement, smallestUnit, roundingMode);
return new TemporalInstantObject(state, state.context()->globalObject()->temporalInstantPrototype(), roundedInstant.epochNanoseconds());
}
TemporalInstantObject* TemporalInstantObject::addDurationToInstant(AddDurationOperation operation, const Value& temporalDurationLike)
{
TemporalInstantObject* instant = this;

View file

@ -42,6 +42,7 @@ public:
String* toString(ExecutionState& state, Value options);
static String* toString(ExecutionState& state, Int128 epochNanoseconds, TimeZone timeZone, Value precision);
TemporalInstantObject* round(ExecutionState& state, Value roundTo);
private:
// https://tc39.es/proposal-temporal/#sec-temporal-adddurationtoinstant

View file

@ -594,7 +594,13 @@ Optional<String*> Temporal::getTemporalUnitValuedOption(ExecutionState& state, O
Value value;
if (resolvedOptions) {
value = Intl::getOption(state, resolvedOptions.value(), key, Intl::StringValue, allowedStrings, sizeof(allowedStrings) / sizeof(Value), defaultValue.value());
if (value.isEmpty()) {
ErrorObject::throwBuiltinError(state, ErrorCode::RangeError, "Required options value is missing");
}
} else {
if (defaultValue && defaultValue.value().isEmpty()) {
ErrorObject::throwBuiltinError(state, ErrorCode::RangeError, "Required options value is missing");
}
value = defaultValue.value();
}
// If value is undefined, return unset.
@ -805,6 +811,24 @@ void Temporal::validateTemporalRoundingIncrement(ExecutionState& state, unsigned
// Return unused.
}
int32_t Temporal::getRoundingIncrementOption(ExecutionState& state, Object* options)
{
// Let value be ? Get(options, "roundingIncrement").
Value value = options->get(state, ObjectPropertyName(state.context()->staticStrings().lazyRoundingIncrement())).value(state, options);
// If value is undefined, return 1𝔽.
if (value.isUndefined()) {
return 1;
}
// Let integerIncrement be ? ToIntegerWithTruncation(value).
auto integerIncrement = value.toIntegerWithTruncation(state);
// If integerIncrement < 1 or integerIncrement > 10**9, throw a RangeError exception.
if (integerIncrement < 1 || integerIncrement > 1000000000) {
ErrorObject::throwBuiltinError(state, ErrorCode::RangeError, "Invalid roundingIncrement value");
}
// Return integerIncrement.
return integerIncrement;
}
TemporalObject::TemporalObject(ExecutionState& state)
: TemporalObject(state, state.context()->globalObject()->objectPrototype())
{

View file

@ -306,7 +306,7 @@ public:
static String* getRoundingModeOption(ExecutionState& state, Optional<Object*> resolvedOptions, String* fallback);
// https://tc39.es/proposal-temporal/#sec-temporal-gettemporalunitvaluedoption
static Optional<String*> getTemporalUnitValuedOption(ExecutionState& state, Optional<Object*> resolvedOptions, String* key, Optional<Value> defaultValue);
static Optional<String*> getTemporalUnitValuedOption(ExecutionState& state, Optional<Object*> resolvedOptions, String* key, Optional<Value> defaultValue /* give DefaultValue to EmptyValue means Required = true*/);
// https://tc39.es/proposal-temporal/#sec-temporal-validatetemporalunitvaluedoption
static void validateTemporalUnitValue(ExecutionState& state, Optional<String*> value, ISO8601::DateTimeUnitCategory unitGroup, Optional<String*> extraValues, size_t extraValueSize);
@ -324,6 +324,9 @@ public:
// https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement
static void validateTemporalRoundingIncrement(ExecutionState& state, unsigned increment, Int128 dividend, bool inclusive);
// https://tc39.es/proposal-temporal/#sec-temporal-getroundingincrementoption
static int32_t getRoundingIncrementOption(ExecutionState& state, Object* options);
};
class TemporalObject : public DerivedObject {

View file

@ -874,6 +874,18 @@ double Value::toIntegerIfIntergral(ExecutionState& state) const
return number;
}
int64_t Value::toIntegerWithTruncation(ExecutionState& state) const
{
// Let number be ? ToNumber(argument).
double number = toNumber(state);
// If number is NaN, +∞𝔽 or -∞𝔽, throw a RangeError exception.
if (std::isnan(number) || std::isinf(number)) {
ErrorObject::throwBuiltinError(state, ErrorCode::RangeError, "Failed to execute ToIntegerWithTruncation");
}
// Return truncate((number)).
return std::trunc(number);
}
#if defined(ESCARGOT_ENABLE_TEST)
bool Value::checkIfObjectWithIsHTMLDDA() const
{

View file

@ -240,6 +240,7 @@ public:
toNumeric(ExecutionState& ec) const; // https://www.ecma-international.org/ecma-262/#sec-tonumeric
double toInteger(ExecutionState& ec) const; // $7.1.4 ToInteger
double toIntegerIfIntergral(ExecutionState& state) const; // https://tc39.es/ecma402/#sec-tointegerifintegral
int64_t toIntegerWithTruncation(ExecutionState& state) const; // https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation
bool isInteger(ExecutionState& ec) const; // $7.1.4 ToInteger
uint64_t toLength(ExecutionState& ec) const;
int32_t toInt32(ExecutionState& ec) const; // $7.1.5 ToInt32

View file

@ -565,43 +565,6 @@
<test id="built-ins/Temporal/Instant/prototype/add/subclassing-ignored"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/epochMilliseconds/basic"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/equals/argument-zoneddatetime"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/accepts-plural-units"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/accepts-string-parameter-for-smallestunit"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/allow-increments-that-divide-evenly-into-solar-days"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/branding"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/builtin"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/length"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/name"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/not-a-constructor"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/prop-desc"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/round-to-days"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/rounding-direction"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/rounding-increments"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingincrement-nan"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingincrement-non-integer"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingincrement-out-of-range"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingincrement-undefined"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingincrement-wrong-type"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-ceil"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-expand"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-floor"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-halfCeil"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-halfEven"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-halfExpand"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-halfFloor"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-halfTrunc"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-invalid-string"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-trunc"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-undefined"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundingmode-wrong-type"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/roundto-invalid-string"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/smallestunit-invalid-string"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/smallestunit-plurals-accepted"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/smallestunit-string-shorthand"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/smallestunit-wrong-type"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/subclassing-ignored"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/throws-on-increments-that-do-not-divide-evenly"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/round/throws-without-smallest-unit"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/since/add-subtract"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/since/argument-object-tostring"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/since/argument-string-calendar-annotation"><reason>TODO</reason></test>
@ -689,17 +652,6 @@
<test id="built-ins/Temporal/Instant/prototype/subtract/prop-desc"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/subtract/result-out-of-range"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/subtract/subclassing-ignored"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/basic"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/branding"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/builtin"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/length"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/name"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/negative-epochnanoseconds"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/not-a-constructor"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/prop-desc"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toJSON/year-format"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toLocaleString/branding"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toLocaleString/prop-desc"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toString/smallestunit-plurals-accepted"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toZonedDateTimeISO/branding"><reason>TODO</reason></test>
<test id="built-ins/Temporal/Instant/prototype/toZonedDateTimeISO/builtin"><reason>TODO</reason></test>