This commit is contained in:
Tim Flynn 2024-11-21 14:14:15 +00:00 committed by GitHub
commit a9153713ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 4172 additions and 344 deletions

View file

@ -205,12 +205,21 @@ set(SOURCES
Runtime/SymbolObject.cpp
Runtime/SymbolPrototype.cpp
Runtime/Temporal/AbstractOperations.cpp
Runtime/Temporal/Calendar.cpp
Runtime/Temporal/DateEquations.cpp
Runtime/Temporal/Duration.cpp
Runtime/Temporal/DurationConstructor.cpp
Runtime/Temporal/DurationPrototype.cpp
Runtime/Temporal/Instant.cpp
Runtime/Temporal/ISO8601.cpp
Runtime/Temporal/PlainDate.cpp
Runtime/Temporal/PlainDateTime.cpp
Runtime/Temporal/PlainMonthDay.cpp
Runtime/Temporal/PlainMonthDayConstructor.cpp
Runtime/Temporal/PlainMonthDayPrototype.cpp
Runtime/Temporal/PlainTime.cpp
Runtime/Temporal/Temporal.cpp
Runtime/Temporal/TimeZone.cpp
Runtime/TypedArray.cpp
Runtime/TypedArrayConstructor.cpp
Runtime/TypedArrayPrototype.cpp

View file

@ -88,7 +88,8 @@
__JS_ENUMERATE(Segmenter, segmenter, SegmenterPrototype, SegmenterConstructor)
#define JS_ENUMERATE_TEMPORAL_OBJECTS \
__JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor)
__JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor) \
__JS_ENUMERATE(PlainMonthDay, plain_month_day, PlainMonthDayPrototype, PlainMonthDayConstructor)
#define JS_ENUMERATE_BUILTIN_NAMESPACE_OBJECTS \
__JS_ENUMERATE(AtomicsObject, atomics) \
@ -277,6 +278,18 @@ JS_ENUMERATE_TEMPORAL_OBJECTS
#undef __JS_ENUMERATE
class Temporal;
struct CalendarDate;
struct CalendarFields;
struct DateDuration;
struct InternalDuration;
struct ISODate;
struct ISODateTime;
struct ParseResult;
struct PartialDuration;
struct Time;
struct TimeZone;
struct TimeZoneOffset;
};
template<typename T>

View file

@ -48,6 +48,7 @@
#include <LibJS/Runtime/StringObject.h>
#include <LibJS/Runtime/StringPrototype.h>
#include <LibJS/Runtime/Temporal/Duration.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/WeakMap.h>
@ -835,6 +836,15 @@ ErrorOr<void> print_temporal_duration(JS::PrintContext& print_context, JS::Tempo
return {};
}
ErrorOr<void> print_temporal_plain_month_day(JS::PrintContext& print_context, JS::Temporal::PlainMonthDay const& plain_month_day, HashTable<JS::Object*>& seen_objects)
{
TRY(print_type(print_context, "Temporal.PlainMonthDay"sv));
TRY(js_out(print_context, " \033[34;1m{:02}-{:02}\033[0m", plain_month_day.iso_date().month, plain_month_day.iso_date().day));
TRY(js_out(print_context, "\n calendar: "));
TRY(print_value(print_context, JS::PrimitiveString::create(plain_month_day.vm(), plain_month_day.calendar()), seen_objects));
return {};
}
ErrorOr<void> print_boolean_object(JS::PrintContext& print_context, JS::BooleanObject const& boolean_object, HashTable<JS::Object*>& seen_objects)
{
TRY(print_type(print_context, "Boolean"sv));
@ -952,6 +962,8 @@ ErrorOr<void> print_value(JS::PrintContext& print_context, JS::Value value, Hash
return print_intl_duration_format(print_context, static_cast<JS::Intl::DurationFormat&>(object), seen_objects);
if (is<JS::Temporal::Duration>(object))
return print_temporal_duration(print_context, static_cast<JS::Temporal::Duration&>(object), seen_objects);
if (is<JS::Temporal::PlainMonthDay>(object))
return print_temporal_plain_month_day(print_context, static_cast<JS::Temporal::PlainMonthDay&>(object), seen_objects);
return print_object(print_context, object, seen_objects);
}

View file

@ -1,20 +1,19 @@
/*
* Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/CharacterTypes.h>
#include <AK/GenericLexer.h>
#include <AK/NumericLimits.h>
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <AK/Time.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Date.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <time.h>
namespace JS {
@ -457,7 +456,7 @@ String system_time_zone_identifier()
// time zone identifier or an offset time zone identifier.
auto system_time_zone_string = Unicode::current_time_zone();
if (!is_time_zone_offset_string(system_time_zone_string)) {
if (!is_offset_time_zone_identifier(system_time_zone_string)) {
auto time_zone_identifier = Intl::get_available_named_time_zone_identifier(system_time_zone_string);
if (!time_zone_identifier.has_value())
return "UTC"_string;
@ -476,46 +475,55 @@ void clear_system_time_zone_cache()
}
// 21.4.1.25 LocalTime ( t ), https://tc39.es/ecma262/#sec-localtime
// 14.5.6 LocalTime ( t ), https://tc39.es/proposal-temporal/#sec-localtime
double local_time(double time)
{
// 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier().
auto system_time_zone_identifier = JS::system_time_zone_identifier();
// 2. Let parseResult be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier).
auto parse_result = Temporal::parse_time_zone_identifier(system_time_zone_identifier);
double offset_nanoseconds { 0 };
// 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then
if (is_time_zone_offset_string(system_time_zone_identifier)) {
// a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier).
offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier);
// 3. If parseResult.[[OffsetMinutes]] is not EMPTY, then
if (parse_result.offset_minutes.has_value()) {
// a. Let offsetNs be parseResult.[[OffsetMinutes]] × (60 × 10**9).
offset_nanoseconds = static_cast<double>(*parse_result.offset_minutes) * 60'000'000'000;
}
// 3. Else,
// 4. Else,
else {
// a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ((t) × 10^6)).
auto offset = get_named_time_zone_offset_milliseconds(system_time_zone_identifier, time);
offset_nanoseconds = static_cast<double>(offset.offset.to_nanoseconds());
}
// 4. Let offsetMs be truncate(offsetNs / 10^6).
// 5. Let offsetMs be truncate(offsetNs / 10^6).
auto offset_milliseconds = trunc(offset_nanoseconds / 1e6);
// 5. Return t + 𝔽(offsetMs).
// 6. Return t + 𝔽(offsetMs).
return time + offset_milliseconds;
}
// 21.4.1.26 UTC ( t ), https://tc39.es/ecma262/#sec-utc-t
// 14.5.7 UTC ( t ), https://tc39.es/proposal-temporal/#sec-localtime
// FIXME: Update the rest of this AO for Temporal once we have the required Temporal objects.
double utc_time(double time)
{
// 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier().
auto system_time_zone_identifier = JS::system_time_zone_identifier();
// 2. Let parseResult be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier).
auto parse_result = Temporal::parse_time_zone_identifier(system_time_zone_identifier);
double offset_nanoseconds { 0 };
// 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then
if (is_time_zone_offset_string(system_time_zone_identifier)) {
// a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier).
offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier);
// 3. If parseResult.[[OffsetMinutes]] is not EMPTY, then
if (parse_result.offset_minutes.has_value()) {
// a. Let offsetNs be parseResult.[[OffsetMinutes]] × (60 × 10**9).
offset_nanoseconds = static_cast<double>(*parse_result.offset_minutes) * 60'000'000'000;
}
// 3. Else,
// 4. Else,
else {
// a. Let possibleInstants be GetNamedTimeZoneEpochNanoseconds(systemTimeZoneIdentifier, (YearFromTime(t)), (MonthFromTime(t)) + 1, (DateFromTime(t)), (HourFromTime(t)), (MinFromTime(t)), (SecFromTime(t)), (msFromTime(t)), 0, 0).
auto possible_instants = get_named_time_zone_epoch_nanoseconds(system_time_zone_identifier, year_from_time(time), month_from_time(time) + 1, date_from_time(time), hour_from_time(time), min_from_time(time), sec_from_time(time), ms_from_time(time), 0, 0);
@ -544,10 +552,10 @@ double utc_time(double time)
offset_nanoseconds = static_cast<double>(offset.offset.to_nanoseconds());
}
// 4. Let offsetMs be truncate(offsetNs / 10^6).
// 5. Let offsetMs be truncate(offsetNs / 10^6).
auto offset_milliseconds = trunc(offset_nanoseconds / 1e6);
// 5. Return t - 𝔽(offsetMs).
// 6. Return t - 𝔽(offsetMs).
return time - offset_milliseconds;
}
@ -636,171 +644,12 @@ double time_clip(double time)
return to_integer_or_infinity(time);
}
// 21.4.1.33 Time Zone Offset String Format, https://tc39.es/ecma262/#sec-time-zone-offset-strings
Optional<UTCOffset> parse_utc_offset(StringView offset_string)
{
GenericLexer lexer { offset_string };
UTCOffset parse_result;
// https://tc39.es/ecma262/#prod-ASCIISign
auto parse_ascii_sign = [&]() {
// ASCIISign ::: one of
// + -
if (lexer.next_is(is_any_of("+-"sv))) {
parse_result.sign = lexer.consume();
return true;
}
return false;
};
auto parse_two_digits = [&](size_t max_value) -> Optional<u8> {
if (auto digits = lexer.peek_string(2); digits.has_value()) {
auto number = digits->to_number<u8>(TrimWhitespace::No);
if (number.has_value() && *number <= max_value) {
lexer.ignore(2);
return *number;
}
}
return {};
};
// https://tc39.es/ecma262/#prod-Hour
auto parse_hour = [&]() {
// Hour :::
// 0 DecimalDigit
// 1 DecimalDigit
// 20
// 21
// 22
// 23
parse_result.hour = parse_two_digits(23);
return parse_result.hour.has_value();
};
// https://tc39.es/ecma262/#prod-TimeSeparator
auto parse_time_separator = [&](auto extended) {
// TimeSeparator[Extended] :::
// [+Extended] :
// [~Extended] [empty]
if (extended)
return lexer.consume_specific(':');
return true;
};
// https://tc39.es/ecma262/#prod-MinuteSecond
auto parse_minute_second = [&](auto& result) {
// MinuteSecond :::
// 0 DecimalDigit
// 1 DecimalDigit
// 2 DecimalDigit
// 3 DecimalDigit
// 4 DecimalDigit
// 5 DecimalDigit
result = parse_two_digits(59);
return result.has_value();
};
// https://tc39.es/ecma262/#prod-TemporalDecimalSeparator
auto parse_temporal_decimal_separator = [&]() {
// TemporalDecimalSeparator ::: one of
// . ,
return lexer.consume_specific('.') || lexer.consume_specific(',');
};
// https://tc39.es/ecma262/#prod-TemporalDecimalFraction
auto parse_temporal_decimal_fraction = [&]() {
// TemporalDecimalFraction :::
// TemporalDecimalSeparator DecimalDigit
// TemporalDecimalSeparator DecimalDigit DecimalDigit
// TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit
// TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit
// TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit
// TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit
// TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit
// TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit
// TemporalDecimalSeparator DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit
auto position = lexer.tell();
if (!parse_temporal_decimal_separator())
return false;
for (size_t i = 0; i < 9; ++i) {
if (!lexer.next_is(is_ascii_digit))
break;
lexer.ignore();
}
if (auto fraction = lexer.input().substring_view(position, lexer.tell() - position); fraction.length() > 1) {
parse_result.fraction = fraction;
return true;
}
return false;
};
// https://tc39.es/ecma262/#prod-HourSubcomponents
auto parse_hour_subcomponents = [&](auto extended) {
// HourSubcomponents[Extended] :::
// TimeSeparator[?Extended] MinuteSecond
// TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond TemporalDecimalFraction[opt]
ArmedScopeGuard guard { [&, position = lexer.tell()]() { lexer.retreat(lexer.tell() - position); } };
if (!parse_time_separator(extended))
return false;
if (!parse_minute_second(parse_result.minute))
return false;
if (lexer.is_eof()) {
guard.disarm();
return true;
}
if (!parse_time_separator(extended))
return false;
if (!parse_minute_second(parse_result.second))
return false;
if (lexer.is_eof()) {
guard.disarm();
return true;
}
if (!parse_temporal_decimal_fraction())
return false;
guard.disarm();
return true;
};
// https://tc39.es/ecma262/#prod-UTCOffset
// UTCOffset :::
// ASCIISign Hour
// ASCIISign Hour HourSubcomponents[+Extended]
// ASCIISign Hour HourSubcomponents[~Extended]
if (!parse_ascii_sign())
return {};
if (!parse_hour())
return {};
if (lexer.is_eof())
return parse_result;
if (!parse_hour_subcomponents(true) && !parse_hour_subcomponents(false))
return {};
if (lexer.is_eof())
return parse_result;
return {};
}
// 21.4.1.33.1 IsTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-istimezoneoffsetstring
bool is_time_zone_offset_string(StringView offset_string)
// 14.5.10 IsOffsetTimeZoneIdentifier ( offsetString ), https://tc39.es/proposal-temporal/#sec-isoffsettimezoneidentifier
bool is_offset_time_zone_identifier(StringView offset_string)
{
// 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset).
auto parse_result = parse_utc_offset(offset_string);
// 1. Let parseResult be ParseText(StringToCodePoints(offsetString), UTCOffset[~SubMinutePrecision]).
auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::No);
// 2. If parseResult is a List of errors, return false.
// 3. Return true.
@ -808,66 +657,83 @@ bool is_time_zone_offset_string(StringView offset_string)
}
// 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring
double parse_time_zone_offset_string(StringView offset_string)
// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset
ThrowCompletionOr<double> parse_date_time_utc_offset(VM& vm, StringView offset_string)
{
// 1. Let parseResult be ParseText(offsetString, UTCOffset).
auto parse_result = parse_utc_offset(offset_string);
// 1. Let parseResult be ParseText(offsetString, UTCOffset[+SubMinutePrecision]).
auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::Yes);
// 2. Assert: parseResult is not a List of errors.
// 2. If parseResult is a List of errors, throw a RangeError exception.
if (!parse_result.has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidTimeZoneString, offset_string);
return parse_date_time_utc_offset(*parse_result);
}
// 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring
// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset
double parse_date_time_utc_offset(StringView offset_string)
{
// OPTIMIZATION: Some callers can assume that parsing will succeed.
// 1. Let parseResult be ParseText(offsetString, UTCOffset[+SubMinutePrecision]).
auto parse_result = Temporal::parse_utc_offset(offset_string, Temporal::SubMinutePrecision::Yes);
VERIFY(parse_result.has_value());
return parse_date_time_utc_offset(*parse_result);
}
// 21.4.1.33.2 ParseTimeZoneOffsetString ( offsetString ), https://tc39.es/ecma262/#sec-parsetimezoneoffsetstring
// 14.5.11 ParseDateTimeUTCOffset ( offsetString ), https://tc39.es/proposal-temporal/#sec-parsedatetimeutcoffset
double parse_date_time_utc_offset(Temporal::TimeZoneOffset const& parse_result)
{
// OPTIMIZATION: Some callers will have already parsed and validated the time zone identifier.
// 3. Assert: parseResult contains a ASCIISign Parse Node.
VERIFY(parse_result->sign.has_value());
VERIFY(parse_result.sign.has_value());
// 4. Let parsedSign be the source text matched by the ASCIISign Parse Node contained within parseResult.
auto parsed_sign = *parse_result->sign;
i8 sign { 0 };
// 5. If parsedSign is the single code point U+002D (HYPHEN-MINUS), then
if (parsed_sign == '-') {
// a. Let sign be -1.
sign = -1;
}
// 6. Else,
else {
// a. Let sign be 1.
sign = 1;
}
auto sign = parse_result.sign == '-' ? -1 : 1;
// 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed to be a sufficiently short string of decimal digits.
// 7. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed
// to be a sufficiently short string of decimal digits.
// 8. Assert: parseResult contains an Hour Parse Node.
VERIFY(parse_result->hour.has_value());
VERIFY(parse_result.hours.has_value());
// 9. Let parsedHours be the source text matched by the Hour Parse Node contained within parseResult.
// 10. Let hours be (StringToNumber(CodePointsToString(parsedHours))).
auto hours = *parse_result->hour;
auto hours = parse_result.hours->to_number<u8>().value();
// 11. If parseResult does not contain a MinuteSecond Parse Node, then
// a. Let minutes be 0.
// 12. Else,
// a. Let parsedMinutes be the source text matched by the first MinuteSecond Parse Node contained within parseResult.
// b. Let minutes be (StringToNumber(CodePointsToString(parsedMinutes))).
double minutes = parse_result->minute.value_or(0);
double minutes = parse_result.minutes.has_value() ? parse_result.minutes->to_number<u8>().value() : 0;
// 13. If parseResult does not contain two MinuteSecond Parse Nodes, then
// a. Let seconds be 0.
// 14. Else,
// a. Let parsedSeconds be the source text matched by the second secondSecond Parse Node contained within parseResult.
// b. Let seconds be (StringToNumber(CodePointsToString(parsedSeconds))).
double seconds = parse_result->second.value_or(0);
double seconds = parse_result.seconds.has_value() ? parse_result.seconds->to_number<u8>().value() : 0;
double nanoseconds = 0;
// 15. If parseResult does not contain a TemporalDecimalFraction Parse Node, then
if (!parse_result->fraction.has_value()) {
if (!parse_result.fraction.has_value()) {
// a. Let nanoseconds be 0.
nanoseconds = 0;
}
// 16. Else,
else {
// a. Let parsedFraction be the source text matched by the TemporalDecimalFraction Parse Node contained within parseResult.
auto parsed_fraction = *parse_result->fraction;
auto parsed_fraction = *parse_result.fraction;
// b. Let fraction be the string-concatenation of CodePointsToString(parsedFraction) and "000000000".
auto fraction = ByteString::formatted("{}000000000", parsed_fraction);

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2020-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -58,14 +58,6 @@ constexpr inline double ms_per_day = 86'400'000;
constexpr inline double ns_per_day = 86'400'000'000'000;
extern Crypto::SignedBigInteger const ns_per_day_bigint;
struct UTCOffset {
Optional<char> sign;
Optional<u8> hour;
Optional<u8> minute;
Optional<u8> second;
Optional<StringView> fraction;
};
double day(double);
double time_within_day(double);
u16 days_in_year(i32);
@ -93,8 +85,9 @@ double make_time(double hour, double min, double sec, double ms);
double make_day(double year, double month, double date);
double make_date(double day, double time);
double time_clip(double time);
Optional<UTCOffset> parse_utc_offset(StringView);
bool is_time_zone_offset_string(StringView offset_string);
double parse_time_zone_offset_string(StringView offset_string);
bool is_offset_time_zone_identifier(StringView offset_string);
ThrowCompletionOr<double> parse_date_time_utc_offset(VM&, StringView offset_string);
double parse_date_time_utc_offset(StringView offset_string);
double parse_date_time_utc_offset(Temporal::TimeZoneOffset const&);
}

View file

@ -2,7 +2,7 @@
* Copyright (c) 2020-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021, Petróczi Zoltán <petroczizoltan@tutanota.com>
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -21,6 +21,8 @@
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/DateTimeFormat.h>
#include <LibJS/Runtime/Intl/DateTimeFormatConstructor.h>
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <LibJS/Runtime/Value.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <LibUnicode/DisplayNames.h>
@ -1066,19 +1068,14 @@ JS_DEFINE_NATIVE_FUNCTION(DatePrototype::to_string)
}
// 21.4.4.41.1 TimeString ( tv ), https://tc39.es/ecma262/#sec-timestring
// 14.5.8 TimeString ( tv ), https://tc39.es/proposal-temporal/#sec-timestring
ByteString time_string(double time)
{
// 1. Let hour be ToZeroPaddedDecimalString((HourFromTime(tv)), 2).
auto hour = hour_from_time(time);
// 1. Let timeString be FormatTimeString((HourFromTime(tv)), (MinFromTime(tv)), (SecFromTime(tv)), 0, 0).
auto time_string = Temporal::format_time_string(hour_from_time(time), min_from_time(time), sec_from_time(time), 0, 0);
// 2. Let minute be ToZeroPaddedDecimalString((MinFromTime(tv)), 2).
auto minute = min_from_time(time);
// 3. Let second be ToZeroPaddedDecimalString((SecFromTime(tv)), 2).
auto second = sec_from_time(time);
// 4. Return the string-concatenation of hour, ":", minute, ":", second, the code unit 0x0020 (SPACE), and "GMT".
return ByteString::formatted("{:02}:{:02}:{:02} GMT", hour, minute, second);
// 4. Return the string-concatenation of timeString, the code unit 0x0020 (SPACE), and "GMT".
return ByteString::formatted("{} GMT", time_string);
}
// 21.4.4.41.2 DateString ( tv ), https://tc39.es/ecma262/#sec-datestring
@ -1105,62 +1102,40 @@ ByteString date_string(double time)
}
// 21.4.4.41.3 TimeZoneString ( tv ), https://tc39.es/ecma262/#sec-timezoneestring
// 14.5.9 TimeZoneString ( tv ), https://tc39.es/proposal-temporal/#sec-timezoneestring
ByteString time_zone_string(double time)
{
// 1. Let systemTimeZoneIdentifier be SystemTimeZoneIdentifier().
auto system_time_zone_identifier = JS::system_time_zone_identifier();
double offset_nanoseconds { 0 };
// 2. Let offsetMinutes be ! ParseTimeZoneIdentifier(systemTimeZoneIdentifier).[[OffsetMinutes]].
auto offset_minutes = Temporal::parse_time_zone_identifier(system_time_zone_identifier).offset_minutes;
auto in_dst = Unicode::TimeZoneOffset::InDST::No;
// 2. If IsTimeZoneOffsetString(systemTimeZoneIdentifier) is true, then
if (is_time_zone_offset_string(system_time_zone_identifier)) {
// a. Let offsetNs be ParseTimeZoneOffsetString(systemTimeZoneIdentifier).
offset_nanoseconds = parse_time_zone_offset_string(system_time_zone_identifier);
}
// 3. Else,
else {
// 2. If offsetMinutes is EMPTY, then
if (!offset_minutes.has_value()) {
// a. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(systemTimeZoneIdentifier, ((tv) × 10^6)).
auto offset = get_named_time_zone_offset_milliseconds(system_time_zone_identifier, time);
offset_nanoseconds = static_cast<double>(offset.offset.to_nanoseconds());
in_dst = offset.in_dst;
// b. Set offsetMinutes to truncate(offsetNs / (60 × 10**9)).
offset_minutes = trunc(static_cast<double>(offset.offset.to_nanoseconds()) / 60'000'000'000.0);
}
// 4. Let offset be 𝔽(truncate(offsetNs / 106)).
auto offset = trunc(offset_nanoseconds / 1e6);
// 3. Let offsetString be FormatOffsetTimeZoneIdentifier(offsetMinutes, UNSEPARATED).
auto offset_string = Temporal::format_offset_time_zone_identifier(*offset_minutes, Temporal::TimeStyle::Unseparated);
StringView offset_sign;
// 5. If offset is +0𝔽 or offset > +0𝔽, then
if (offset >= 0) {
// a. Let offsetSign be "+".
offset_sign = "+"sv;
// b. Let absOffset be offset.
}
// 6. Else,
else {
// a. Let offsetSign be "-".
offset_sign = "-"sv;
// b. Let absOffset be -offset.
offset *= -1;
}
// 7. Let offsetMin be ToZeroPaddedDecimalString((MinFromTime(absOffset)), 2).
auto offset_min = min_from_time(offset);
// 8. Let offsetHour be ToZeroPaddedDecimalString((HourFromTime(absOffset)), 2).
auto offset_hour = hour_from_time(offset);
// 9. Let tzName be an implementation-defined string that is either the empty String or the string-concatenation of the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), an implementation-defined timezone name, and the code unit 0x0029 (RIGHT PARENTHESIS).
// 5. Let tzName be an implementation-defined string that is either the empty String or the string-concatenation of
// the code unit 0x0020 (SPACE), the code unit 0x0028 (LEFT PARENTHESIS), an implementation-defined timezone name,
// and the code unit 0x0029 (RIGHT PARENTHESIS).
auto tz_name = Unicode::current_time_zone();
// Most implementations seem to prefer the long-form display name of the time zone. Not super important, but we may as well match that behavior.
if (auto name = Unicode::time_zone_display_name(Unicode::default_locale(), tz_name, in_dst, time); name.has_value())
tz_name = name.release_value();
// 10. Return the string-concatenation of offsetSign, offsetHour, offsetMin, and tzName.
return ByteString::formatted("{}{:02}{:02} ({})", offset_sign, offset_hour, offset_min, tz_name);
// 10. Return the string-concatenation of offsetString and tzName.
return ByteString::formatted("{} ({})", offset_string, tz_name);
}
// 21.4.4.41.4 ToDateString ( tv ), https://tc39.es/ecma262/#sec-todatestring

View file

@ -249,6 +249,7 @@
M(TemporalInvalidCalendarFunctionResult, "Invalid calendar, {}() function returned {}") \
M(TemporalInvalidCalendarIdentifier, "Invalid calendar identifier '{}'") \
M(TemporalInvalidCalendarString, "Invalid calendar string '{}'") \
M(TemporalInvalidCriticalAnnotation, "Invalid critical annotation: '{}={}'") \
M(TemporalInvalidDateTimeString, "Invalid date time string '{}'") \
M(TemporalInvalidDateTimeStringUTCDesignator, "Invalid date time string '{}': must not contain a UTC designator") \
M(TemporalInvalidDuration, "Invalid duration") \
@ -295,6 +296,7 @@
"nanoseconds with the opposite sign") \
M(TemporalNanosecondsConvertedToRemainderOfNanosecondsLongerThanDayLength, "Time zone or calendar ended up with a remainder of " \
"nanoseconds longer than the day length") \
M(TemporalObjectMustBePartialTemporalObject, "Object must be a partial Temporal object") \
M(TemporalObjectMustHaveOneOf, "Object must have at least one of the following properties: {}") \
M(TemporalObjectMustNotHave, "Object must not have a defined {} property") \
M(TemporalPropertyMustBeFinite, "Property must not be Infinity") \

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2021-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -11,6 +11,7 @@
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Intl/DateTimeFormat.h>
#include <LibJS/Runtime/Intl/DateTimeFormatConstructor.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibUnicode/DateTimeFormat.h>
#include <LibUnicode/Locale.h>
@ -81,6 +82,8 @@ JS_DEFINE_NATIVE_FUNCTION(DateTimeFormatConstructor::supported_locales_of)
}
// 11.1.2 CreateDateTimeFormat ( newTarget, locales, options, required, defaults ), https://tc39.es/ecma402/#sec-createdatetimeformat
// 15.7.1 CreateDateTimeFormat ( newTarget, locales, options, required, defaults [ , toLocaleStringTimeZone ] ), https://tc39.es/proposal-temporal/#sec-createdatetimeformat
// FIXME: Update the rest of this AO for Temporal once we have the required Temporal objects.
ThrowCompletionOr<GC::Ref<DateTimeFormat>> create_date_time_format(VM& vm, FunctionObject& new_target, Value locales_value, Value options_value, OptionRequired required, OptionDefaults defaults)
{
// 1. Let dateTimeFormat be ? OrdinaryCreateFromConstructor(newTarget, "%Intl.DateTimeFormat.prototype%", « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[HourCycle]], [[DateStyle]], [[TimeStyle]], [[DateTimeFormat]], [[BoundFormat]] »).
@ -200,29 +203,25 @@ ThrowCompletionOr<GC::Ref<DateTimeFormat>> create_date_time_format(VM& vm, Funct
}
// 29. If IsTimeZoneOffsetString(timeZone) is true, then
bool is_time_zone_offset_string = JS::is_time_zone_offset_string(time_zone);
bool is_time_zone_offset_string = JS::is_offset_time_zone_identifier(time_zone);
if (is_time_zone_offset_string) {
// a. Let parseResult be ParseText(StringToCodePoints(timeZone), UTCOffset).
auto parse_result = parse_utc_offset(time_zone);
// a. Let parseResult be ParseText(StringToCodePoints(timeZone), UTCOffset[~SubMinutePrecision]).
auto parse_result = Temporal::parse_utc_offset(time_zone, Temporal::SubMinutePrecision::No);
// b. Assert: parseResult is a Parse Node.
VERIFY(parse_result.has_value());
// c. If parseResult contains more than one MinuteSecond Parse Node, throw a RangeError exception.
if (parse_result->second.has_value())
return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, time_zone, vm.names.timeZone);
// c. Let offsetNanoseconds be ? ParseDateTimeUTCOffset(timeZone).
auto offset_nanoseconds = TRY(parse_date_time_utc_offset(vm, time_zone));
// d. Let offsetNanoseconds be ParseTimeZoneOffsetString(timeZone).
auto offset_nanoseconds = parse_time_zone_offset_string(time_zone);
// e. Let offsetMinutes be offsetNanoseconds / (6 × 10**10).
// d. Let offsetMinutes be offsetNanoseconds / (6 × 10**10).
auto offset_minutes = offset_nanoseconds / 60'000'000'000;
// f. Assert: offsetMinutes is an integer.
// e. Assert: offsetMinutes is an integer.
VERIFY(trunc(offset_minutes) == offset_minutes);
// g. Set timeZone to FormatOffsetTimeZoneIdentifier(offsetMinutes).
// f. Set timeZone to FormatOffsetTimeZoneIdentifier(offsetMinutes).
time_zone = format_offset_time_zone_identifier(offset_minutes);
}
// 30. Else,

View file

@ -101,6 +101,8 @@
#include <LibJS/Runtime/SymbolPrototype.h>
#include <LibJS/Runtime/Temporal/DurationConstructor.h>
#include <LibJS/Runtime/Temporal/DurationPrototype.h>
#include <LibJS/Runtime/Temporal/PlainMonthDayConstructor.h>
#include <LibJS/Runtime/Temporal/PlainMonthDayPrototype.h>
#include <LibJS/Runtime/Temporal/Temporal.h>
#include <LibJS/Runtime/TypedArray.h>
#include <LibJS/Runtime/TypedArrayConstructor.h>

View file

@ -9,11 +9,16 @@
#include <LibCrypto/BigFraction/BigFraction.h>
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Date.h>
#include <LibJS/Runtime/PropertyKey.h>
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/Calendar.h>
#include <LibJS/Runtime/Temporal/Duration.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibJS/Runtime/Temporal/Instant.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainDateTime.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
namespace JS::Temporal {
@ -43,6 +48,65 @@ StringView temporal_unit_to_string(Unit unit)
return temporal_units[to_underlying(unit)].singular_property_name;
}
// 13.1 ISODateToEpochDays ( year, month, date ), https://tc39.es/proposal-temporal/#sec-isodatetoepochdays
double iso_date_to_epoch_days(double year, double month, double date)
{
// 1. Let resolvedYear be year + floor(month / 12).
// 2. Let resolvedMonth be month modulo 12.
// 3. Find a time t such that EpochTimeToEpochYear(t) = resolvedYear, EpochTimeToMonthInYear(t) = resolvedMonth, and EpochTimeToDate(t) = 1.
// 4. Return EpochTimeToDayNumber(t) + date - 1.
// EDITOR'S NOTE: This operation corresponds to ECMA-262 operation MakeDay(year, month, date). It calculates the
// result in mathematical values instead of Number values. These two operations would be unified when
// https://github.com/tc39/ecma262/issues/1087 is fixed.
// Since we don't have a real MV type to work with, let's defer to MakeDay.
return JS::make_day(year, month, date);
}
// 13.2 EpochDaysToEpochMs ( day, time ), https://tc39.es/proposal-temporal/#sec-epochdaystoepochms
double epoch_days_to_epoch_ms(double day, double time)
{
// 1. Return day × (msPerDay) + time.
return day * JS::ms_per_day + time;
}
// 13.6 GetTemporalOverflowOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporaloverflowoption
ThrowCompletionOr<Overflow> get_temporal_overflow_option(VM& vm, Object const& options)
{
// 1. Let stringValue be ? GetOption(options, "overflow", STRING, « "constrain", "reject" », "constrain").
auto string_value = TRY(get_option(vm, options, vm.names.overflow, OptionType::String, { "constrain"sv, "reject"sv }, "constrain"sv));
// 2. If stringValue is "constrain", return CONSTRAIN.
if (string_value.as_string().utf8_string() == "constrain"sv)
return Overflow::Constrain;
// 3. Return REJECT.
return Overflow::Reject;
}
// 13.10 GetTemporalShowCalendarNameOption ( options ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalshowcalendarnameoption
ThrowCompletionOr<ShowCalendar> get_temporal_show_calendar_name_option(VM& vm, Object const& options)
{
// 1. Let stringValue be ? GetOption(options, "calendarName", STRING, « "auto", "always", "never", "critical" », "auto").
auto string_value = TRY(get_option(vm, options, vm.names.calendarName, OptionType::String, { "auto"sv, "always"sv, "never"sv, "critical"sv }, "auto"sv));
// 2. If stringValue is "always", return ALWAYS.
if (string_value.as_string().utf8_string_view() == "always"sv)
return ShowCalendar::Always;
// 3. If stringValue is "never", return NEVER.
if (string_value.as_string().utf8_string_view() == "never"sv)
return ShowCalendar::Never;
// 4. If stringValue is "critical", return CRITICAL.
if (string_value.as_string().utf8_string_view() == "critical"sv)
return ShowCalendar::Critical;
// 5. Return AUTO.
return ShowCalendar::Auto;
}
// 13.14 ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ), https://tc39.es/proposal-temporal/#sec-validatetemporalroundingincrement
ThrowCompletionOr<void> validate_temporal_rounding_increment(VM& vm, u64 increment, u64 dividend, bool inclusive)
{
@ -388,6 +452,40 @@ Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit unit)
}
}
// 13.23 IsPartialTemporalObject ( value ), https://tc39.es/proposal-temporal/#sec-temporal-ispartialtemporalobject
ThrowCompletionOr<bool> is_partial_temporal_object(VM& vm, Value value)
{
// 1. If value is not an Object, return false.
if (!value.is_object())
return false;
auto const& object = value.as_object();
// 2. If value has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]],
// [[InitializedTemporalTime]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal
// slot, return false.
// FIXME: Add the other types as we define them.
if (is<PlainMonthDay>(object))
return false;
// 3. Let calendarProperty be ? Get(value, "calendar").
auto calendar_property = TRY(object.get(vm.names.calendar));
// 4. If calendarProperty is not undefined, return false.
if (!calendar_property.is_undefined())
return false;
// 5. Let timeZoneProperty be ? Get(value, "timeZone").
auto time_zone_property = TRY(object.get(vm.names.timeZone));
// 6. If timeZoneProperty is not undefined, return false.
if (!time_zone_property.is_undefined())
return false;
// 7. Return true.
return true;
}
// 13.24 FormatFractionalSeconds ( subSecondNanoseconds, precision ), https://tc39.es/proposal-temporal/#sec-temporal-formatfractionalseconds
String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision)
{
@ -422,6 +520,27 @@ String format_fractional_seconds(u64 sub_second_nanoseconds, Precision precision
return MUST(String::formatted(".{}", fraction_string));
}
// 13.25 FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formattimestring
String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision precision, Optional<TimeStyle> style)
{
// 1. If style is present and style is UNSEPARATED, let separator be the empty String; otherwise, let separator be ":".
auto separator = style == TimeStyle::Unseparated ? ""sv : ":"sv;
// 2. Let hh be ToZeroPaddedDecimalString(hour, 2).
// 3. Let mm be ToZeroPaddedDecimalString(minute, 2).
// 4. If precision is minute, return the string-concatenation of hh, separator, and mm.
if (precision.has<SecondsStringPrecision::Minute>())
return MUST(String::formatted("{:02}{}{:02}", hour, separator, minute));
// 5. Let ss be ToZeroPaddedDecimalString(second, 2).
// 6. Let subSecondsPart be FormatFractionalSeconds(subSecondNanoseconds, precision).
auto sub_seconds_part = format_fractional_seconds(sub_second_nanoseconds, precision.downcast<Auto, u8>());
// 7. Return the string-concatenation of hh, separator, mm, separator, ss, and subSecondsPart.
return MUST(String::formatted("{:02}{}{:02}{}{:02}{}", hour, separator, minute, separator, second, sub_seconds_part));
}
// 13.26 GetUnsignedRoundingMode ( roundingMode, sign ), https://tc39.es/proposal-temporal/#sec-getunsignedroundingmode
UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode rounding_mode, Sign sign)
{
@ -664,6 +783,260 @@ Crypto::SignedBigInteger round_number_to_increment(Crypto::SignedBigInteger cons
return rounded.multiplied_by(increment);
}
// 13.33 ParseISODateTime ( isoString, allowedFormats ), https://tc39.es/proposal-temporal/#sec-temporal-parseisodatetime
ThrowCompletionOr<ParsedISODateTime> parse_iso_date_time(VM& vm, StringView iso_string, ReadonlySpan<Production> allowed_formats)
{
// 1. Let parseResult be EMPTY.
Optional<ParseResult> parse_result;
// 2. Let calendar be EMPTY.
Optional<String> calendar;
// 3. Let yearAbsent be false.
auto year_absent = false;
// 4. For each nonterminal goal of allowedFormats, do
for (auto goal : allowed_formats) {
// a. If parseResult is not a Parse Node, then
if (parse_result.has_value())
break;
// i. Set parseResult to ParseText(StringToCodePoints(isoString), goal).
parse_result = parse_iso8601(goal, iso_string);
// ii. If parseResult is a Parse Node, then
if (parse_result.has_value()) {
// 1. Let calendarWasCritical be false.
auto calendar_was_critical = false;
// 2. For each Annotation Parse Node annotation contained within parseResult, do
for (auto const& annotation : parse_result->annotations) {
// a. Let key be the source text matched by the AnnotationKey Parse Node contained within annotation.
auto const& key = annotation.key;
// b. Let value be the source text matched by the AnnotationValue Parse Node contained within annotation.
auto const& value = annotation.value;
// c. If CodePointsToString(key) is "u-ca", then
if (key == "u-ca"sv) {
// i. If calendar is EMPTY, then
if (!calendar.has_value()) {
// i. Set calendar to CodePointsToString(value).
calendar = String::from_utf8_without_validation(value.bytes());
// ii. If annotation contains an AnnotationCriticalFlag Parse Node, set calendarWasCritical to true.
if (annotation.critical)
calendar_was_critical = true;
}
// ii. Else,
else {
// i. If annotation contains an AnnotationCriticalFlag Parse Node, or calendarWasCritical is true,
// throw a RangeError exception.
if (annotation.critical || calendar_was_critical)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCriticalAnnotation, key, value);
}
}
// d. Else,
else {
// i. If annotation contains an AnnotationCriticalFlag Parse Node, throw a RangeError exception.
if (annotation.critical)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCriticalAnnotation, key, value);
}
}
// 3. If goal is TemporalMonthDayString or TemporalYearMonthString, calendar is not EMPTY, and the
// ASCII-lowercase of calendar is not "iso8601", throw a RangeError exception.
if (goal == Production::TemporalMonthDayString || goal == Production::TemporalYearMonthString) {
if (calendar.has_value() && !calendar->equals_ignoring_ascii_case("iso8601"sv))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarIdentifier, *calendar);
}
// 4. If goal is TemporalMonthDayString and parseResult does not contain a DateYear Parse Node, then
if (goal == Production::TemporalMonthDayString && !parse_result->date_year.has_value()) {
// a. Assert: goal is the last element of allowedFormats.
VERIFY(goal == allowed_formats.last());
// b. Set yearAbsent to true.
year_absent = true;
}
}
}
// 5. If parseResult is not a Parse Node, throw a RangeError exception.
if (!parse_result.has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODateTime);
// 6. NOTE: Applications of StringToNumber below do not lose precision, since each of the parsed values is guaranteed
// to be a sufficiently short string of decimal digits.
// 7. Let each of year, month, day, hour, minute, second, and fSeconds be the source text matched by the respective
// DateYear, DateMonth, DateDay, the first Hour, the first MinuteSecond, TimeSecond, and the first
// TemporalDecimalFraction Parse Node contained within parseResult, or an empty sequence of code points if not present.
auto year = parse_result->date_year.value_or({});
auto month = parse_result->date_month.value_or({});
auto day = parse_result->date_day.value_or({});
auto hour = parse_result->time_hour.value_or({});
auto minute = parse_result->time_minute.value_or({});
auto second = parse_result->time_second.value_or({});
auto fractional_seconds = parse_result->time_fraction.value_or({});
// 8. Let yearMV be (StringToNumber(CodePointsToString(year))).
auto year_value = string_to_number(year);
// 9. If month is empty, then
// a. Let monthMV be 1.
// 10. Else,
// a. Let monthMV be (StringToNumber(CodePointsToString(month))).
auto month_value = month.is_empty() ? 1 : string_to_number(month);
// 11. If day is empty, then
// a. Let dayMV be 1.
// 12. Else,
// a. Let dayMV be (StringToNumber(CodePointsToString(day))).
auto day_value = day.is_empty() ? 1 : string_to_number(day);
// 13. If hour is empty, then
// a. Let hourMV be 0.
// 14. Else,
// a. Let hourMV be (StringToNumber(CodePointsToString(hour))).
auto hour_value = hour.is_empty() ? 0 : string_to_number(hour);
// 15. If minute is empty, then
// a. Let minuteMV be 0.
// 16. Else,
// a. Let minuteMV be (StringToNumber(CodePointsToString(minute))).
auto minute_value = minute.is_empty() ? 0 : string_to_number(minute);
// 17. If second is empty, then
// a. Let secondMV be 0.
// 18. Else,
// a. Let secondMV be (StringToNumber(CodePointsToString(second))).
// b. If secondMV = 60, then
// i. Set secondMV to 59.
auto second_value = second.is_empty() ? 0 : min(string_to_number(second), 59.0);
double millisecond_value = 0;
double microsecond_value = 0;
double nanosecond_value = 0;
// 19. If fSeconds is not empty, then
if (!fractional_seconds.is_empty()) {
// a. Let fSecondsDigits be the substring of CodePointsToString(fSeconds) from 1.
auto fractional_seconds_digits = fractional_seconds.substring_view(1);
// b. Let fSecondsDigitsExtended be the string-concatenation of fSecondsDigits and "000000000".
auto fractional_seconds_extended = MUST(String::formatted("{}000000000", fractional_seconds_digits));
// c. Let millisecond be the substring of fSecondsDigitsExtended from 0 to 3.
auto millisecond = fractional_seconds_extended.bytes_as_string_view().substring_view(0, 3);
// d. Let microsecond be the substring of fSecondsDigitsExtended from 3 to 6.
auto microsecond = fractional_seconds_extended.bytes_as_string_view().substring_view(3, 3);
// e. Let nanosecond be the substring of fSecondsDigitsExtended from 6 to 9.
auto nanosecond = fractional_seconds_extended.bytes_as_string_view().substring_view(6, 3);
// f. Let millisecondMV be (StringToNumber(millisecond)).
millisecond_value = string_to_number(millisecond);
// g. Let microsecondMV be (StringToNumber(microsecond)).
microsecond_value = string_to_number(microsecond);
// h. Let nanosecondMV be (StringToNumber(nanosecond)).
nanosecond_value = string_to_number(nanosecond);
}
// 20. Else,
else {
// a. Let millisecondMV be 0.
// b. Let microsecondMV be 0.
// c. Let nanosecondMV be 0.
}
// 21. Assert: IsValidISODate(yearMV, monthMV, dayMV) is true.
VERIFY(is_valid_iso_date(year_value, month_value, day_value));
Variant<ParsedISODateTime::StartOfDay, Time> time { ParsedISODateTime::StartOfDay {} };
// 22. If hour is empty, then
if (hour.is_empty()) {
// a. Let time be START-OF-DAY.
}
// 23. Else,
else {
// a. Let time be CreateTimeRecord(hourMV, minuteMV, secondMV, millisecondMV, microsecondMV, nanosecondMV).
time = create_time_record(hour_value, minute_value, second_value, millisecond_value, microsecond_value, nanosecond_value);
}
// 24. Let timeZoneResult be ISO String Time Zone Parse Record { [[Z]]: false, [[OffsetString]]: EMPTY, [[TimeZoneAnnotation]]: EMPTY }.
ParsedISOTimeZone time_zone_result;
// 25. If parseResult contains a TimeZoneIdentifier Parse Node, then
if (parse_result->time_zone_identifier.has_value()) {
// a. Let identifier be the source text matched by the TimeZoneIdentifier Parse Node contained within parseResult.
// b. Set timeZoneResult.[[TimeZoneAnnotation]] to CodePointsToString(identifier).
time_zone_result.time_zone_annotation = String::from_utf8_without_validation(parse_result->time_zone_identifier->bytes());
}
// 26. If parseResult contains a UTCDesignator Parse Node, then
if (parse_result->utc_designator.has_value()) {
// a. Set timeZoneResult.[[Z]] to true.
time_zone_result.z_designator = true;
}
// 27. Else if parseResult contains a UTCOffset[+SubMinutePrecision] Parse Node, then
else if (parse_result->date_time_offset.has_value()) {
// a. Let offset be the source text matched by the UTCOffset[+SubMinutePrecision] Parse Node contained within parseResult.
// b. Set timeZoneResult.[[OffsetString]] to CodePointsToString(offset).
time_zone_result.offset_string = String::from_utf8_without_validation(parse_result->date_time_offset->source_text.bytes());
}
// 28. If yearAbsent is true, let yearReturn be EMPTY; else let yearReturn be yearMV.
Optional<i32> year_return;
if (!year_absent)
year_return = static_cast<i32>(year_value);
// 29. Return ISO Date-Time Parse Record { [[Year]]: yearReturn, [[Month]]: monthMV, [[Day]]: dayMV, [[Time]]: time, [[TimeZone]]: timeZoneResult, [[Calendar]]: calendar }.
return ParsedISODateTime { .year = year_return, .month = static_cast<u8>(month_value), .day = static_cast<u8>(day_value), .time = move(time), .time_zone = move(time_zone_result), .calendar = move(calendar) };
}
// 13.34 ParseTemporalCalendarString ( string ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporalcalendarstring
ThrowCompletionOr<String> parse_temporal_calendar_string(VM& vm, String const& string)
{
// 1. Let parseResult be Completion(ParseISODateTime(string, « TemporalDateTimeString[+Zoned], TemporalDateTimeString[~Zoned],
// TemporalInstantString, TemporalTimeString, TemporalMonthDayString, TemporalYearMonthString »)).
static constexpr auto productions = to_array<Production>({
Production::TemporalZonedDateTimeString,
Production::TemporalDateTimeString,
Production::TemporalInstantString,
Production::TemporalTimeString,
Production::TemporalMonthDayString,
Production::TemporalYearMonthString,
});
auto parse_result = parse_iso_date_time(vm, string, productions);
// 2. If parseResult is a normal completion, then
if (!parse_result.is_error()) {
// a. Let calendar be parseResult.[[Value]].[[Calendar]].
auto calendar = parse_result.value().calendar;
// b. If calendar is empty, return "iso8601".
// c. Else, return calendar.
return calendar.value_or("iso8601"_string);
}
// 3. Else,
else {
// a. Set parseResult to ParseText(StringToCodePoints(string), AnnotationValue).
auto annotation_parse_result = parse_iso8601(Production::AnnotationValue, string);
// b. If parseResult is a List of errors, throw a RangeError exception.
if (!annotation_parse_result.has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarString, string);
// c. Else, return string.
return string;
}
}
// 13.35 ParseTemporalDurationString ( isoString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaldurationstring
ThrowCompletionOr<GC::Ref<Duration>> parse_temporal_duration_string(VM& vm, StringView iso_string)
{
@ -884,6 +1257,149 @@ ThrowCompletionOr<GC::Ref<Duration>> parse_temporal_duration_string(VM& vm, Stri
return TRY(create_temporal_duration(vm, years_value, months_value, weeks_value, days_value, hours_value, factored_minutes_value, factored_seconds_value, factored_milliseconds_value, factored_microseconds_value, factored_nanoseconds_value));
}
// 13.36 ParseTemporalTimeZoneString ( timeZoneString ), https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaltimezonestring
ThrowCompletionOr<TimeZone> parse_temporal_time_zone_string(VM& vm, StringView time_zone_string)
{
// 1. Let parseResult be ParseText(StringToCodePoints(timeZoneString), TimeZoneIdentifier).
auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, time_zone_string);
// 2. If parseResult is a Parse Node, then
if (parse_result.has_value()) {
// a. Return ! ParseTimeZoneIdentifier(timeZoneString).
return parse_time_zone_identifier(parse_result.release_value());
}
// 3. Let result be ? ParseISODateTime(timeZoneString, « TemporalDateTimeString[+Zoned], TemporalDateTimeString[~Zoned],
// TemporalInstantString, TemporalTimeString, TemporalMonthDayString, TemporalYearMonthString »).
static constexpr auto productions = to_array<Production>({
Production::TemporalZonedDateTimeString,
Production::TemporalDateTimeString,
Production::TemporalInstantString,
Production::TemporalTimeString,
Production::TemporalMonthDayString,
Production::TemporalYearMonthString,
});
auto result = TRY(parse_iso_date_time(vm, time_zone_string, productions));
// 4. Let timeZoneResult be result.[[TimeZone]].
auto time_zone_result = move(result.time_zone);
// 5. If timeZoneResult.[[TimeZoneAnnotation]] is not empty, then
if (time_zone_result.time_zone_annotation.has_value()) {
// a. Return ! ParseTimeZoneIdentifier(timeZoneResult.[[TimeZoneAnnotation]]).
return MUST(parse_time_zone_identifier(vm, *time_zone_result.time_zone_annotation));
}
// 6. If timeZoneResult.[[Z]] is true, then
if (time_zone_result.z_designator) {
// a. Return ! ParseTimeZoneIdentifier("UTC").
return MUST(parse_time_zone_identifier(vm, "UTC"sv));
}
// 7. If timeZoneResult.[[OffsetString]] is not empty, then
if (time_zone_result.offset_string.has_value()) {
// a. Return ? ParseTimeZoneIdentifier(timeZoneResult.[[OffsetString]]).
return TRY(parse_time_zone_identifier(vm, *time_zone_result.offset_string));
}
// 8. Throw a RangeError exception.
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidTimeZoneString, time_zone_string);
}
// 13.40 ToMonthCode ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-tomonthcode
ThrowCompletionOr<String> to_month_code(VM& vm, Value argument)
{
// 1. Let monthCode be ? ToPrimitive(argument, STRING).
auto month_code = TRY(argument.to_primitive(vm, Value::PreferredType::String));
// 2. If monthCode is not a String, throw a TypeError exception.
if (!month_code.is_string())
return vm.throw_completion<TypeError>(ErrorType::TemporalInvalidMonthCode);
auto month_code_string = month_code.as_string().utf8_string_view();
// 3. If the length of monthCode is not 3 or 4, throw a RangeError exception.
if (month_code_string.length() != 3 && month_code_string.length() != 4)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthCode);
// 4. If the first code unit of monthCode is not 0x004D (LATIN CAPITAL LETTER M), throw a RangeError exception.
if (month_code_string[0] != 'M')
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthCode);
// 5. If the second code unit of monthCode is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE),
// throw a RangeError exception.
if (!is_ascii_digit(month_code_string[1]) || parse_ascii_digit(month_code_string[1]) > 9)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthCode);
// 6. If the third code unit of monthCode is not in the inclusive interval from 0x0030 (DIGIT ZERO) to 0x0039 (DIGIT NINE),
// throw a RangeError exception.
if (!is_ascii_digit(month_code_string[2]) || parse_ascii_digit(month_code_string[2]) > 9)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthCode);
// 7. If the length of monthCode is 4 and the fourth code unit of monthCode is not 0x004C (LATIN CAPITAL LETTER L),
// throw a RangeError exception.
if (month_code_string.length() == 4 && month_code_string[3] != 'L')
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthCode);
// 8. Let monthCodeDigits be the substring of monthCode from 1 to 3.
auto month_code_digits = month_code_string.substring_view(1, 2);
// 9. Let monthCodeInteger be (StringToNumber(monthCodeDigits)).
auto month_code_integer = month_code_digits.to_number<u8>().value();
// 10. If monthCodeInteger is 0 and the length of monthCode is not 4, throw a RangeError exception.
if (month_code_integer == 0 && month_code_string.length() != 4)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidMonthCode);
// 11. Return monthCode.
return month_code.as_string().utf8_string();
}
// 13.41 ToOffsetString ( argument ), https://tc39.es/proposal-temporal/#sec-temporal-tooffsetstring
ThrowCompletionOr<String> to_offset_string(VM& vm, Value argument)
{
// 1. Let offset be ? ToPrimitive(argument, STRING).
auto offset = TRY(argument.to_primitive(vm, Value::PreferredType::String));
// 2. If offset is not a String, throw a TypeError exception.
if (!offset.is_string())
return vm.throw_completion<TypeError>(ErrorType::TemporalInvalidTimeZoneString, offset);
// 3. Perform ? ParseDateTimeUTCOffset(offset).
TRY(parse_date_time_utc_offset(vm, offset.as_string().utf8_string_view()));
// 4. Return offset.
return offset.as_string().utf8_string();
}
// 13.42 ISODateToFields ( calendar, isoDate, type ), https://tc39.es/proposal-temporal/#sec-temporal-isodatetofields
CalendarFields iso_date_to_fields(StringView calendar, ISODate const& iso_date, DateType type)
{
// 1. Let fields be an empty Calendar Fields Record with all fields set to unset.
auto fields = CalendarFields::unset();
// 2. Let calendarDate be CalendarISOToDate(calendar, isoDate).
auto calendar_date = calendar_iso_to_date(calendar, iso_date);
// 3. Set fields.[[MonthCode]] to calendarDate.[[MonthCode]].
fields.month_code = calendar_date.month_code;
// 4. If type is MONTH-DAY or DATE, then
if (type == DateType::MonthDay || type == DateType::Date) {
// a. Set fields.[[Day]] to calendarDate.[[Day]].
fields.day = calendar_date.day;
}
// 5. If type is YEAR-MONTH or DATE, then
if (type == DateType::YearMonth || type == DateType::Date) {
// a. Set fields.[[Year]] to calendarDate.[[Year]].
fields.year = calendar_date.year;
}
// 6. Return fields.
return fields;
}
// 14.4.1.1 GetOptionsObject ( options ), https://tc39.es/proposal-temporal/#sec-getoptionsobject
ThrowCompletionOr<GC::Ref<Object>> get_options_object(VM& vm, Value options)
{
@ -992,4 +1508,19 @@ ThrowCompletionOr<u64> get_rounding_increment_option(VM& vm, Object const& optio
return static_cast<u64>(integer_increment);
}
// 14.5.1 GetUTCEpochNanoseconds ( isoDateTime ), https://tc39.es/proposal-temporal/#sec-getutcepochnanoseconds
Crypto::SignedBigInteger get_utc_epoch_nanoseconds(ISODateTime const& iso_date_time)
{
return JS::get_utc_epoch_nanoseconds(
iso_date_time.iso_date.year,
iso_date_time.iso_date.month,
iso_date_time.iso_date.day,
iso_date_time.time.hour,
iso_date_time.time.minute,
iso_date_time.time.second,
iso_date_time.time.millisecond,
iso_date_time.time.microsecond,
iso_date_time.time.nanosecond);
}
}

View file

@ -14,6 +14,8 @@
#include <LibGC/Ptr.h>
#include <LibJS/Forward.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibJS/Runtime/Temporal/PlainTime.h>
#include <LibJS/Runtime/VM.h>
#include <LibJS/Runtime/ValueInlines.h>
#include <math.h>
@ -25,6 +27,29 @@ enum class ArithmeticOperation {
Subtract,
};
enum class DateType {
Date,
MonthDay,
YearMonth,
};
enum class Overflow {
Constrain,
Reject,
};
enum class ShowCalendar {
Auto,
Always,
Never,
Critical,
};
enum class TimeStyle {
Separated,
Unseparated,
};
// https://tc39.es/proposal-temporal/#sec-temporal-units
enum class Unit {
Year,
@ -91,8 +116,9 @@ using UnitValue = Variant<Unset, Auto, Unit>;
struct SecondsStringPrecision {
struct Minute { };
using Precision = Variant<Minute, Auto, u8>;
Variant<Minute, Auto, u8> precision;
Precision precision;
Unit unit;
u8 increment { 0 };
};
@ -103,6 +129,29 @@ struct RelativeTo {
GC::Ptr<JS::Object> zoned_relative_to; // [[ZonedRelativeTo]]
};
// 13.31 ISO String Time Zone Parse Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-string-time-zone-parse-records
struct ParsedISOTimeZone {
bool z_designator { false };
Optional<String> offset_string;
Optional<String> time_zone_annotation;
};
// 13.32 ISO Date-Time Parse Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-time-parse-records
struct ParsedISODateTime {
struct StartOfDay { };
Optional<i32> year { 0 };
u8 month { 0 };
u8 day { 0 };
Variant<StartOfDay, Time> time;
ParsedISOTimeZone time_zone;
Optional<String> calendar;
};
double iso_date_to_epoch_days(double year, double month, double date);
double epoch_days_to_epoch_ms(double day, double time);
ThrowCompletionOr<Overflow> get_temporal_overflow_option(VM&, Object const& options);
ThrowCompletionOr<ShowCalendar> get_temporal_show_calendar_name_option(VM&, Object const& options);
ThrowCompletionOr<void> validate_temporal_rounding_increment(VM&, u64 increment, u64 dividend, bool inclusive);
ThrowCompletionOr<Precision> get_temporal_fractional_second_digits_option(VM&, Object const& options);
SecondsStringPrecision to_seconds_string_precision_record(UnitValue, Precision);
@ -113,13 +162,21 @@ bool is_calendar_unit(Unit);
UnitCategory temporal_unit_category(Unit);
RoundingIncrement maximum_temporal_duration_rounding_increment(Unit);
Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit);
ThrowCompletionOr<bool> is_partial_temporal_object(VM&, Value);
String format_fractional_seconds(u64, Precision);
String format_time_string(u8 hour, u8 minute, u8 second, u16 sub_second_nanoseconds, SecondsStringPrecision::Precision, Optional<TimeStyle> = {});
UnsignedRoundingMode get_unsigned_rounding_mode(RoundingMode, Sign);
double apply_unsigned_rounding_mode(double, double r1, double r2, UnsignedRoundingMode);
Crypto::SignedBigInteger apply_unsigned_rounding_mode(Crypto::SignedDivisionResult const&, Crypto::SignedBigInteger const& r1, Crypto::SignedBigInteger const& r2, UnsignedRoundingMode, Crypto::UnsignedBigInteger const& increment);
double round_number_to_increment(double, u64 increment, RoundingMode);
Crypto::SignedBigInteger round_number_to_increment(Crypto::SignedBigInteger const&, Crypto::UnsignedBigInteger const& increment, RoundingMode);
ThrowCompletionOr<ParsedISODateTime> parse_iso_date_time(VM&, StringView iso_string, ReadonlySpan<Production> allowed_formats);
ThrowCompletionOr<String> parse_temporal_calendar_string(VM&, String const&);
ThrowCompletionOr<GC::Ref<Duration>> parse_temporal_duration_string(VM&, StringView iso_string);
ThrowCompletionOr<TimeZone> parse_temporal_time_zone_string(VM& vm, StringView time_zone_string);
ThrowCompletionOr<String> to_month_code(VM&, Value argument);
ThrowCompletionOr<String> to_offset_string(VM&, Value argument);
CalendarFields iso_date_to_fields(StringView calendar, ISODate const&, DateType);
// 13.38 ToIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerwithtruncation
template<typename... Args>
@ -153,6 +210,21 @@ ThrowCompletionOr<double> to_integer_with_truncation(VM& vm, StringView argument
return trunc(number);
}
// 13.37 ToPositiveIntegerWithTruncation ( argument ), https://tc39.es/proposal-temporal/#sec-topositiveintegerwithtruncation
template<typename... Args>
ThrowCompletionOr<double> to_positive_integer_with_truncation(VM& vm, Value argument, ErrorType error_type, Args&&... args)
{
// 1. Let integer be ? ToIntegerWithTruncation(argument).
auto integer = TRY(to_integer_with_truncation(vm, argument, error_type, args...));
// 2. If integer ≤ 0, throw a RangeError exception.
if (integer <= 0)
return vm.throw_completion<RangeError>(error_type, args...);
// 3. Return integer.
return integer;
}
// 13.39 ToIntegerIfIntegral ( argument ), https://tc39.es/proposal-temporal/#sec-tointegerifintegral
template<typename... Args>
ThrowCompletionOr<double> to_integer_if_integral(VM& vm, Value argument, ErrorType error_type, Args&&... args)
@ -168,6 +240,12 @@ ThrowCompletionOr<double> to_integer_if_integral(VM& vm, Value argument, ErrorTy
return number.as_double();
}
// 14.2 The Year-Week Record Specification Type, https://tc39.es/proposal-temporal/#sec-year-week-record-specification-type
struct YearWeek {
Optional<u8> week;
Optional<i32> year;
};
enum class OptionType {
Boolean,
String,
@ -186,5 +264,6 @@ ThrowCompletionOr<Value> get_option(VM& vm, Object const& options, PropertyKey c
ThrowCompletionOr<RoundingMode> get_rounding_mode_option(VM&, Object const& options, RoundingMode fallback);
ThrowCompletionOr<u64> get_rounding_increment_option(VM&, Object const& options);
Crypto::SignedBigInteger get_utc_epoch_nanoseconds(ISODateTime const&);
}

View file

@ -0,0 +1,713 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/NonnullRawPtr.h>
#include <AK/QuickSort.h>
#include <LibJS/Runtime/Temporal/Calendar.h>
#include <LibJS/Runtime/Temporal/DateEquations.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <LibJS/Runtime/VM.h>
#include <LibUnicode/Locale.h>
#include <LibUnicode/UnicodeKeywords.h>
namespace JS::Temporal {
enum class CalendarFieldConversion {
ToIntegerWithTruncation,
ToMonthCode,
ToOffsetString,
ToPositiveIntegerWithTruncation,
ToString,
ToTemporalTimeZoneIdentifier,
};
// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields
#define JS_ENUMERATE_CALENDAR_FIELDS \
__JS_ENUMERATE(CalendarField::Era, era, vm.names.era, CalendarFieldConversion::ToString) \
__JS_ENUMERATE(CalendarField::EraYear, era_year, vm.names.eraYear, CalendarFieldConversion::ToIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Year, year, vm.names.year, CalendarFieldConversion::ToIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Month, month, vm.names.month, CalendarFieldConversion::ToPositiveIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::MonthCode, month_code, vm.names.monthCode, CalendarFieldConversion::ToMonthCode) \
__JS_ENUMERATE(CalendarField::Day, day, vm.names.day, CalendarFieldConversion::ToPositiveIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Hour, hour, vm.names.hour, CalendarFieldConversion::ToIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Minute, minute, vm.names.minute, CalendarFieldConversion::ToIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Second, second, vm.names.second, CalendarFieldConversion::ToIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Millisecond, millisecond, vm.names.millisecond, CalendarFieldConversion::ToIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Microsecond, microsecond, vm.names.microsecond, CalendarFieldConversion::ToIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Nanosecond, nanosecond, vm.names.nanosecond, CalendarFieldConversion::ToIntegerWithTruncation) \
__JS_ENUMERATE(CalendarField::Offset, offset, vm.names.offset, CalendarFieldConversion::ToOffsetString) \
__JS_ENUMERATE(CalendarField::TimeZone, time_zone, vm.names.timeZone, CalendarFieldConversion::ToTemporalTimeZoneIdentifier)
struct CalendarFieldData {
CalendarField key;
NonnullRawPtr<PropertyKey> property;
CalendarFieldConversion conversion;
};
static Vector<CalendarFieldData> sorted_calendar_fields(VM& vm, CalendarFieldList fields)
{
auto data_for_field = [&](auto field) -> CalendarFieldData {
switch (field) {
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
case enumeration: \
return { enumeration, property_key, conversion };
JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE
}
VERIFY_NOT_REACHED();
};
Vector<CalendarFieldData> result;
result.ensure_capacity(fields.size());
for (auto field : fields)
result.unchecked_append(data_for_field(field));
quick_sort(result, [](auto const& lhs, auto const& rhs) {
return StringView { lhs.property->as_string() } < StringView { rhs.property->as_string() };
});
return result;
}
template<typename T>
static void set_field_value(CalendarField field, CalendarFields& fields, T&& value)
{
switch (field) {
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
case enumeration: \
if constexpr (IsAssignable<decltype(fields.field_name), RemoveCVReference<T>>) \
fields.field_name = value; \
return;
JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE
}
VERIFY_NOT_REACHED();
}
static void set_default_field_value(CalendarField field, CalendarFields& fields)
{
CalendarFields default_ {};
switch (field) {
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
case enumeration: \
fields.field_name = default_.field_name; \
return;
JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE
}
VERIFY_NOT_REACHED();
}
// 12.1.1 CanonicalizeCalendar ( id ), https://tc39.es/proposal-temporal/#sec-temporal-canonicalizecalendar
ThrowCompletionOr<String> canonicalize_calendar(VM& vm, StringView id)
{
// 1. Let calendars be AvailableCalendars().
auto const& calendars = available_calendars();
// 2. If calendars does not contain the ASCII-lowercase of id, throw a RangeError exception.
for (auto const& calendar : calendars) {
if (calendar.equals_ignoring_ascii_case(id)) {
// 3. Return CanonicalizeUValue("ca", id).
return Unicode::canonicalize_unicode_extension_values("ca"sv, id);
}
}
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarIdentifier, id);
}
// 12.1.2 AvailableCalendars ( ), https://tc39.es/proposal-temporal/#sec-availablecalendars
Vector<String> const& available_calendars()
{
// The implementation-defined abstract operation AvailableCalendars takes no arguments and returns a List of calendar
// types. The returned List is sorted according to lexicographic code unit order, and contains unique calendar types
// in canonical form (12.1) identifying the calendars for which the implementation provides the functionality of
// Intl.DateTimeFormat objects, including their aliases (e.g., either both or neither of "islamicc" and
// "islamic-civil"). The List must include "iso8601".
return Unicode::available_calendars();
}
// 12.2.3 PrepareCalendarFields ( calendar, fields, calendarFieldNames, nonCalendarFieldNames, requiredFieldNames ), https://tc39.es/proposal-temporal/#sec-temporal-preparecalendarfields
ThrowCompletionOr<CalendarFields> prepare_calendar_fields(VM& vm, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names)
{
// 1. Assert: If requiredFieldNames is a List, requiredFieldNames contains zero or one of each of the elements of
// calendarFieldNames and nonCalendarFieldNames.
// 2. Let fieldNames be the list-concatenation of calendarFieldNames and nonCalendarFieldNames.
Vector<CalendarField> field_names;
field_names.append(calendar_field_names.data(), calendar_field_names.size());
field_names.append(non_calendar_field_names.data(), non_calendar_field_names.size());
// 3. Let extraFieldNames be CalendarExtraFields(calendar, calendarFieldNames).
auto extra_field_names = calendar_extra_fields(calendar, calendar_field_names);
// 4. Set fieldNames to the list-concatenation of fieldNames and extraFieldNames.
field_names.extend(move(extra_field_names));
// 5. Assert: fieldNames contains no duplicate elements.
// 6. Let result be a Calendar Fields Record with all fields equal to UNSET.
auto result = CalendarFields::unset();
// 7. Let any be false.
auto any = false;
// 8. Let sortedPropertyNames be a List whose elements are the values in the Property Key column of Table 19
// corresponding to the elements of fieldNames, sorted according to lexicographic code unit order.
auto sorted_property_names = sorted_calendar_fields(vm, field_names);
// 9. For each property name property of sortedPropertyNames, do
for (auto const& [key, property, conversion] : sorted_property_names) {
// a. Let key be the value in the Enumeration Key column of Table 19 corresponding to the row whose Property Key value is property.
// b. Let value be ? Get(fields, property).
auto value = TRY(fields.get(property));
// c. If value is not undefined, then
if (!value.is_undefined()) {
// i. Set any to true.
any = true;
// ii. Let Conversion be the Conversion value of the same row.
switch (conversion) {
// iii. If Conversion is TO-INTEGER-WITH-TRUNCATION, then
case CalendarFieldConversion::ToIntegerWithTruncation:
// 1. Set value to ? ToIntegerWithTruncation(value).
// 2. Set value to 𝔽(value).
set_field_value(key, result, TRY(to_integer_with_truncation(vm, value, ErrorType::TemporalInvalidCalendarFieldName, *property)));
break;
// iv. Else if Conversion is TO-POSITIVE-INTEGER-WITH-TRUNCATION, then
case CalendarFieldConversion::ToPositiveIntegerWithTruncation:
// 1. Set value to ? ToPositiveIntegerWithTruncation(value).
// 2. Set value to 𝔽(value).
set_field_value(key, result, TRY(to_positive_integer_with_truncation(vm, value, ErrorType::TemporalInvalidCalendarFieldName, *property)));
break;
// v. Else if Conversion is TO-STRING, then
case CalendarFieldConversion::ToString:
// 1. Set value to ? ToString(value).
set_field_value(key, result, TRY(value.to_string(vm)));
break;
// vi. Else if Conversion is TO-TEMPORAL-TIME-ZONE-IDENTIFIER, then
case CalendarFieldConversion::ToTemporalTimeZoneIdentifier:
// 1. Set value to ? ToTemporalTimeZoneIdentifier(value).
set_field_value(key, result, TRY(to_temporal_time_zone_identifier(vm, value)));
break;
// vii. Else if Conversion is TO-MONTH-CODE, then
case CalendarFieldConversion::ToMonthCode:
// 1. Set value to ? ToMonthCode(value).
set_field_value(key, result, TRY(to_month_code(vm, value)));
break;
// viii. Else,
case CalendarFieldConversion::ToOffsetString:
// 1. Assert: Conversion is TO-OFFSET-STRING.
// 2. Set value to ? ToOffsetString(value).
set_field_value(key, result, TRY(to_offset_string(vm, value)));
break;
}
// ix. Set result's field whose name is given in the Field Name column of the same row to value.
}
// d. Else if requiredFieldNames is a List, then
else if (auto const* required = required_field_names.get_pointer<CalendarFieldList>()) {
// i. If requiredFieldNames contains key, then
if (required->contains_slow(key)) {
// 1. Throw a TypeError exception.
return vm.throw_completion<TypeError>(ErrorType::MissingRequiredProperty, *property);
}
// ii. Set result's field whose name is given in the Field Name column of the same row to the corresponding
// Default value of the same row.
set_default_field_value(key, result);
}
}
// 10. If requiredFieldNames is PARTIAL and any is false, then
if (required_field_names.has<Partial>() && !any) {
// a. Throw a TypeError exception.
return vm.throw_completion<TypeError>(ErrorType::TemporalObjectMustBePartialTemporalObject);
}
// 11. Return result.
return result;
}
// 12.2.4 CalendarFieldKeysPresent ( fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeyspresent
Vector<CalendarField> calendar_field_keys_present(CalendarFields const& fields)
{
// 1. Let list be « ».
Vector<CalendarField> list;
auto handle_field = [&](auto enumeration_key, auto const& value) {
// a. Let value be fields' field whose name is given in the Field Name column of the row.
// b. Let enumerationKey be the value in the Enumeration Key column of the row.
// c. If value is not unset, append enumerationKey to list.
if (value.has_value())
list.append(enumeration_key);
};
// 2. For each row of Table 19, except the header row, do
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
handle_field(enumeration, fields.field_name);
JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE
// 3. Return list.
return list;
}
// 12.2.5 CalendarMergeFields ( calendar, fields, additionalFields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmergefields
CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields)
{
// 1. Let additionalKeys be CalendarFieldKeysPresent(additionalFields).
auto additional_keys = calendar_field_keys_present(additional_fields);
// 2. Let overriddenKeys be CalendarFieldKeysToIgnore(calendar, additionalKeys).
auto overridden_keys = calendar_field_keys_to_ignore(calendar, additional_keys);
// 3. Let merged be a Calendar Fields Record with all fields set to unset.
auto merged = CalendarFields::unset();
// 4. Let fieldsKeys be CalendarFieldKeysPresent(fields).
auto fields_keys = calendar_field_keys_present(fields);
auto merge_field = [&](auto key, auto& merged_field, auto const& fields_field, auto const& additional_fields_field) {
// a. Let key be the value in the Enumeration Key column of the row.
// b. If fieldsKeys contains key and overriddenKeys does not contain key, then
if (fields_keys.contains_slow(key) && !overridden_keys.contains_slow(key)) {
// i. Let propValue be fields' field whose name is given in the Field Name column of the row.
// ii. Set merged's field whose name is given in the Field Name column of the row to propValue.
merged_field = fields_field;
}
// c. If additionalKeys contains key, then
if (additional_keys.contains_slow(key)) {
// i. Let propValue be additionalFields' field whose name is given in the Field Name column of the row.
// ii. Set merged's field whose name is given in the Field Name column of the row to propValue.
merged_field = additional_fields_field;
}
};
// 5. For each row of Table 19, except the header row, do
#define __JS_ENUMERATE(enumeration, field_name, property_key, conversion) \
merge_field(enumeration, merged.field_name, fields.field_name, additional_fields.field_name);
JS_ENUMERATE_CALENDAR_FIELDS
#undef __JS_ENUMERATE
// 6. Return merged.
return merged;
}
// 12.2.8 ToTemporalCalendarIdentifier ( temporalCalendarLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalcalendaridentifier
ThrowCompletionOr<String> to_temporal_calendar_identifier(VM& vm, Value temporal_calendar_like)
{
// 1. If temporalCalendarLike is an Object, then
if (temporal_calendar_like.is_object()) {
auto const& temporal_calendar_object = temporal_calendar_like.as_object();
// a. If temporalCalendarLike has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]],
// [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]]
// internal slot, then
// i. Return temporalCalendarLike.[[Calendar]].
// FIXME: Add the other calendar-holding types as we define them.
if (is<PlainMonthDay>(temporal_calendar_object))
return static_cast<PlainMonthDay const&>(temporal_calendar_object).calendar();
}
// 2. If temporalCalendarLike is not a String, throw a TypeError exception.
if (!temporal_calendar_like.is_string())
return vm.throw_completion<TypeError>(ErrorType::TemporalInvalidCalendar);
// 3. Let identifier be ? ParseTemporalCalendarString(temporalCalendarLike).
auto identifier = TRY(parse_temporal_calendar_string(vm, temporal_calendar_like.as_string().utf8_string()));
// 4. Return ? CanonicalizeCalendar(identifier).
return TRY(canonicalize_calendar(vm, identifier));
}
// 12.2.9 GetTemporalCalendarIdentifierWithISODefault ( item ), https://tc39.es/proposal-temporal/#sec-temporal-gettemporalcalendarslotvaluewithisodefault
ThrowCompletionOr<String> get_temporal_calendar_identifier_with_iso_default(VM& vm, Object const& item)
{
// 1. If item has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]],
// [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then
// a. Return item.[[Calendar]].
// FIXME: Add the other calendar-holding types as we define them.
if (is<PlainMonthDay>(item))
return static_cast<PlainMonthDay const&>(item).calendar();
// 2. Let calendarLike be ? Get(item, "calendar").
auto calendar_like = TRY(item.get(vm.names.calendar));
// 3. If calendarLike is undefined, then
if (calendar_like.is_undefined()) {
// a. Return "iso8601".
return "iso8601"_string;
}
// 4. Return ? ToTemporalCalendarIdentifier(calendarLike).
return TRY(to_temporal_calendar_identifier(vm, calendar_like));
}
// 12.2.12 CalendarMonthDayFromFields ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdayfromfields
ThrowCompletionOr<ISODate> calendar_month_day_from_fields(VM& vm, StringView calendar, CalendarFields fields, Overflow overflow)
{
// 1. Perform ? CalendarResolveFields(calendar, fields, MONTH-DAY).
TRY(calendar_resolve_fields(vm, calendar, fields, DateType::MonthDay));
// 2. Let result be ? CalendarMonthDayToISOReferenceDate(calendar, fields, overflow).
auto result = TRY(calendar_month_day_to_iso_reference_date(vm, calendar, fields, overflow));
// 3. If ISODateWithinLimits(result) is false, throw a RangeError exception.
if (!iso_date_within_limits(result))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);
// 4. Return result.
return result;
}
// 12.2.13 FormatCalendarAnnotation ( id, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-formatcalendarannotation
String format_calendar_annotation(StringView id, ShowCalendar show_calendar)
{
// 1. If showCalendar is NEVER, return the empty String.
if (show_calendar == ShowCalendar::Never)
return String {};
// 2. If showCalendar is AUTO and id is "iso8601", return the empty String.
if (show_calendar == ShowCalendar::Auto && id == "iso8601"sv)
return String {};
// 3. If showCalendar is CRITICAL, let flag be "!"; else, let flag be the empty String.
auto flag = show_calendar == ShowCalendar::Critical ? "!"sv : ""sv;
// 4. Return the string-concatenation of "[", flag, "u-ca=", id, and "]".
return MUST(String::formatted("[{}u-ca={}]", flag, id));
}
// 12.2.14 CalendarEquals ( one, two ), https://tc39.es/proposal-temporal/#sec-temporal-calendarequals
bool calendar_equals(StringView one, StringView two)
{
// 1. If CanonicalizeUValue("ca", one) is CanonicalizeUValue("ca", two), return true.
// 2. Return false.
return Unicode::canonicalize_unicode_extension_values("ca"sv, one)
== Unicode::canonicalize_unicode_extension_values("ca"sv, two);
}
// 12.2.15 ISODaysInMonth ( year, month ), https://tc39.es/proposal-temporal/#sec-temporal-isodaysinmonth
u8 iso_days_in_month(double year, double month)
{
// 1. If month is 1, 3, 5, 7, 8, 10, or 12, return 31.
if (month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12)
return 31;
// 2. If month is 4, 6, 9, or 11, return 30.
if (month == 4 || month == 6 || month == 9 || month == 11)
return 30;
// 3. Assert: month is 2.
VERIFY(month == 2);
// 4. Return 28 + MathematicalInLeapYear(EpochTimeForYear(year)).
return 28 + mathematical_in_leap_year(epoch_time_for_year(year));
}
// 12.2.16 ISOWeekOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isoweekofyear
YearWeek iso_week_of_year(ISODate const& iso_date)
{
// 1. Let year be isoDate.[[Year]].
auto year = iso_date.year;
// 2. Let wednesday be 3.
static constexpr auto wednesday = 3;
// 3. Let thursday be 4.
static constexpr auto thursday = 4;
// 4. Let friday be 5.
static constexpr auto friday = 5;
// 5. Let saturday be 6.
static constexpr auto saturday = 6;
// 6. Let daysInWeek be 7.
static constexpr auto days_in_week = 7;
// 7. Let maxWeekNumber be 53.
static constexpr auto max_week_number = 53;
// 8. Let dayOfYear be ISODayOfYear(isoDate).
auto day_of_year = iso_day_of_year(iso_date);
// 9. Let dayOfWeek be ISODayOfWeek(isoDate).
auto day_of_week = iso_day_of_week(iso_date);
// 10. Let week be floor((dayOfYear + daysInWeek - dayOfWeek + wednesday) / daysInWeek).
auto week = floor(static_cast<double>(day_of_year + days_in_week - day_of_week + wednesday) / static_cast<double>(days_in_week));
// 11. If week < 1, then
if (week < 1) {
// a. NOTE: This is the last week of the previous year.
// b. Let jan1st be CreateISODateRecord(year, 1, 1).
auto jan1st = create_iso_date_record(year, 1, 1);
// c. Let dayOfJan1st be ISODayOfWeek(jan1st).
auto day_of_jan1st = iso_day_of_week(jan1st);
// d. If dayOfJan1st = friday, then
if (day_of_jan1st == friday) {
// i. Return Year-Week Record { [[Week]]: maxWeekNumber, [[Year]]: year - 1 }.
return { .week = max_week_number, .year = year - 1 };
}
// e. If dayOfJan1st = saturday, and MathematicalInLeapYear(EpochTimeForYear(year - 1)) = 1, then
if (day_of_jan1st == saturday && mathematical_in_leap_year(epoch_time_for_year(year - 1)) == 1) {
// i. Return Year-Week Record { [[Week]]: maxWeekNumber. [[Year]]: year - 1 }.
return { .week = max_week_number, .year = year - 1 };
}
// f. Return Year-Week Record { [[Week]]: maxWeekNumber - 1, [[Year]]: year - 1 }.
return { .week = max_week_number - 1, .year = year - 1 };
}
// 12. If week = maxWeekNumber, then
if (week == max_week_number) {
// a. Let daysInYear be MathematicalDaysInYear(year).
auto days_in_year = mathematical_days_in_year(year);
// b. Let daysLaterInYear be daysInYear - dayOfYear.
auto days_later_in_year = days_in_year - day_of_year;
// c. Let daysAfterThursday be thursday - dayOfWeek.
auto days_after_thursday = thursday - day_of_week;
// d. If daysLaterInYear < daysAfterThursday, then
if (days_later_in_year < days_after_thursday) {
// i. Return Year-Week Record { [[Week]]: 1, [[Year]]: year + 1 }.
return { .week = 1, .year = year + 1 };
}
}
// 13. Return Year-Week Record { [[Week]]: week, [[Year]]: year }.
return { .week = week, .year = year };
}
// 12.2.17 ISODayOfYear ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofyear
u16 iso_day_of_year(ISODate const& iso_date)
{
// 1. Let epochDays be ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]]).
auto epoch_days = iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day);
// 2. Return EpochTimeToDayInYear(EpochDaysToEpochMs(epochDays, 0)) + 1.
return epoch_time_to_day_in_year(epoch_days_to_epoch_ms(epoch_days, 0)) + 1;
}
// 12.2.18 ISODayOfWeek ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodayofweek
u8 iso_day_of_week(ISODate const& iso_date)
{
// 1. Let epochDays be ISODateToEpochDays(isoDate.[[Year]], isoDate.[[Month]] - 1, isoDate.[[Day]]).
auto epoch_days = iso_date_to_epoch_days(iso_date.year, iso_date.month - 1, iso_date.day);
// 2. Let dayOfWeek be EpochTimeToWeekDay(EpochDaysToEpochMs(epochDays, 0)).
auto day_of_week = epoch_time_to_week_day(epoch_days_to_epoch_ms(epoch_days, 0));
// 3. If dayOfWeek = 0, return 7.
if (day_of_week == 0)
return 7;
// 4. Return dayOfWeek.
return day_of_week;
}
// 12.2.20 CalendarMonthDayToISOReferenceDate ( calendar, fields, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-calendarmonthdaytoisoreferencedate
ThrowCompletionOr<ISODate> calendar_month_day_to_iso_reference_date(VM& vm, StringView calendar, CalendarFields const& fields, Overflow overflow)
{
// 1. If calendar is "iso8601", then
if (calendar == "iso8601"sv) {
// a. Assert: fields.[[Month]] and fields.[[Day]] are not UNSET.
VERIFY(fields.month.has_value());
VERIFY(fields.day.has_value());
// b. Let referenceISOYear be 1972 (the first ISO 8601 leap year after the epoch).
static constexpr i32 reference_iso_year = 1972;
// c. If fields.[[Year]] is UNSET, let year be referenceISOYear; else let year be fields.[[Year]].
auto year = !fields.year.has_value() ? reference_iso_year : *fields.year;
// d. Let result be ? RegulateISODate(year, fields.[[Month]], fields.[[Day]], overflow).
auto result = TRY(regulate_iso_date(vm, year, *fields.month, *fields.day, overflow));
// e. Return CreateISODateRecord(referenceISOYear, result.[[Month]], result.[[Day]]).
return create_iso_date_record(reference_iso_year, result.month, result.day);
}
// 2. Return an implementation-defined ISO Date Record, or throw a RangeError exception, as described below.
// FIXME: Create an ISODateRecord based on an ISO8601 calendar for now. See also: CalendarResolveFields.
return calendar_month_day_to_iso_reference_date(vm, "iso8601"sv, fields, overflow);
}
// 12.2.21 CalendarISOToDate ( calendar, isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-calendarisotodate
CalendarDate calendar_iso_to_date(StringView calendar, ISODate const& iso_date)
{
// 1. If calendar is "iso8601", then
if (calendar == "iso8601"sv) {
// a. Let monthNumberPart be ToZeroPaddedDecimalString(isoDate.[[Month]], 2).
// b. Let monthCode be the string-concatenation of "M" and monthNumberPart.
auto month_code = MUST(String::formatted("M{:02}", iso_date.month));
// c. If MathematicalInLeapYear(EpochTimeForYear(isoDate.[[Year]])) = 1, let inLeapYear be true; else let inLeapYear be false.
auto in_leap_year = mathematical_in_leap_year(epoch_time_for_year(iso_date.year)) == 1;
// d. Return Calendar Date Record { [[Era]]: undefined, [[EraYear]]: undefined, [[Year]]: isoDate.[[Year]],
// [[Month]]: isoDate.[[Month]], [[MonthCode]]: monthCode, [[Day]]: isoDate.[[Day]], [[DayOfWeek]]: ISODayOfWeek(isoDate),
// [[DayOfYear]]: ISODayOfYear(isoDate), [[WeekOfYear]]: ISOWeekOfYear(isoDate), [[DaysInWeek]]: 7,
// [[DaysInMonth]]: ISODaysInMonth(isoDate.[[Year]], isoDate.[[Month]]), [[DaysInYear]]: MathematicalDaysInYear(isoDate.[[Year]]),
// [[MonthsInYear]]: 12, [[InLeapYear]]: inLeapYear }.
return CalendarDate {
.era = {},
.era_year = {},
.year = iso_date.year,
.month = iso_date.month,
.month_code = move(month_code),
.day = iso_date.day,
.day_of_week = iso_day_of_week(iso_date),
.day_of_year = iso_day_of_year(iso_date),
.week_of_year = iso_week_of_year(iso_date),
.days_in_week = 7,
.days_in_month = iso_days_in_month(iso_date.year, iso_date.month),
.days_in_year = mathematical_days_in_year(iso_date.year),
.months_in_year = 12,
.in_leap_year = in_leap_year,
};
}
// 2. Return an implementation-defined Calendar Date Record with fields as described in Table 18.
// FIXME: Return an ISO8601 calendar date for now.
return calendar_iso_to_date("iso8601"sv, iso_date);
}
// 12.2.22 CalendarExtraFields ( calendar, fields ), https://tc39.es/proposal-temporal/#sec-temporal-calendarextrafields
Vector<CalendarField> calendar_extra_fields(StringView calendar, CalendarFieldList)
{
// 1. If calendar is "iso8601", return an empty List.
if (calendar == "iso8601"sv)
return {};
// FIXME: 2. Return an implementation-defined List as described above.
return {};
}
// 12.2.23 CalendarFieldKeysToIgnore ( calendar, keys ), https://tc39.es/proposal-temporal/#sec-temporal-calendarfieldkeystoignore
Vector<CalendarField> calendar_field_keys_to_ignore(StringView calendar, ReadonlySpan<CalendarField> keys)
{
// 1. If calendar is "iso8601", then
if (calendar == "iso8601"sv) {
// a. Let ignoredKeys be an empty List.
Vector<CalendarField> ignored_keys;
// b. For each element key of keys, do
for (auto key : keys) {
// i. Append key to ignoredKeys.
ignored_keys.append(key);
// ii. If key is MONTH, append MONTH-CODE to ignoredKeys.
if (key == CalendarField::Month)
ignored_keys.append(CalendarField::MonthCode);
// iii. Else if key is MONTH-CODE, append MONTH to ignoredKeys.
else if (key == CalendarField::MonthCode)
ignored_keys.append(CalendarField::Month);
}
// c. NOTE: While ignoredKeys can have duplicate elements, this is not intended to be meaningful. This specification
// only checks whether particular keys are or are not members of the list.
// d. Return ignoredKeys.
return ignored_keys;
}
// 2. Return an implementation-defined List as described below.
// FIXME: Return keys for an ISO8601 calendar for now.
return calendar_field_keys_to_ignore("iso8601"sv, keys);
}
// 12.2.24 CalendarResolveFields ( calendar, fields, type ), https://tc39.es/proposal-temporal/#sec-temporal-calendarresolvefields
ThrowCompletionOr<void> calendar_resolve_fields(VM& vm, StringView calendar, CalendarFields& fields, DateType type)
{
// 1. If calendar is "iso8601", then
if (calendar == "iso8601"sv) {
// a. If type is DATE or YEAR-MONTH and fields.[[Year]] is UNSET, throw a TypeError exception.
if ((type == DateType::Date || type == DateType::YearMonth) && !fields.year.has_value())
return vm.throw_completion<TypeError>(ErrorType::MissingRequiredProperty, "year"sv);
// b. If type is DATE or MONTH-DAY and fields.[[Day]] is UNSET, throw a TypeError exception.
if ((type == DateType::Date || type == DateType::MonthDay) && !fields.day.has_value())
return vm.throw_completion<TypeError>(ErrorType::MissingRequiredProperty, "day"sv);
// c. Let month be fields.[[Month]].
auto const& month = fields.month;
// d. Let monthCode be fields.[[MonthCode]].
auto const& month_code = fields.month_code;
// e. If monthCode is UNSET, then
if (!month_code.has_value()) {
// i. If month is UNSET, throw a TypeError exception.
if (!month.has_value())
return vm.throw_completion<TypeError>(ErrorType::MissingRequiredProperty, "month"sv);
// ii. Return UNUSED.
return {};
}
// f. Assert: monthCode is a String.
VERIFY(month_code.has_value());
// g. NOTE: The ISO 8601 calendar does not include leap months.
// h. If the length of monthCode is not 3, throw a RangeError exception.
if (month_code->byte_count() != 3)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv);
// i. If the first code unit of monthCode is not 0x004D (LATIN CAPITAL LETTER M), throw a RangeError exception.
if (month_code->bytes_as_string_view()[0] != 'M')
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv);
// j. Let monthCodeDigits be the substring of monthCode from 1.
auto month_code_digits = month_code->bytes_as_string_view().substring_view(1);
// k. If ParseText(StringToCodePoints(monthCodeDigits), DateMonth) is a List of errors, throw a RangeError exception.
if (!parse_iso8601(Production::DateMonth, month_code_digits).has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarFieldName, "monthCode"sv);
// l. Let monthCodeInteger be (StringToNumber(monthCodeDigits)).
auto month_code_integer = month_code_digits.to_number<u8>().value();
// m. If month is not UNSET and month ≠ monthCodeInteger, throw a RangeError exception.
if (month.has_value() && month != month_code_integer)
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidCalendarFieldName, "month"sv);
// n. Set fields.[[Month]] to monthCodeInteger.
fields.month = month_code_integer;
}
// 2. Else,
else {
// a. Perform implementation-defined processing to mutate fields, or throw a TypeError or RangeError exception, as described below.
// FIXME: Resolve fields as an ISO8601 calendar for now. See also: CalendarMonthDayToISOReferenceDate.
TRY(calendar_resolve_fields(vm, "iso8601"sv, fields, type));
}
// 3. Return UNUSED.
return {};
}
}

View file

@ -0,0 +1,119 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2023-2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibJS/Forward.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
namespace JS::Temporal {
// 12.2.1 Calendar Date Records, https://tc39.es/proposal-temporal/#sec-temporal-calendar-date-records
struct CalendarDate {
Optional<String> era;
Optional<i32> era_year;
i32 year { 0 };
u8 month { 0 };
String month_code;
u8 day { 0 };
u8 day_of_week { 0 };
u16 day_of_year { 0 };
YearWeek week_of_year;
u8 days_in_week { 0 };
u8 days_in_month { 0 };
u16 days_in_year { 0 };
u8 months_in_year { 0 };
bool in_leap_year { false };
};
// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields
enum class CalendarField {
Era,
EraYear,
Year,
Month,
MonthCode,
Day,
Hour,
Minute,
Second,
Millisecond,
Microsecond,
Nanosecond,
Offset,
TimeZone,
};
// https://tc39.es/proposal-temporal/#table-temporal-calendar-fields-record-fields
struct CalendarFields {
static CalendarFields unset()
{
return {
.era = {},
.era_year = {},
.year = {},
.month = {},
.month_code = {},
.day = {},
.hour = {},
.minute = {},
.second = {},
.millisecond = {},
.microsecond = {},
.nanosecond = {},
.offset = {},
.time_zone = {},
};
}
Optional<String> era;
Optional<i32> era_year;
Optional<i32> year;
Optional<u32> month;
Optional<String> month_code;
Optional<u32> day;
Optional<u8> hour { 0 };
Optional<u8> minute { 0 };
Optional<u8> second { 0 };
Optional<u16> millisecond { 0 };
Optional<u16> microsecond { 0 };
Optional<u16> nanosecond { 0 };
Optional<String> offset;
Optional<String> time_zone;
};
struct Partial { };
using CalendarFieldList = ReadonlySpan<CalendarField>;
using CalendarFieldListOrPartial = Variant<Partial, CalendarFieldList>;
ThrowCompletionOr<String> canonicalize_calendar(VM&, StringView id);
Vector<String> const& available_calendars();
ThrowCompletionOr<CalendarFields> prepare_calendar_fields(VM&, StringView calendar, Object const& fields, CalendarFieldList calendar_field_names, CalendarFieldList non_calendar_field_names, CalendarFieldListOrPartial required_field_names);
ThrowCompletionOr<ISODate> calendar_month_day_from_fields(VM&, StringView calendar, CalendarFields, Overflow);
String format_calendar_annotation(StringView id, ShowCalendar);
bool calendar_equals(StringView one, StringView two);
u8 iso_days_in_month(double year, double month);
YearWeek iso_week_of_year(ISODate const&);
u16 iso_day_of_year(ISODate const&);
u8 iso_day_of_week(ISODate const&);
Vector<CalendarField> calendar_field_keys_present(CalendarFields const&);
CalendarFields calendar_merge_fields(StringView calendar, CalendarFields const& fields, CalendarFields const& additional_fields);
ThrowCompletionOr<String> to_temporal_calendar_identifier(VM&, Value temporal_calendar_like);
ThrowCompletionOr<String> get_temporal_calendar_identifier_with_iso_default(VM&, Object const& item);
ThrowCompletionOr<ISODate> calendar_month_day_to_iso_reference_date(VM&, StringView calendar, CalendarFields const&, Overflow);
CalendarDate calendar_iso_to_date(StringView calendar, ISODate const&);
Vector<CalendarField> calendar_extra_fields(StringView calendar, CalendarFieldList);
Vector<CalendarField> calendar_field_keys_to_ignore(StringView calendar, ReadonlySpan<CalendarField>);
ThrowCompletionOr<void> calendar_resolve_fields(VM&, StringView calendar, CalendarFields&, DateType);
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Date.h>
#include <LibJS/Runtime/Temporal/DateEquations.h>
namespace JS::Temporal {
// https://tc39.es/proposal-temporal/#eqn-mathematicaldaysinyear
u16 mathematical_days_in_year(i32 year)
{
// MathematicalDaysInYear(y)
// = 365 if ((y) modulo 4) ≠ 0
// = 366 if ((y) modulo 4) = 0 and ((y) modulo 100) ≠ 0
// = 365 if ((y) modulo 100) = 0 and ((y) modulo 400) ≠ 0
// = 366 if ((y) modulo 400) = 0
if (modulo(year, 4) != 0)
return 365;
if (modulo(year, 4) == 0 && modulo(year, 100) != 0)
return 366;
if (modulo(year, 100) == 0 && modulo(year, 400) != 0)
return 365;
if (modulo(year, 400) == 0)
return 366;
VERIFY_NOT_REACHED();
}
// https://tc39.es/proposal-temporal/#eqn-mathematicalinleapyear
u8 mathematical_in_leap_year(double time)
{
// MathematicalInLeapYear(t)
// = 0 if MathematicalDaysInYear(EpochTimeToEpochYear(t)) = 365
// = 1 if MathematicalDaysInYear(EpochTimeToEpochYear(t)) = 366
auto days_in_year = mathematical_days_in_year(epoch_time_to_epoch_year(time));
if (days_in_year == 365)
return 0;
if (days_in_year == 366)
return 1;
VERIFY_NOT_REACHED();
}
// https://tc39.es/proposal-temporal/#eqn-EpochTimeToDayNumber
double epoch_time_to_day_number(double time)
{
// EpochTimeToDayNumber(t) = floor(t / (msPerDay))
return floor(time / JS::ms_per_day);
}
// https://tc39.es/proposal-temporal/#eqn-epochdaynumberforyear
double epoch_day_number_for_year(double year)
{
// EpochDayNumberForYear(y) = 365 × (y - 1970) + floor((y - 1969) / 4) - floor((y - 1901) / 100) + floor((y - 1601) / 400)
return 365.0 * (year - 1970.0) + floor((year - 1969.0) / 4.0) - floor((year - 1901.0) / 100.0) + floor((year - 1601.0) / 400.0);
}
// https://tc39.es/proposal-temporal/#eqn-epochtimeforyear
double epoch_time_for_year(double year)
{
// EpochTimeForYear(y) = (msPerDay) × EpochDayNumberForYear(y)
return ms_per_day * epoch_day_number_for_year(year);
}
// https://tc39.es/proposal-temporal/#eqn-epochtimetoepochyear
i32 epoch_time_to_epoch_year(double time)
{
// EpochTimeToEpochYear(t) = the largest integral Number y (closest to +∞) such that EpochTimeForYear(y) ≤ t
return JS::year_from_time(time);
}
// https://tc39.es/proposal-temporal/#eqn-epochtimetodayinyear
u16 epoch_time_to_day_in_year(double time)
{
// EpochTimeToDayInYear(t) = EpochTimeToDayNumber(t) - EpochDayNumberForYear(EpochTimeToEpochYear(t))
return static_cast<u16>(epoch_time_to_day_number(time) - epoch_day_number_for_year(epoch_time_to_epoch_year(time)));
}
// https://tc39.es/proposal-temporal/#eqn-epochtimetoweekday
u8 epoch_time_to_week_day(double time)
{
// EpochTimeToWeekDay(t) = (EpochTimeToDayNumber(t) + 4) modulo 7
return static_cast<u8>(modulo(epoch_time_to_day_number(time) + 4, 7.0));
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
namespace JS::Temporal {
// 13.3 Date Equations, https://tc39.es/proposal-temporal/#sec-date-equations
u16 mathematical_days_in_year(i32 year);
u8 mathematical_in_leap_year(double time);
double epoch_time_to_day_number(double time);
double epoch_day_number_for_year(double year);
double epoch_time_for_year(double year);
i32 epoch_time_to_epoch_year(double time);
u16 epoch_time_to_day_in_year(double time);
u8 epoch_time_to_week_day(double time);
}

File diff suppressed because it is too large Load diff

View file

@ -9,11 +9,42 @@
#include <AK/Optional.h>
#include <AK/StringView.h>
#include <AK/Vector.h>
namespace JS::Temporal {
struct Annotation {
bool critical { false };
StringView key;
StringView value;
};
struct TimeZoneOffset {
Optional<char> sign;
Optional<StringView> hours;
Optional<StringView> minutes;
Optional<StringView> seconds;
Optional<StringView> fraction;
StringView source_text;
};
struct ParseResult {
Optional<char> sign;
Optional<StringView> date_year;
Optional<StringView> date_month;
Optional<StringView> date_day;
Optional<StringView> time_hour;
Optional<StringView> time_minute;
Optional<StringView> time_second;
Optional<StringView> time_fraction;
Optional<TimeZoneOffset> date_time_offset;
Optional<StringView> utc_designator;
Optional<StringView> time_zone_identifier;
Optional<StringView> time_zone_iana_name;
Optional<TimeZoneOffset> time_zone_offset;
Optional<StringView> duration_years;
Optional<StringView> duration_months;
Optional<StringView> duration_weeks;
@ -24,12 +55,30 @@ struct ParseResult {
Optional<StringView> duration_minutes_fraction;
Optional<StringView> duration_seconds;
Optional<StringView> duration_seconds_fraction;
Vector<Annotation> annotations;
};
enum class Production {
AnnotationValue,
DateMonth,
TemporalDateTimeString,
TemporalDurationString,
TemporalInstantString,
TemporalMonthDayString,
TemporalTimeString,
TemporalYearMonthString,
TemporalZonedDateTimeString,
TimeZoneIdentifier,
};
Optional<ParseResult> parse_iso8601(Production, StringView);
enum class SubMinutePrecision {
No,
Yes,
};
Optional<TimeZoneOffset> parse_utc_offset(StringView, SubMinutePrecision);
}

View file

@ -0,0 +1,145 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Checked.h>
#include <LibJS/Runtime/Temporal/Calendar.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainDateTime.h>
#include <LibJS/Runtime/Temporal/PlainTime.h>
namespace JS::Temporal {
// 3.5.2 CreateISODateRecord ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-create-iso-date-record
ISODate create_iso_date_record(double year, double month, double day)
{
// 1. Assert: IsValidISODate(year, month, day) is true.
VERIFY(is_valid_iso_date(year, month, day));
// 2. Return ISO Date Record { [[Year]]: year, [[Month]]: month, [[Day]]: day }.
return { .year = static_cast<i32>(year), .month = static_cast<u8>(month), .day = static_cast<u8>(day) };
}
// 3.5.6 RegulateISODate ( year, month, day, overflow ), https://tc39.es/proposal-temporal/#sec-temporal-regulateisodate
ThrowCompletionOr<ISODate> regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow)
{
switch (overflow) {
// 1. If overflow is CONSTRAIN, then
case Overflow::Constrain:
// a. Set month to the result of clamping month between 1 and 12.
month = clamp(month, 1, 12);
// b. Let daysInMonth be ISODaysInMonth(year, month).
// c. Set day to the result of clamping day between 1 and daysInMonth.
day = clamp(day, 1, iso_days_in_month(year, month));
// AD-HOC: We perform the IsValidISODate check even for CONSTRAIN to ensure the year may be treated as a normal
// integer. See the note in IsValidISODate for more details.
[[fallthrough]];
// 2. Else,
case Overflow::Reject:
// a. Assert: overflow is REJECT.
// b. If IsValidISODate(year, month, day) is false, throw a RangeError exception.
if (!is_valid_iso_date(year, month, day))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidISODate);
break;
}
// 3. Return CreateISODateRecord(year, month, day).
return create_iso_date_record(year, month, day);
}
// 3.5.7 IsValidISODate ( year, month, day ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidisodate
bool is_valid_iso_date(double year, double month, double day)
{
// AD-HOC: This is an optimization that allows us to treat these doubles as normal integers from this point onwards.
// This does not change the exposed behavior as the call to CreateISODateRecord will immediately check that
// these values are valid ISO values (for years: -273975 - 273975, for months: 1 - 12, for days: 1 - 31),
// all of which are subsets of this check.
if (!AK::is_within_range<i32>(year) || !AK::is_within_range<u8>(month) || !AK::is_within_range<u8>(day))
return false;
// 1. If month < 1 or month > 12, then
if (month < 1 || month > 12) {
// a. Return false.
return false;
}
// 2. Let daysInMonth be ISODaysInMonth(year, month).
auto days_in_month = iso_days_in_month(year, month);
// 3. If day < 1 or day > daysInMonth, then
if (day < 1 || day > days_in_month) {
// a. Return false.
return false;
}
// 4. Return true.
return true;
}
// 3.5.9 PadISOYear ( y ), https://tc39.es/proposal-temporal/#sec-temporal-padisoyear
String pad_iso_year(i32 year)
{
// 1. If y ≥ 0 and y ≤ 9999, then
if (year >= 0 && year <= 9999) {
// a. Return ToZeroPaddedDecimalString(y, 4).
return MUST(String::formatted("{:04}", year));
}
// 2. If y > 0, let yearSign be "+"; otherwise, let yearSign be "-".
auto year_sign = year > 0 ? '+' : '-';
// 3. Let year be ToZeroPaddedDecimalString(abs(y), 6).
// 4. Return the string-concatenation of yearSign and year.
return MUST(String::formatted("{}{:06}", year_sign, abs(year)));
}
// 3.5.11 ISODateWithinLimits ( isoDate ), https://tc39.es/proposal-temporal/#sec-temporal-isodatewithinlimits
bool iso_date_within_limits(ISODate iso_date)
{
// 1. Let isoDateTime be CombineISODateAndTimeRecord(isoDate, NoonTimeRecord()).
auto iso_date_time = combine_iso_date_and_time_record(iso_date, noon_time_record());
// 2. Return ISODateTimeWithinLimits(isoDateTime).
return iso_date_time_within_limits(iso_date_time);
}
// 3.5.12 CompareISODate ( isoDate1, isoDate2 ), https://tc39.es/proposal-temporal/#sec-temporal-compareisodate
i8 compare_iso_date(ISODate iso_date1, ISODate iso_date2)
{
// 1. If isoDate1.[[Year]] > isoDate2.[[Year]], return 1.
if (iso_date1.year > iso_date2.year)
return 1;
// 2. If isoDate1.[[Year]] < isoDate2.[[Year]], return -1.
if (iso_date1.year < iso_date2.year)
return -1;
// 3. If isoDate1.[[Month]] > isoDate2.[[Month]], return 1.
if (iso_date1.month > iso_date2.month)
return 1;
// 4. If isoDate1.[[Month]] < isoDate2.[[Month]], return -1.
if (iso_date1.month < iso_date2.month)
return -1;
// 5. If isoDate1.[[Day]] > isoDate2.[[Day]], return 1.
if (iso_date1.day > iso_date2.day)
return 1;
// 6. If isoDate1.[[Day]] < isoDate2.[[Day]], return -1.
if (iso_date1.day < iso_date2.day)
return -1;
// 7. Return 0.
return 0;
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
namespace JS::Temporal {
// 3.5.1 ISO Date Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-records
struct ISODate {
i32 year { 0 };
u8 month { 0 };
u8 day { 0 };
};
ISODate create_iso_date_record(double year, double month, double day);
ThrowCompletionOr<ISODate> regulate_iso_date(VM& vm, double year, double month, double day, Overflow overflow);
bool is_valid_iso_date(double year, double month, double day);
String pad_iso_year(i32 year);
bool iso_date_within_limits(ISODate);
i8 compare_iso_date(ISODate, ISODate);
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/Instant.h>
#include <LibJS/Runtime/Temporal/PlainDateTime.h>
namespace JS::Temporal {
// 5.5.3 CombineISODateAndTimeRecord ( isoDate, time ), https://tc39.es/proposal-temporal/#sec-temporal-combineisodateandtimerecord
ISODateTime combine_iso_date_and_time_record(ISODate iso_date, Time time)
{
// 1. NOTE: time.[[Days]] is ignored.
// 2. Return ISO Date-Time Record { [[ISODate]]: isoDate, [[Time]]: time }.
return { .iso_date = iso_date, .time = time };
}
// nsMinInstant - nsPerDay
static auto const DATETIME_NANOSECONDS_MIN = "-8640000086400000000000"_sbigint;
// nsMaxInstant + nsPerDay
static auto const DATETIME_NANOSECONDS_MAX = "8640000086400000000000"_sbigint;
// 5.5.4 ISODateTimeWithinLimits ( isoDateTime ), https://tc39.es/proposal-temporal/#sec-temporal-isodatetimewithinlimits
bool iso_date_time_within_limits(ISODateTime iso_date_time)
{
// 1. If abs(ISODateToEpochDays(isoDateTime.[[ISODate]].[[Year]], isoDateTime.[[ISODate]].[[Month]] - 1, isoDateTime.[[ISODate]].[[Day]])) > 10**8 + 1, return false.
if (fabs(iso_date_to_epoch_days(iso_date_time.iso_date.year, iso_date_time.iso_date.month - 1, iso_date_time.iso_date.day)) > 100000001)
return false;
// 2. Let ns be (GetUTCEpochNanoseconds(isoDateTime)).
auto nanoseconds = get_utc_epoch_nanoseconds(iso_date_time);
// 3. If ns ≤ nsMinInstant - nsPerDay, then
if (nanoseconds <= DATETIME_NANOSECONDS_MIN) {
// a. Return false.
return false;
}
// 4. If ns ≥ nsMaxInstant + nsPerDay, then
if (nanoseconds >= DATETIME_NANOSECONDS_MAX) {
// a. Return false.
return false;
}
// 5. Return true.
return true;
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainTime.h>
namespace JS::Temporal {
// 5.5.1 ISO Date-Time Records, https://tc39.es/proposal-temporal/#sec-temporal-iso-date-time-records
struct ISODateTime {
ISODate iso_date;
Time time;
};
ISODateTime combine_iso_date_and_time_record(ISODate, Time);
bool iso_date_time_within_limits(ISODateTime);
}

View file

@ -0,0 +1,170 @@
/*
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2021, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Intrinsics.h>
#include <LibJS/Runtime/Realm.h>
#include <LibJS/Runtime/Temporal/Calendar.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
#include <LibJS/Runtime/Temporal/PlainMonthDayConstructor.h>
#include <LibJS/Runtime/VM.h>
namespace JS::Temporal {
GC_DEFINE_ALLOCATOR(PlainMonthDay);
// 10 Temporal.PlainMonthDay Objects, https://tc39.es/proposal-temporal/#sec-temporal-plainmonthday-objects
PlainMonthDay::PlainMonthDay(ISODate iso_date, String calendar, Object& prototype)
: Object(ConstructWithPrototypeTag::Tag, prototype)
, m_iso_date(iso_date)
, m_calendar(move(calendar))
{
}
// 10.5.1 ToTemporalMonthDay ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal-totemporalmonthday
ThrowCompletionOr<GC::Ref<PlainMonthDay>> to_temporal_month_day(VM& vm, Value item, Value options)
{
// 1. If options is not present, set options to undefined.
// 2. If item is a Object, then
if (item.is_object()) {
auto const& object = item.as_object();
// a. If item has an [[InitializedTemporalMonthDay]] internal slot, then
if (is<PlainMonthDay>(object)) {
auto const& plain_month_day = static_cast<PlainMonthDay const&>(object);
// i. Let resolvedOptions be ? GetOptionsObject(options).
auto resolved_options = TRY(get_options_object(vm, options));
// ii. Perform ? GetTemporalOverflowOption(resolvedOptions).
TRY(get_temporal_overflow_option(vm, resolved_options));
// iii. Return ! CreateTemporalMonthDay(item.[[ISODate]], item.[[Calendar]]).
return MUST(create_temporal_month_day(vm, plain_month_day.iso_date(), plain_month_day.calendar()));
}
// b. Let calendar be ? GetTemporalCalendarIdentifierWithISODefault(item).
auto calendar = TRY(get_temporal_calendar_identifier_with_iso_default(vm, object));
// c. Let fields be ? PrepareCalendarFields(calendar, item, « YEAR, MONTH, MONTH-CODE, DAY », «», «»).
auto fields = TRY(prepare_calendar_fields(vm, calendar, object, { { CalendarField::Year, CalendarField::Month, CalendarField::MonthCode, CalendarField::Day } }, {}, CalendarFieldList {}));
// d. Let resolvedOptions be ? GetOptionsObject(options).
auto resolved_options = TRY(get_options_object(vm, options));
// e. Let overflow be ? GetTemporalOverflowOption(resolvedOptions).
auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options));
// f. Let isoDate be ? CalendarMonthDayFromFields(calendar, fields, overflow).
auto iso_date = TRY(calendar_month_day_from_fields(vm, calendar, move(fields), overflow));
// g. Return ! CreateTemporalMonthDay(isoDate, calendar).
return MUST(create_temporal_month_day(vm, iso_date, move(calendar)));
}
// 3. If item is not a String, throw a TypeError exception.
if (!item.is_string())
return vm.throw_completion<TypeError>(ErrorType::TemporalInvalidPlainMonthDay);
// 4. Let result be ? ParseISODateTime(item, « TemporalMonthDayString »).
auto parse_result = TRY(parse_iso_date_time(vm, item.as_string().utf8_string_view(), { { Production::TemporalMonthDayString } }));
// 5. Let calendar be result.[[Calendar]].
// 6. If calendar is empty, set calendar to "iso8601".
auto calendar = parse_result.calendar.value_or("iso8601"_string);
// 7. Set calendar to ? CanonicalizeCalendar(calendar).
calendar = TRY(canonicalize_calendar(vm, calendar));
// 8. Let resolvedOptions be ? GetOptionsObject(options).
auto resolved_options = TRY(get_options_object(vm, options));
// 9. Perform ? GetTemporalOverflowOption(resolvedOptions).
TRY(get_temporal_overflow_option(vm, resolved_options));
// 10. If result.[[Year]] is empty, then
if (!parse_result.year.has_value()) {
// a. Assert: calendar is "iso8601".
VERIFY(calendar == "iso8601"sv);
// b. Let referenceISOYear be 1972 (the first ISO 8601 leap year after the epoch).
static constexpr i32 reference_iso_year = 1972;
// c. Let isoDate be CreateISODateRecord(referenceISOYear, result.[[Month]], result.[[Day]]).
auto iso_date = create_iso_date_record(reference_iso_year, parse_result.month, parse_result.day);
// d. Return ! CreateTemporalMonthDay(isoDate, calendar).
return MUST(create_temporal_month_day(vm, iso_date, move(calendar)));
}
// 11. Let isoDate be CreateISODateRecord(result.[[Year]], result.[[Month]], result.[[Day]]).
auto iso_date = create_iso_date_record(*parse_result.year, parse_result.month, parse_result.day);
// 12. Set result to ISODateToFields(calendar, isoDate, MONTH-DAY).
auto result = iso_date_to_fields(calendar, iso_date, DateType::MonthDay);
// 13. NOTE: The following operation is called with CONSTRAIN regardless of the value of overflow, in order for the
// calendar to store a canonical value in the [[Year]] field of the [[ISODate]] internal slot of the result.
// 14. Set isoDate to ? CalendarMonthDayFromFields(calendar, result, CONSTRAIN).
iso_date = TRY(calendar_month_day_from_fields(vm, calendar, result, Overflow::Constrain));
// 15. Return ! CreateTemporalMonthDay(isoDate, calendar).
return MUST(create_temporal_month_day(vm, iso_date, move(calendar)));
}
// 10.5.2 CreateTemporalMonthDay ( isoDate, calendar [ , newTarget ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtemporalmonthday
ThrowCompletionOr<GC::Ref<PlainMonthDay>> create_temporal_month_day(VM& vm, ISODate iso_date, String calendar, GC::Ptr<FunctionObject> new_target)
{
auto& realm = *vm.current_realm();
// 1. If ISODateWithinLimits(isoDate) is false, throw a RangeError exception.
if (!iso_date_within_limits(iso_date))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidPlainMonthDay);
// 2. If newTarget is not present, set newTarget to %Temporal.PlainMonthDay%.
if (!new_target)
new_target = realm.intrinsics().temporal_plain_month_day_constructor();
// 3. Let object be ? OrdinaryCreateFromConstructor(newTarget, "%Temporal.PlainMonthDay.prototype%", « [[InitializedTemporalMonthDay]], [[ISODate]], [[Calendar]] »).
// 4. Set object.[[ISODate]] to isoDate.
// 5. Set object.[[Calendar]] to calendar.
auto object = TRY(ordinary_create_from_constructor<PlainMonthDay>(vm, *new_target, &Intrinsics::temporal_plain_month_day_prototype, iso_date, move(calendar)));
// 6. Return object.
return object;
}
// 10.5.3 TemporalMonthDayToString ( monthDay, showCalendar ), https://tc39.es/proposal-temporal/#sec-temporal-temporalmonthdaytostring
String temporal_month_day_to_string(PlainMonthDay const& month_day, ShowCalendar show_calendar)
{
// 1. Let month be ToZeroPaddedDecimalString(monthDay.[[ISODate]].[[Month]], 2).
// 2. Let day be ToZeroPaddedDecimalString(monthDay.[[ISODate]].[[Day]], 2).
// 3. Let result be the string-concatenation of month, the code unit 0x002D (HYPHEN-MINUS), and day.
auto result = MUST(String::formatted("{:02}-{:02}", month_day.iso_date().month, month_day.iso_date().day));
// 4. If showCalendar is one of ALWAYS or CRITICAL, or if monthDay.[[Calendar]] is not "iso8601", then
if (show_calendar == ShowCalendar::Always || show_calendar == ShowCalendar::Critical || month_day.calendar() != "iso8601"sv) {
// a. Let year be PadISOYear(monthDay.[[ISODate]].[[Year]]).
auto year = pad_iso_year(month_day.iso_date().year);
// b. Set result to the string-concatenation of year, the code unit 0x002D (HYPHEN-MINUS), and result.
result = MUST(String::formatted("{}-{}", year, result));
}
// 5. Let calendarString be FormatCalendarAnnotation(monthDay.[[Calendar]], showCalendar).
auto calendar_string = format_calendar_annotation(month_day.calendar(), show_calendar);
// 6. Set result to the string-concatenation of result and calendarString.
result = MUST(String::formatted("{}{}", result, calendar_string));
// 7. Return result.
return result;
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Object.h>
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
namespace JS::Temporal {
class PlainMonthDay final : public Object {
JS_OBJECT(PlainMonthDay, Object);
GC_DECLARE_ALLOCATOR(PlainMonthDay);
public:
virtual ~PlainMonthDay() override = default;
[[nodiscard]] ISODate iso_date() const { return m_iso_date; }
[[nodiscard]] String const& calendar() const { return m_calendar; }
private:
PlainMonthDay(ISODate, String calendar, Object& prototype);
ISODate m_iso_date; // [[ISODate]]
String m_calendar; // [[Calendar]]
};
ThrowCompletionOr<GC::Ref<PlainMonthDay>> to_temporal_month_day(VM&, Value item, Value options = js_undefined());
ThrowCompletionOr<GC::Ref<PlainMonthDay>> create_temporal_month_day(VM&, ISODate, String calendar, GC::Ptr<FunctionObject> new_target = {});
String temporal_month_day_to_string(PlainMonthDay const&, ShowCalendar);
}

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/Calendar.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
#include <LibJS/Runtime/Temporal/PlainMonthDayConstructor.h>
namespace JS::Temporal {
GC_DEFINE_ALLOCATOR(PlainMonthDayConstructor);
// 10.1 The Temporal.PlainMonthDay Constructor, https://tc39.es/proposal-temporal/#sec-temporal-plainmonthday-constructor
PlainMonthDayConstructor::PlainMonthDayConstructor(Realm& realm)
: NativeFunction(realm.vm().names.PlainMonthDay.as_string(), realm.intrinsics().function_prototype())
{
}
void PlainMonthDayConstructor::initialize(Realm& realm)
{
Base::initialize(realm);
auto& vm = this->vm();
// 10.2.1 Temporal.PlainMonthDay.prototype, https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype
define_direct_property(vm.names.prototype, realm.intrinsics().temporal_plain_month_day_prototype(), 0);
define_direct_property(vm.names.length, Value(2), Attribute::Configurable);
u8 attr = Attribute::Writable | Attribute::Configurable;
define_native_function(realm, vm.names.from, from, 1, attr);
}
// 10.1.1 Temporal.PlainMonthDay ( isoMonth, isoDay [ , calendar [ , referenceISOYear ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday
ThrowCompletionOr<Value> PlainMonthDayConstructor::call()
{
auto& vm = this->vm();
// 1. If NewTarget is undefined, throw a TypeError exception.
return vm.throw_completion<TypeError>(ErrorType::ConstructorWithoutNew, "Temporal.PlainMonthDay");
}
// 10.1.1 Temporal.PlainMonthDay ( isoMonth, isoDay [ , calendar [ , referenceISOYear ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday
ThrowCompletionOr<GC::Ref<Object>> PlainMonthDayConstructor::construct(FunctionObject& new_target)
{
auto& vm = this->vm();
auto iso_month = vm.argument(0);
auto iso_day = vm.argument(1);
auto calendar_value = vm.argument(2);
auto reference_iso_year = vm.argument(3);
// 2. If referenceISOYear is undefined, then
if (reference_iso_year.is_undefined()) {
// a. Set referenceISOYear to 1972𝔽 (the first ISO 8601 leap year after the epoch).
reference_iso_year = Value { 1972 };
}
// 3. Let m be ? ToIntegerWithTruncation(isoMonth).
auto month = TRY(to_integer_with_truncation(vm, iso_month, ErrorType::TemporalInvalidPlainMonthDay));
// 4. Let d be ? ToIntegerWithTruncation(isoDay).
auto day = TRY(to_integer_with_truncation(vm, iso_day, ErrorType::TemporalInvalidPlainMonthDay));
// 5. If calendar is undefined, set calendar to "iso8601".
if (calendar_value.is_undefined())
calendar_value = PrimitiveString::create(vm, "iso8601"_string);
// 6. If calendar is not a String, throw a TypeError exception.
if (!calendar_value.is_string())
return vm.throw_completion<TypeError>(ErrorType::NotAString, calendar_value);
// 7. Set calendar to ? CanonicalizeCalendar(calendar).
auto calendar = TRY(canonicalize_calendar(vm, calendar_value.as_string().utf8_string_view()));
// 8. Let y be ? ToIntegerWithTruncation(referenceISOYear).
auto year = TRY(to_integer_with_truncation(vm, reference_iso_year, ErrorType::TemporalInvalidPlainMonthDay));
// 9. If IsValidISODate(y, m, d) is false, throw a RangeError exception.
if (!is_valid_iso_date(year, month, day))
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidPlainMonthDay);
// 10. Let isoDate be CreateISODateRecord(y, m, d).
auto iso_date = create_iso_date_record(year, month, day);
// 11. Return ? CreateTemporalMonthDay(isoDate, calendar, NewTarget).
return TRY(create_temporal_month_day(vm, iso_date, move(calendar), &new_target));
}
// 10.2.2 Temporal.PlainMonthDay.from ( item [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.from
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayConstructor::from)
{
// 1. Return ? ToTemporalMonthDay(item, options).
return TRY(to_temporal_month_day(vm, vm.argument(0), vm.argument(1)));
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2021-2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/NativeFunction.h>
namespace JS::Temporal {
class PlainMonthDayConstructor final : public NativeFunction {
JS_OBJECT(PlainMonthDayConstructor, NativeFunction);
GC_DECLARE_ALLOCATOR(PlainMonthDayConstructor);
public:
virtual void initialize(Realm&) override;
virtual ~PlainMonthDayConstructor() override = default;
virtual ThrowCompletionOr<Value> call() override;
virtual ThrowCompletionOr<GC::Ref<Object>> construct(FunctionObject& new_target) override;
private:
explicit PlainMonthDayConstructor(Realm&);
virtual bool has_constructor() const override { return true; }
JS_DECLARE_NATIVE_FUNCTION(from);
};
}

View file

@ -0,0 +1,183 @@
/*
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Temporal/Calendar.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
#include <LibJS/Runtime/Temporal/PlainMonthDayPrototype.h>
#include <LibJS/Runtime/VM.h>
namespace JS::Temporal {
GC_DEFINE_ALLOCATOR(PlainMonthDayPrototype);
// 10.3 Properties of the Temporal.PlainMonthDay Prototype Object, https://tc39.es/proposal-temporal/#sec-properties-of-the-temporal-plainmonthday-prototype-object
PlainMonthDayPrototype::PlainMonthDayPrototype(Realm& realm)
: PrototypeObject(realm.intrinsics().object_prototype())
{
}
void PlainMonthDayPrototype::initialize(Realm& realm)
{
Base::initialize(realm);
auto& vm = this->vm();
// 10.3.2 Temporal.PlainMonthDay.prototype[ %Symbol.toStringTag% ], https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype-%symbol.tostringtag%
define_direct_property(vm.well_known_symbol_to_string_tag(), PrimitiveString::create(vm, "Temporal.PlainMonthDay"_string), Attribute::Configurable);
define_native_accessor(realm, vm.names.calendarId, calendar_id_getter, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.monthCode, month_code_getter, {}, Attribute::Configurable);
define_native_accessor(realm, vm.names.day, day_getter, {}, Attribute::Configurable);
u8 attr = Attribute::Writable | Attribute::Configurable;
define_native_function(realm, vm.names.with, with, 1, attr);
define_native_function(realm, vm.names.equals, equals, 1, attr);
define_native_function(realm, vm.names.toString, to_string, 0, attr);
define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr);
define_native_function(realm, vm.names.toJSON, to_json, 0, attr);
define_native_function(realm, vm.names.valueOf, value_of, 0, attr);
}
// 10.3.3 get Temporal.PlainMonthDay.prototype.calendarId, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.calendarid
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::calendar_id_getter)
{
// 1. Let monthDay be the this value
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
auto month_day = TRY(typed_this_object(vm));
// 3. Return monthDay.[[Calendar]].
return PrimitiveString::create(vm, month_day->calendar());
}
// 10.3.4 get Temporal.PlainMonthDay.prototype.monthCode, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.monthcode
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::month_code_getter)
{
// 1. Let monthDay be the this value
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
auto month_day = TRY(typed_this_object(vm));
// 3. Return CalendarISOToDate(monthDay.[[Calendar]], monthDay.[[ISODate]]).[[MonthCode]].
return PrimitiveString::create(vm, calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).month_code);
}
// 10.3.5 get Temporal.PlainMonthDay.prototype.day, https://tc39.es/proposal-temporal/#sec-get-temporal.plainmonthday.prototype.day
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::day_getter)
{
// 1. Let monthDay be the this value.
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
auto month_day = TRY(typed_this_object(vm));
// 3. Return 𝔽(CalendarISOToDate(monthDay.[[Calendar]], monthDay.[[ISODate]]).[[Day]]).
return calendar_iso_to_date(month_day->calendar(), month_day->iso_date()).day;
}
// 10.3.6 Temporal.PlainMonthDay.prototype.with ( temporalMonthDayLike [ , options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.with
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::with)
{
auto temporal_month_day_like = vm.argument(0);
auto options = vm.argument(1);
// 1. Let monthDay be the this value.
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
auto month_day = TRY(typed_this_object(vm));
// 3. If ? IsPartialTemporalObject(temporalMonthDayLike) is false, throw a TypeError exception.
if (!TRY(is_partial_temporal_object(vm, temporal_month_day_like)))
return vm.throw_completion<TypeError>(ErrorType::TemporalObjectMustBePartialTemporalObject);
// 4. Let calendar be monthDay.[[Calendar]].
auto const& calendar = month_day->calendar();
// 5. Let fields be ISODateToFields(calendar, monthDay.[[ISODate]], MONTH-DAY).
auto fields = iso_date_to_fields(calendar, month_day->iso_date(), DateType::MonthDay);
// 6. Let partialMonthDay be ? PrepareCalendarFields(calendar, temporalMonthDayLike, « YEAR, MONTH, MONTH-CODE, DAY », « », PARTIAL).
auto partial_month_day = TRY(prepare_calendar_fields(vm, calendar, temporal_month_day_like.as_object(), { { CalendarField::Year, CalendarField::Month, CalendarField::MonthCode, CalendarField::Day } }, {}, Partial {}));
// 7. Set fields to CalendarMergeFields(calendar, fields, partialMonthDay).
fields = calendar_merge_fields(calendar, fields, partial_month_day);
// 8. Let resolvedOptions be ? GetOptionsObject(options).
auto resolved_options = TRY(get_options_object(vm, options));
// 9. Let overflow be ? GetTemporalOverflowOption(resolvedOptions).
auto overflow = TRY(get_temporal_overflow_option(vm, resolved_options));
// 10. Let isoDate be ? CalendarMonthDayFromFields(calendar, fields, overflow).
auto iso_date = TRY(calendar_month_day_from_fields(vm, calendar, fields, overflow));
// 11. Return ! CreateTemporalMonthDay(isoDate, calendar).
return MUST(create_temporal_month_day(vm, iso_date, calendar));
}
// 10.3.7 Temporal.PlainMonthDay.prototype.equals ( other ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.equals
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::equals)
{
// 1. Let monthDay be the this value.
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
auto month_day = TRY(typed_this_object(vm));
// 3. Set other to ? ToTemporalMonthDay(other).
auto other = TRY(to_temporal_month_day(vm, vm.argument(0)));
// 4. If CompareISODate(monthDay.[[ISODate]], other.[[ISODate]]) ≠ 0, return false.
if (compare_iso_date(month_day->iso_date(), other->iso_date()) != 0)
return false;
// 5. Return CalendarEquals(monthDay.[[Calendar]], other.[[Calendar]]).
return calendar_equals(month_day->calendar(), other->calendar());
}
// 10.3.8 Temporal.PlainMonthDay.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tostring
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_string)
{
// 1. Let monthDay be the this value.
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
auto month_day = TRY(typed_this_object(vm));
// 3. Let resolvedOptions be ? GetOptionsObject(options).
auto resolved_options = TRY(get_options_object(vm, vm.argument(0)));
// 4. Let showCalendar be ? GetTemporalShowCalendarNameOption(resolvedOptions).
auto show_calendar = TRY(get_temporal_show_calendar_name_option(vm, resolved_options));
// 5. Return TemporalMonthDayToString(monthDay, showCalendar).
return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, show_calendar));
}
// 10.3.9 Temporal.PlainMonthDay.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tolocalestring
// NOTE: This is the minimum toLocaleString implementation for engines without ECMA-402.
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_locale_string)
{
// 1. Let monthDay be the this value.
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
auto month_day = TRY(typed_this_object(vm));
// 3. Return TemporalMonthDayToString(monthDay, auto).
return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, ShowCalendar::Auto));
}
// 10.3.10 Temporal.PlainMonthDay.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.tolocalestring
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::to_json)
{
// 1. Let monthDay be the this value.
// 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]).
auto month_day = TRY(typed_this_object(vm));
// 3. Return TemporalMonthDayToString(monthDay, auto).
return PrimitiveString::create(vm, temporal_month_day_to_string(month_day, ShowCalendar::Auto));
}
// 10.3.11 Temporal.PlainMonthDay.prototype.valueOf ( ), https://tc39.es/proposal-temporal/#sec-temporal.plainmonthday.prototype.valueof
JS_DEFINE_NATIVE_FUNCTION(PlainMonthDayPrototype::value_of)
{
// 1. Throw a TypeError exception.
return vm.throw_completion<TypeError>(ErrorType::Convert, "Temporal.PlainMonthDay", "a primitive value");
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibJS/Runtime/PrototypeObject.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
namespace JS::Temporal {
class PlainMonthDayPrototype final : public PrototypeObject<PlainMonthDayPrototype, PlainMonthDay> {
JS_PROTOTYPE_OBJECT(PlainMonthDayPrototype, PlainMonthDay, Temporal.PlainMonthDay);
GC_DECLARE_ALLOCATOR(PlainMonthDayPrototype);
public:
virtual void initialize(Realm&) override;
virtual ~PlainMonthDayPrototype() override = default;
private:
explicit PlainMonthDayPrototype(Realm&);
JS_DECLARE_NATIVE_FUNCTION(calendar_id_getter);
JS_DECLARE_NATIVE_FUNCTION(month_code_getter);
JS_DECLARE_NATIVE_FUNCTION(day_getter);
JS_DECLARE_NATIVE_FUNCTION(with);
JS_DECLARE_NATIVE_FUNCTION(equals);
JS_DECLARE_NATIVE_FUNCTION(to_string);
JS_DECLARE_NATIVE_FUNCTION(to_locale_string);
JS_DECLARE_NATIVE_FUNCTION(to_json);
JS_DECLARE_NATIVE_FUNCTION(value_of);
};
}

View file

@ -0,0 +1,83 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Assertions.h>
#include <LibJS/Runtime/Temporal/PlainTime.h>
namespace JS::Temporal {
// 4.5.2 CreateTimeRecord ( hour, minute, second, millisecond, microsecond, nanosecond [ , deltaDays ] ), https://tc39.es/proposal-temporal/#sec-temporal-createtimerecord
Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days)
{
// 1. If deltaDays is not present, set deltaDays to 0.
// 2. Assert: IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond).
VERIFY(is_valid_time(hour, minute, second, millisecond, microsecond, nanosecond));
// 3. Return Time Record { [[Days]]: deltaDays, [[Hour]]: hour, [[Minute]]: minute, [[Second]]: second, [[Millisecond]]: millisecond, [[Microsecond]]: microsecond, [[Nanosecond]]: nanosecond }.
return {
.days = delta_days,
.hour = static_cast<u8>(hour),
.minute = static_cast<u8>(minute),
.second = static_cast<u8>(second),
.millisecond = static_cast<u16>(millisecond),
.microsecond = static_cast<u16>(microsecond),
.nanosecond = static_cast<u16>(nanosecond),
};
}
// 4.5.4 NoonTimeRecord ( ), https://tc39.es/proposal-temporal/#sec-temporal-noontimerecord
Time noon_time_record()
{
// 1. Return Time Record { [[Days]]: 0, [[Hour]]: 12, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }.
return { .days = 0, .hour = 12, .minute = 0, .second = 0, .millisecond = 0, .microsecond = 0, .nanosecond = 0 };
}
// 4.5.9 IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-isvalidtime
bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond)
{
// 1. If hour < 0 or hour > 23, then
if (hour < 0 || hour > 23) {
// a. Return false.
return false;
}
// 2. If minute < 0 or minute > 59, then
if (minute < 0 || minute > 59) {
// a. Return false.
return false;
}
// 3. If second < 0 or second > 59, then
if (second < 0 || second > 59) {
// a. Return false.
return false;
}
// 4. If millisecond < 0 or millisecond > 999, then
if (millisecond < 0 || millisecond > 999) {
// a. Return false.
return false;
}
// 5. If microsecond < 0 or microsecond > 999, then
if (microsecond < 0 || microsecond > 999) {
// a. Return false.
return false;
}
// 6. If nanosecond < 0 or nanosecond > 999, then
if (nanosecond < 0 || nanosecond > 999) {
// a. Return false.
return false;
}
// 7. Return true.
return true;
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2021, Idan Horowitz <idan.horowitz@serenityos.org>
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
namespace JS::Temporal {
// 4.5.1 Time Records, https://tc39.es/proposal-temporal/#sec-temporal-time-records
struct Time {
double days { 0 };
u8 hour { 0 };
u8 minute { 0 };
u8 second { 0 };
u16 millisecond { 0 };
u16 microsecond { 0 };
u16 nanosecond { 0 };
};
Time create_time_record(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond, double delta_days = 0);
Time noon_time_record();
bool is_valid_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond);
}

View file

@ -7,6 +7,7 @@
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Temporal/DurationConstructor.h>
#include <LibJS/Runtime/Temporal/PlainMonthDayConstructor.h>
#include <LibJS/Runtime/Temporal/Temporal.h>
namespace JS::Temporal {
@ -30,6 +31,7 @@ void Temporal::initialize(Realm& realm)
u8 attr = Attribute::Writable | Attribute::Configurable;
define_intrinsic_accessor(vm.names.Duration, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_duration_constructor(); });
define_intrinsic_accessor(vm.names.PlainMonthDay, attr, [](auto& realm) -> Value { return realm.intrinsics().temporal_plain_month_day_constructor(); });
}
}

View file

@ -0,0 +1,131 @@
/*
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/AbstractOperations.h>
#include <LibJS/Runtime/Date.h>
#include <LibJS/Runtime/Intl/AbstractOperations.h>
#include <LibJS/Runtime/Temporal/ISO8601.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <LibJS/Runtime/VM.h>
namespace JS::Temporal {
// 11.1.5 FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] ), https://tc39.es/proposal-temporal/#sec-temporal-formatoffsettimezoneidentifier
String format_offset_time_zone_identifier(i64 offset_minutes, Optional<TimeStyle> style)
{
// 1. If offsetMinutes ≥ 0, let sign be the code unit 0x002B (PLUS SIGN); otherwise, let sign be the code unit 0x002D (HYPHEN-MINUS).
auto sign = offset_minutes >= 0 ? '+' : '-';
// 2. Let absoluteMinutes be abs(offsetMinutes).
auto absolute_minutes = abs(offset_minutes);
// 3. Let hour be floor(absoluteMinutes / 60).
auto hour = static_cast<u8>(floor(static_cast<double>(absolute_minutes) / 60.0));
// 4. Let minute be absoluteMinutes modulo 60.
auto minute = static_cast<u8>(modulo(static_cast<double>(absolute_minutes), 60.0));
// 5. Let timeString be FormatTimeString(hour, minute, 0, 0, MINUTE, style).
auto time_string = format_time_string(hour, minute, 0, 0, SecondsStringPrecision::Minute {}, style);
// 6. Return the string-concatenation of sign and timeString.
return MUST(String::formatted("{}{}", sign, time_string));
}
// 11.1.8 ToTemporalTimeZoneIdentifier ( temporalTimeZoneLike ), https://tc39.es/proposal-temporal/#sec-temporal-totemporaltimezoneidentifier
ThrowCompletionOr<String> to_temporal_time_zone_identifier(VM& vm, Value temporal_time_zone_like)
{
// 1. If temporalTimeZoneLike is an Object, then
if (temporal_time_zone_like.is_object()) {
// FIXME: a. If temporalTimeZoneLike has an [[InitializedTemporalZonedDateTime]] internal slot, then
// FIXME: i. Return temporalTimeZoneLike.[[TimeZone]].
}
// 2. If temporalTimeZoneLike is not a String, throw a TypeError exception.
if (!temporal_time_zone_like.is_string())
return vm.throw_completion<TypeError>(ErrorType::TemporalInvalidTimeZoneName, temporal_time_zone_like);
// 3. Let parseResult be ? ParseTemporalTimeZoneString(temporalTimeZoneLike).
auto parse_result = TRY(parse_temporal_time_zone_string(vm, temporal_time_zone_like.as_string().utf8_string_view()));
// 4. Let offsetMinutes be parseResult.[[OffsetMinutes]].
// 5. If offsetMinutes is not empty, return FormatOffsetTimeZoneIdentifier(offsetMinutes).
if (parse_result.offset_minutes.has_value())
return format_offset_time_zone_identifier(*parse_result.offset_minutes);
// 6. Let name be parseResult.[[Name]].
// 7. Let timeZoneIdentifierRecord be GetAvailableNamedTimeZoneIdentifier(name).
auto time_zone_identifier_record = Intl::get_available_named_time_zone_identifier(*parse_result.name);
// 8. If timeZoneIdentifierRecord is empty, throw a RangeError exception.
if (!time_zone_identifier_record.has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidTimeZoneName, temporal_time_zone_like);
// 9. Return timeZoneIdentifierRecord.[[Identifier]].
return time_zone_identifier_record->identifier;
}
// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier
ThrowCompletionOr<TimeZone> parse_time_zone_identifier(VM& vm, StringView identifier)
{
// 1. Let parseResult be ParseText(StringToCodePoints(identifier), TimeZoneIdentifier).
auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, identifier);
// 2. If parseResult is a List of errors, throw a RangeError exception.
if (!parse_result.has_value())
return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidTimeZoneString, identifier);
return parse_time_zone_identifier(*parse_result);
}
// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier
TimeZone parse_time_zone_identifier(StringView identifier)
{
// OPTIMIZATION: Some callers can assume that parsing will succeed.
// 1. Let parseResult be ParseText(StringToCodePoints(identifier), TimeZoneIdentifier).
auto parse_result = parse_iso8601(Production::TimeZoneIdentifier, identifier);
VERIFY(parse_result.has_value());
return parse_time_zone_identifier(*parse_result);
}
// 11.1.16 ParseTimeZoneIdentifier ( identifier ), https://tc39.es/proposal-temporal/#sec-parsetimezoneidentifier
TimeZone parse_time_zone_identifier(ParseResult const& parse_result)
{
// OPTIMIZATION: Some callers will have already parsed and validated the time zone identifier.
// 3. If parseResult contains a TimeZoneIANAName Parse Node, then
if (parse_result.time_zone_iana_name.has_value()) {
// a. Let name be the source text matched by the TimeZoneIANAName Parse Node contained within parseResult.
// b. NOTE: name is syntactically valid, but does not necessarily conform to IANA Time Zone Database naming
// guidelines or correspond with an available named time zone identifier.
// c. Return the Record { [[Name]]: CodePointsToString(name), [[OffsetMinutes]]: empty }.
return TimeZone { .name = String::from_utf8_without_validation(parse_result.time_zone_iana_name->bytes()), .offset_minutes = {} };
}
// 4. Else,
else {
// a. Assert: parseResult contains a UTCOffset[~SubMinutePrecision] Parse Node.
VERIFY(parse_result.time_zone_offset.has_value());
// b. Let offsetString be the source text matched by the UTCOffset[~SubMinutePrecision] Parse Node contained within parseResult.
// c. Let offsetNanoseconds be ! ParseDateTimeUTCOffset(offsetString).
auto offset_nanoseconds = parse_date_time_utc_offset(parse_result.time_zone_offset->source_text);
// d. Let offsetMinutes be offsetNanoseconds / (60 × 10**9).
auto offset_minutes = offset_nanoseconds / 60'000'000'000;
// e. Assert: offsetMinutes is an integer.
VERIFY(trunc(offset_minutes) == offset_minutes);
// f. Return the Record { [[Name]]: empty, [[OffsetMinutes]]: offsetMinutes }.
return TimeZone { .name = {}, .offset_minutes = static_cast<i64>(offset_minutes) };
}
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021-2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Temporal/AbstractOperations.h>
#include <LibJS/Runtime/Value.h>
namespace JS::Temporal {
struct TimeZone {
Optional<String> name;
Optional<i64> offset_minutes;
};
String format_offset_time_zone_identifier(i64 offset_minutes, Optional<TimeStyle> = {});
ThrowCompletionOr<String> to_temporal_time_zone_identifier(VM&, Value temporal_time_zone_like);
ThrowCompletionOr<TimeZone> parse_time_zone_identifier(VM&, StringView identifier);
TimeZone parse_time_zone_identifier(StringView identifier);
TimeZone parse_time_zone_identifier(ParseResult const&);
}

View file

@ -0,0 +1,104 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.PlainMonthDay.from).toHaveLength(1);
});
test("fields object argument", () => {
const object = {
month: 7,
day: 6,
};
const plainMonthDay = Temporal.PlainMonthDay.from(object);
expect(plainMonthDay.monthCode).toBe("M07");
expect(plainMonthDay.day).toBe(6);
});
test("from month day string", () => {
const plainMonthDay = Temporal.PlainMonthDay.from("--07-06");
expect(plainMonthDay.monthCode).toBe("M07");
expect(plainMonthDay.day).toBe(6);
});
test("from date time string", () => {
const plainMonthDay = Temporal.PlainMonthDay.from("2021-07-06T23:42:01");
expect(plainMonthDay.monthCode).toBe("M07");
expect(plainMonthDay.day).toBe(6);
});
test("compares calendar name in month day string in lowercase", () => {
const values = [
"02-10[u-ca=iso8601]",
"02-10[u-ca=isO8601]",
"02-10[u-ca=iSo8601]",
"02-10[u-ca=iSO8601]",
"02-10[u-ca=Iso8601]",
"02-10[u-ca=IsO8601]",
"02-10[u-ca=ISo8601]",
"02-10[u-ca=ISO8601]",
];
for (const value of values) {
expect(() => {
Temporal.PlainMonthDay.from(value);
}).not.toThrowWithMessage(
RangeError,
"MM-DD string format can only be used with the iso8601 calendar"
);
}
});
});
describe("errors", () => {
test("missing fields", () => {
expect(() => {
Temporal.PlainMonthDay.from({});
}).toThrowWithMessage(TypeError, "Required property day is missing or undefined");
expect(() => {
Temporal.PlainMonthDay.from({ month: 1 });
}).toThrowWithMessage(TypeError, "Required property day is missing or undefined");
expect(() => {
Temporal.PlainMonthDay.from({ day: 1 });
}).toThrowWithMessage(TypeError, "Required property month is missing or undefined");
});
test("invalid month day string", () => {
expect(() => {
Temporal.PlainMonthDay.from("foo");
}).toThrowWithMessage(RangeError, "Invalid ISO date time");
});
test("string must not contain a UTC designator", () => {
expect(() => {
Temporal.PlainMonthDay.from("2021-07-06T23:42:01Z");
}).toThrowWithMessage(RangeError, "Invalid ISO date time");
});
test("extended year must not be negative zero", () => {
expect(() => {
Temporal.PlainMonthDay.from("-000000-01-01");
}).toThrowWithMessage(RangeError, "Invalid ISO date time");
});
test("can only use iso8601 calendar with month day strings", () => {
expect(() => {
Temporal.PlainMonthDay.from("02-10[u-ca=iso8602]");
}).toThrowWithMessage(RangeError, "Invalid calendar identifier 'iso8602'");
expect(() => {
Temporal.PlainMonthDay.from("02-10[u-ca=ladybird]");
}).toThrowWithMessage(RangeError, "Invalid calendar identifier 'ladybird'");
});
test("doesn't throw non-iso8601 calendar error when using a superset format string such as DateTime", () => {
// NOTE: This will still throw, but only because "ladybird" is not a recognised calendar, not because of the string format restriction.
try {
Temporal.PlainMonthDay.from("2023-02-10T22:56[u-ca=ladybird]");
} catch (e) {
expect(e).toBeInstanceOf(RangeError);
expect(e.message).not.toBe(
"MM-DD string format can only be used with the iso8601 calendar"
);
expect(e.message).toBe("Invalid calendar identifier 'ladybird'");
}
});
});

View file

@ -0,0 +1,66 @@
describe("errors", () => {
test("called without new", () => {
expect(() => {
Temporal.PlainMonthDay();
}).toThrowWithMessage(
TypeError,
"Temporal.PlainMonthDay constructor must be called with 'new'"
);
});
test("cannot pass Infinity", () => {
expect(() => {
new Temporal.PlainMonthDay(Infinity);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
expect(() => {
new Temporal.PlainMonthDay(1, Infinity);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
expect(() => {
new Temporal.PlainMonthDay(1, 1, undefined, Infinity);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
expect(() => {
new Temporal.PlainMonthDay(-Infinity);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
expect(() => {
new Temporal.PlainMonthDay(1, -Infinity);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
expect(() => {
new Temporal.PlainMonthDay(1, 1, undefined, -Infinity);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
});
test("cannot pass invalid ISO month/day", () => {
expect(() => {
new Temporal.PlainMonthDay(0, 1);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
expect(() => {
new Temporal.PlainMonthDay(1, 0);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
});
test("not within iso date time limit", () => {
expect(() => {
new Temporal.PlainMonthDay(9, 30, "iso8601", 999_999_999_999_999);
}).toThrowWithMessage(RangeError, "Invalid plain month day");
});
});
describe("normal behavior", () => {
test("length is 2", () => {
expect(Temporal.PlainMonthDay).toHaveLength(2);
});
test("basic functionality", () => {
const plainMonthDay = new Temporal.PlainMonthDay(7, 6);
expect(typeof plainMonthDay).toBe("object");
expect(plainMonthDay).toBeInstanceOf(Temporal.PlainMonthDay);
expect(Object.getPrototypeOf(plainMonthDay)).toBe(Temporal.PlainMonthDay.prototype);
});
// FIXME: Re-implement this test with Temporal.PlainMonthDay.prototype.toString({ calendarName: "always" }).
// test("default reference year is 1972", () => {
// const plainMonthDay = new Temporal.PlainMonthDay(7, 6);
// const fields = plainMonthDay.getISOFields();
// expect(fields.isoYear).toBe(1972);
// });
});

View file

@ -0,0 +1,14 @@
describe("correct behavior", () => {
test("calendarId basic functionality", () => {
const plainMonthDay = new Temporal.PlainMonthDay(5, 1, "iso8601");
expect(plainMonthDay.calendarId).toBe("iso8601");
});
});
describe("errors", () => {
test("this value must be a Temporal.PlainMonthDay object", () => {
expect(() => {
Reflect.get(Temporal.PlainMonthDay.prototype, "calendarId", "foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay");
});
});

View file

@ -0,0 +1,14 @@
describe("correct behavior", () => {
test("basic functionality", () => {
const plainMonthDay = new Temporal.PlainMonthDay(7, 6);
expect(plainMonthDay.day).toBe(6);
});
});
describe("errors", () => {
test("this value must be a Temporal.PlainMonthDay object", () => {
expect(() => {
Reflect.get(Temporal.PlainMonthDay.prototype, "day", "foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay");
});
});

View file

@ -0,0 +1,14 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.PlainMonthDay.prototype.equals).toHaveLength(1);
});
test("basic functionality", () => {
const firstPlainMonthDay = new Temporal.PlainMonthDay(2, 1, "iso8601");
const secondPlainMonthDay = new Temporal.PlainMonthDay(1, 1, "iso8601");
expect(firstPlainMonthDay.equals(firstPlainMonthDay)).toBeTrue();
expect(secondPlainMonthDay.equals(secondPlainMonthDay)).toBeTrue();
expect(firstPlainMonthDay.equals(secondPlainMonthDay)).toBeFalse();
expect(secondPlainMonthDay.equals(firstPlainMonthDay)).toBeFalse();
});
});

View file

@ -0,0 +1,14 @@
describe("correct behavior", () => {
test("basic functionality", () => {
const plainMonthDay = new Temporal.PlainMonthDay(7, 6);
expect(plainMonthDay.monthCode).toBe("M07");
});
});
describe("errors", () => {
test("this value must be a Temporal.PlainMonthDay object", () => {
expect(() => {
Reflect.get(Temporal.PlainMonthDay.prototype, "monthCode", "foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay");
});
});

View file

@ -0,0 +1,23 @@
describe("correct behavior", () => {
test("length is 0", () => {
expect(Temporal.PlainMonthDay.prototype.toJSON).toHaveLength(0);
});
test("basic functionality", () => {
let plainMonthDay;
plainMonthDay = new Temporal.PlainMonthDay(7, 6);
expect(plainMonthDay.toJSON()).toBe("07-06");
plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021);
expect(plainMonthDay.toJSON()).toBe("2021-07-06[u-ca=gregory]");
});
});
describe("errors", () => {
test("this value must be a Temporal.PlainMonthDay object", () => {
expect(() => {
Temporal.PlainMonthDay.prototype.toJSON.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay");
});
});

View file

@ -0,0 +1,23 @@
describe("correct behavior", () => {
test("length is 0", () => {
expect(Temporal.PlainMonthDay.prototype.toLocaleString).toHaveLength(0);
});
test("basic functionality", () => {
let plainMonthDay;
plainMonthDay = new Temporal.PlainMonthDay(7, 6);
expect(plainMonthDay.toLocaleString()).toBe("07-06");
plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021);
expect(plainMonthDay.toLocaleString()).toBe("2021-07-06[u-ca=gregory]");
});
});
describe("errors", () => {
test("this value must be a Temporal.PlainMonthDay object", () => {
expect(() => {
Temporal.PlainMonthDay.prototype.toLocaleString.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay");
});
});

View file

@ -0,0 +1,42 @@
describe("correct behavior", () => {
test("length is 0", () => {
expect(Temporal.PlainMonthDay.prototype.toString).toHaveLength(0);
});
test("basic functionality", () => {
let plainMonthDay;
plainMonthDay = new Temporal.PlainMonthDay(7, 6);
expect(plainMonthDay.toString()).toBe("07-06");
expect(plainMonthDay.toString({ calendarName: "auto" })).toBe("07-06");
expect(plainMonthDay.toString({ calendarName: "always" })).toBe("1972-07-06[u-ca=iso8601]");
expect(plainMonthDay.toString({ calendarName: "never" })).toBe("07-06");
expect(plainMonthDay.toString({ calendarName: "critical" })).toBe(
"1972-07-06[!u-ca=iso8601]"
);
plainMonthDay = new Temporal.PlainMonthDay(7, 6, "gregory", 2021);
expect(plainMonthDay.toString()).toBe("2021-07-06[u-ca=gregory]");
expect(plainMonthDay.toString({ calendarName: "auto" })).toBe("2021-07-06[u-ca=gregory]");
expect(plainMonthDay.toString({ calendarName: "always" })).toBe("2021-07-06[u-ca=gregory]");
expect(plainMonthDay.toString({ calendarName: "never" })).toBe("2021-07-06");
expect(plainMonthDay.toString({ calendarName: "critical" })).toBe(
"2021-07-06[!u-ca=gregory]"
);
});
});
describe("errors", () => {
test("this value must be a Temporal.PlainMonthDay object", () => {
expect(() => {
Temporal.PlainMonthDay.prototype.toString.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay");
});
test("calendarName option must be one of 'auto', 'always', 'never', 'critical'", () => {
const plainMonthDay = new Temporal.PlainMonthDay(7, 6);
expect(() => {
plainMonthDay.toString({ calendarName: "foo" });
}).toThrowWithMessage(RangeError, "foo is not a valid value for option calendarName");
});
});

View file

@ -0,0 +1,11 @@
describe("errors", () => {
test("throws TypeError", () => {
const plainMonthDay = new Temporal.PlainMonthDay(7, 6);
expect(() => {
plainMonthDay.valueOf();
}).toThrowWithMessage(
TypeError,
"Cannot convert Temporal.PlainMonthDay to a primitive value"
);
});
});

View file

@ -0,0 +1,57 @@
describe("correct behavior", () => {
test("length is 1", () => {
expect(Temporal.PlainMonthDay.prototype.with).toHaveLength(1);
});
test("basic functionality", () => {
const plainMonthDay = new Temporal.PlainMonthDay(1, 1);
const values = [
[{ monthCode: "M07" }, new Temporal.PlainMonthDay(7, 1)],
[{ monthCode: "M07", day: 6 }, new Temporal.PlainMonthDay(7, 6)],
[{ year: 0, month: 7, day: 6 }, new Temporal.PlainMonthDay(7, 6)],
];
for (const [arg, expected] of values) {
expect(plainMonthDay.with(arg).equals(expected)).toBeTrue();
}
// Supplying the same values doesn't change the month/day, but still creates a new object
const plainMonthDayLike = {
month: plainMonthDay.month,
day: plainMonthDay.day,
};
expect(plainMonthDay.with(plainMonthDayLike)).not.toBe(plainMonthDay);
expect(plainMonthDay.with(plainMonthDayLike).equals(plainMonthDay)).toBeTrue();
});
});
describe("errors", () => {
test("this value must be a Temporal.PlainMonthDay object", () => {
expect(() => {
Temporal.PlainMonthDay.prototype.with.call("foo");
}).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainMonthDay");
});
test("argument must be an object", () => {
expect(() => {
new Temporal.PlainMonthDay(1, 1).with("foo");
}).toThrowWithMessage(TypeError, "Object must be a partial Temporal object");
expect(() => {
new Temporal.PlainMonthDay(1, 1).with(42);
}).toThrowWithMessage(TypeError, "Object must be a partial Temporal object");
});
test("argument must have at least one Temporal property", () => {
expect(() => {
new Temporal.PlainMonthDay(1, 1).with({});
}).toThrowWithMessage(TypeError, "Object must be a partial Temporal object");
});
test("argument must not have 'calendar' or 'timeZone'", () => {
expect(() => {
new Temporal.PlainMonthDay(1, 1).with({ calendar: {} });
}).toThrowWithMessage(TypeError, "Object must be a partial Temporal object");
expect(() => {
new Temporal.PlainMonthDay(1, 1).with({ timeZone: {} });
}).toThrowWithMessage(TypeError, "Object must be a partial Temporal object");
});
});