Explorar o código

LibJS: Implement the Temporal.PlainMonthDay constructor

And the simple Temporal.PlainMonthDay.prototype getters, so that the
constructed Temporal.PlainMonthDay may actually be validated.
Timothy Flynn hai 8 meses
pai
achega
1a386e78c3
Modificáronse 31 ficheiros con 2515 adicións e 4 borrados
  1. 9 0
      Libraries/LibJS/CMakeLists.txt
  2. 15 2
      Libraries/LibJS/Forward.h
  3. 12 0
      Libraries/LibJS/Print.cpp
  4. 2 0
      Libraries/LibJS/Runtime/ErrorTypes.h
  5. 2 0
      Libraries/LibJS/Runtime/Intrinsics.cpp
  6. 478 1
      Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp
  7. 71 1
      Libraries/LibJS/Runtime/Temporal/AbstractOperations.h
  8. 586 0
      Libraries/LibJS/Runtime/Temporal/Calendar.cpp
  9. 114 0
      Libraries/LibJS/Runtime/Temporal/Calendar.h
  10. 89 0
      Libraries/LibJS/Runtime/Temporal/DateEquations.cpp
  11. 24 0
      Libraries/LibJS/Runtime/Temporal/DateEquations.h
  12. 99 0
      Libraries/LibJS/Runtime/Temporal/PlainDate.cpp
  13. 30 0
      Libraries/LibJS/Runtime/Temporal/PlainDate.h
  14. 55 0
      Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp
  15. 25 0
      Libraries/LibJS/Runtime/Temporal/PlainDateTime.h
  16. 144 0
      Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp
  17. 37 0
      Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h
  18. 102 0
      Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp
  19. 33 0
      Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h
  20. 70 0
      Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp
  21. 31 0
      Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h
  22. 83 0
      Libraries/LibJS/Runtime/Temporal/PlainTime.cpp
  23. 30 0
      Libraries/LibJS/Runtime/Temporal/PlainTime.h
  24. 2 0
      Libraries/LibJS/Runtime/Temporal/Temporal.cpp
  25. 132 0
      Libraries/LibJS/Runtime/Temporal/TimeZone.cpp
  26. 28 0
      Libraries/LibJS/Runtime/Temporal/TimeZone.h
  27. 104 0
      Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js
  28. 66 0
      Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js
  29. 14 0
      Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js
  30. 14 0
      Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js
  31. 14 0
      Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js

+ 9 - 0
Libraries/LibJS/CMakeLists.txt

@@ -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

+ 15 - 2
Libraries/LibJS/Forward.h

@@ -87,8 +87,9 @@
     __JS_ENUMERATE(RelativeTimeFormat, relative_time_format, RelativeTimeFormatPrototype, RelativeTimeFormatConstructor) \
     __JS_ENUMERATE(Segmenter, segmenter, SegmenterPrototype, SegmenterConstructor)
 
-#define JS_ENUMERATE_TEMPORAL_OBJECTS \
-    __JS_ENUMERATE(Duration, duration, DurationPrototype, DurationConstructor)
+#define JS_ENUMERATE_TEMPORAL_OBJECTS                                          \
+    __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>

+ 12 - 0
Libraries/LibJS/Print.cpp

@@ -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);
     }
 

+ 2 - 0
Libraries/LibJS/Runtime/ErrorTypes.h

@@ -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")                                                                    \

+ 2 - 0
Libraries/LibJS/Runtime/Intrinsics.cpp

@@ -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>

+ 478 - 1
Libraries/LibJS/Runtime/Temporal/AbstractOperations.cpp

@@ -9,11 +9,15 @@
 
 #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/TimeZone.h>
 
 namespace JS::Temporal {
 
@@ -43,6 +47,43 @@ 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.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)
 {
@@ -422,6 +463,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 +726,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 +1200,152 @@ 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).
+    // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal. For now, we
+    //        can just check with the ISO8601 parser directly.
+    if (!parse_utc_offset(argument.as_string().utf8_string_view(), SubMinutePrecision::Yes).has_value())
+        return vm.throw_completion<RangeError>(ErrorType::TemporalInvalidTimeZoneString, offset);
+
+    // 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 +1454,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);
+}
+
 }

+ 71 - 1
Libraries/LibJS/Runtime/Temporal/AbstractOperations.h

@@ -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,22 @@ enum class ArithmeticOperation {
     Subtract,
 };
 
+enum class DateType {
+    Date,
+    MonthDay,
+    YearMonth,
+};
+
+enum class Overflow {
+    Constrain,
+    Reject,
+};
+
+enum class TimeStyle {
+    Separated,
+    Unseparated,
+};
+
 // https://tc39.es/proposal-temporal/#sec-temporal-units
 enum class Unit {
     Year,
@@ -91,8 +109,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 +122,28 @@ 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<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);
@@ -114,12 +155,19 @@ UnitCategory temporal_unit_category(Unit);
 RoundingIncrement maximum_temporal_duration_rounding_increment(Unit);
 Crypto::UnsignedBigInteger const& temporal_unit_length_in_nanoseconds(Unit);
 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 +201,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 +231,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 +255,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&);
 
 }

+ 586 - 0
Libraries/LibJS/Runtime/Temporal/Calendar.cpp

@@ -0,0 +1,586 @@
+/*
+ * 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.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.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.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 {};
+}
+
+}

+ 114 - 0
Libraries/LibJS/Runtime/Temporal/Calendar.h

@@ -0,0 +1,114 @@
+/*
+ * 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);
+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&);
+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);
+ThrowCompletionOr<void> calendar_resolve_fields(VM&, StringView calendar, CalendarFields&, DateType);
+
+}

+ 89 - 0
Libraries/LibJS/Runtime/Temporal/DateEquations.cpp

@@ -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));
+}
+
+}

+ 24 - 0
Libraries/LibJS/Runtime/Temporal/DateEquations.h

@@ -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);
+
+}

+ 99 - 0
Libraries/LibJS/Runtime/Temporal/PlainDate.cpp

@@ -0,0 +1,99 @@
+/*
+ * 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 further clamp the year to the range allowed by ISOYearMonthWithinLimits, to ensure we do not
+        //         overflow when we store the year as an integer.
+        year = clamp(year, -271821, 275760);
+
+        break;
+
+    // 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 (years: [-271821, 275760], months: [1, 12], 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.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);
+}
+
+}

+ 30 - 0
Libraries/LibJS/Runtime/Temporal/PlainDate.h

@@ -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, 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);
+bool iso_date_within_limits(ISODate);
+
+}

+ 55 - 0
Libraries/LibJS/Runtime/Temporal/PlainDateTime.cpp

@@ -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;
+}
+
+}

+ 25 - 0
Libraries/LibJS/Runtime/Temporal/PlainDateTime.h

@@ -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);
+
+}

+ 144 - 0
Libraries/LibJS/Runtime/Temporal/PlainMonthDay.cpp

@@ -0,0 +1,144 @@
+/*
+ * 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/AbstractOperations.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;
+}
+
+}

+ 37 - 0
Libraries/LibJS/Runtime/Temporal/PlainMonthDay.h

@@ -0,0 +1,37 @@
+/*
+ * 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/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 = {});
+
+}

+ 102 - 0
Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.cpp

@@ -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)));
+}
+
+}

+ 33 - 0
Libraries/LibJS/Runtime/Temporal/PlainMonthDayConstructor.h

@@ -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);
+};
+
+}

+ 70 - 0
Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.cpp

@@ -0,0 +1,70 @@
+/*
+ * 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/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);
+}
+
+// 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;
+}
+
+}

+ 31 - 0
Libraries/LibJS/Runtime/Temporal/PlainMonthDayPrototype.h

@@ -0,0 +1,31 @@
+/*
+ * 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);
+};
+
+}

+ 83 - 0
Libraries/LibJS/Runtime/Temporal/PlainTime.cpp

@@ -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;
+}
+
+}

+ 30 - 0
Libraries/LibJS/Runtime/Temporal/PlainTime.h

@@ -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);
+
+}

+ 2 - 0
Libraries/LibJS/Runtime/Temporal/Temporal.cpp

@@ -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(); });
 }
 
 }

+ 132 - 0
Libraries/LibJS/Runtime/Temporal/TimeZone.cpp

@@ -0,0 +1,132 @@
+/*
+ * 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).
+        // FIXME: ParseTimeZoneOffsetString should be renamed to ParseDateTimeUTCOffset and updated for Temporal.
+        auto offset_nanoseconds = parse_time_zone_offset_string(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) };
+    }
+}
+
+}

+ 28 - 0
Libraries/LibJS/Runtime/Temporal/TimeZone.h

@@ -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&);
+
+}

+ 104 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.from.js

@@ -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'");
+        }
+    });
+});

+ 66 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.js

@@ -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);
+    // });
+});

+ 14 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.calendarId.js

@@ -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");
+    });
+});

+ 14 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.day.js

@@ -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");
+    });
+});

+ 14 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainMonthDay/PlainMonthDay.prototype.monthCode.js

@@ -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");
+    });
+});