LibJS: Support non-UTC time zones in Temporal :^)

We can now recognize & normalize all time zones from the IANA time zone
database and not just 'UTC', which makes the LibJS Temporal
implementation a lot more useful! Thanks to the newly added LibTimeZone,
this was incredibly easy to implement :^)

This already includes these recent editorial changes in the Temporal
spec: https://github.com/tc39/proposal-temporal/commit/27bffe1
This commit is contained in:
Linus Groh 2022-01-11 20:20:16 +01:00
parent 205d63c3f0
commit d527eb62da
Notes: sideshowbarker 2024-07-17 21:11:33 +09:00
5 changed files with 49 additions and 27 deletions

View file

@ -219,4 +219,4 @@ set(SOURCES
)
serenity_lib(LibJS js)
target_link_libraries(LibJS LibM LibCore LibCrypto LibRegex LibSyntax LibUnicode)
target_link_libraries(LibJS LibM LibCore LibCrypto LibRegex LibSyntax LibTimeZone LibUnicode)

View file

@ -16,6 +16,7 @@
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <LibJS/Runtime/Temporal/TimeZoneConstructor.h>
#include <LibJS/Runtime/Temporal/ZonedDateTime.h>
#include <LibTimeZone/TimeZone.h>
namespace JS::Temporal {
@ -27,35 +28,30 @@ TimeZone::TimeZone(String identifier, Object& prototype)
}
// 11.1.1 IsValidTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sec-isvalidtimezonename
// NOTE: This is the minimum implementation of IsValidTimeZoneName, supporting only the "UTC" time zone.
// 15.1.1 IsValidTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sup-isvalidtimezonename
bool is_valid_time_zone_name(String const& time_zone)
{
// 1. Assert: Type(timeZone) is String.
// 2. Let tzText be ! StringToCodePoints(timeZone).
// 3. Let tzUpperText be the result of toUppercase(tzText), according to the Unicode Default Case Conversion algorithm.
// 4. Let tzUpper be ! CodePointsToString(tzUpperText).
auto tz_upper = time_zone.to_uppercase();
// 5. If tzUpper and "UTC" are the same sequence of code points, return true.
if (tz_upper == "UTC")
return true;
// 6. Return false.
return false;
// 1. If one of the Zone or Link names of the IANA Time Zone Database is an ASCII-case-insensitive match of timeZone as described in 6.1, return true.
// 2. If timeZone is an ASCII-case-insensitive match of "UTC", return true.
// 3. Return false.
// NOTE: When LibTimeZone is built without ENABLE_TIME_ZONE_DATA, this only recognizes 'UTC',
// which matches the minimum requirements of the Temporal spec.
return ::TimeZone::time_zone_from_string(time_zone).has_value();
}
// 11.1.2 CanonicalizeTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sec-canonicalizetimezonename
// NOTE: This is the minimum implementation of CanonicalizeTimeZoneName, supporting only the "UTC" time zone.
// 15.1.2 CanonicalizeTimeZoneName ( timeZone ), https://tc39.es/proposal-temporal/#sup-canonicalizetimezonename
String canonicalize_time_zone_name(String const& time_zone)
{
// 1. Assert: Type(timeZone) is String.
// 1. Let ianaTimeZone be the String value of the Zone or Link name of the IANA Time Zone Database that is an ASCII-case-insensitive match of timeZone as described in 6.1.
// 2. If ianaTimeZone is a Link name, let ianaTimeZone be the String value of the corresponding Zone name as specified in the file backward of the IANA Time Zone Database.
auto iana_time_zone = ::TimeZone::canonicalize_time_zone(time_zone);
// 2. Assert: ! IsValidTimeZoneName(timeZone) is true.
VERIFY(is_valid_time_zone_name(time_zone));
// 3. If ianaTimeZone is "Etc/UTC" or "Etc/GMT", return "UTC".
// NOTE: This is already done in canonicalize_time_zone().
// 3. Return "UTC".
return "UTC";
// 4. Return ianaTimeZone.
return *iana_time_zone;
}
// 11.1.3 DefaultTimeZone ( ), https://tc39.es/proposal-temporal/#sec-defaulttimezone
@ -160,12 +156,12 @@ ISODateTime get_iso_parts_from_epoch(BigInt const& epoch_nanoseconds)
}
// 11.6.4 GetIANATimeZoneEpochValue ( timeZoneIdentifier, year, month, day, hour, minute, second, millisecond, microsecond, nanosecond ), https://tc39.es/proposal-temporal/#sec-temporal-getianatimezoneepochvalue
MarkedValueList get_iana_time_zone_epoch_value(GlobalObject& global_object, StringView time_zone_identifier, i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond)
MarkedValueList get_iana_time_zone_epoch_value(GlobalObject& global_object, [[maybe_unused]] StringView time_zone_identifier, i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond)
{
// The abstract operation GetIANATimeZoneEpochValue is an implementation-defined algorithm that returns a List of integers. Each integer in the List represents a number of nanoseconds since the Unix epoch in UTC that may correspond to the given calendar date and wall-clock time in the IANA time zone identified by timeZoneIdentifier.
// When the input represents a local time repeating multiple times at a negative time zone transition (e.g. when the daylight saving time ends or the time zone offset is decreased due to a time zone rule change), the returned List will have more than one element. When the input represents a skipped local time at a positive time zone transition (e.g. when the daylight saving time starts or the time zone offset is increased due to a time zone rule change), the returned List will be empty. Otherwise, the returned List will have one element.
VERIFY(time_zone_identifier == "UTC"sv);
// FIXME: Implement this properly for non-UTC timezones.
// FIXME: MarkedValueList<T> for T != Value would still be nice.
auto& vm = global_object.vm();
auto list = MarkedValueList { vm.heap() };

View file

@ -10,6 +10,14 @@ describe("normal behavior", () => {
expect(Temporal.TimeZone.from(timeZone)).toBe(timeZone);
expect(Temporal.TimeZone.from(timeZoneLike)).toBe(timeZoneLike);
expect(Temporal.TimeZone.from(zonedDateTimeLike)).toBe(zonedDateTimeLike.timeZone);
// TODO: test from("string") once ParseTemporalTimeZoneString is working
expect(Temporal.TimeZone.from("UTC").id).toBe("UTC");
expect(Temporal.TimeZone.from("GMT").id).toBe("UTC");
expect(Temporal.TimeZone.from("Etc/UTC").id).toBe("UTC");
expect(Temporal.TimeZone.from("Etc/GMT").id).toBe("UTC");
// FIXME: https://github.com/tc39/proposal-temporal/issues/1993
// expect(Temporal.TimeZone.from("Etc/GMT+12").id).toBe("Etc/GMT+12");
// expect(Temporal.TimeZone.from("Etc/GMT-12").id).toBe("Etc/GMT-12");
expect(Temporal.TimeZone.from("Europe/London").id).toBe("Europe/London");
expect(Temporal.TimeZone.from("Europe/Isle_of_Man").id).toBe("Europe/London");
});
});

View file

@ -36,9 +36,22 @@ describe("normal behavior", () => {
});
test("canonicalizes time zone name", () => {
expect(new Temporal.TimeZone("Utc").id).toBe("UTC");
expect(new Temporal.TimeZone("utc").id).toBe("UTC");
expect(new Temporal.TimeZone("uTC").id).toBe("UTC");
const values = [
["UTC", "UTC"],
["Utc", "UTC"],
["utc", "UTC"],
["uTc", "UTC"],
["GMT", "UTC"],
["Etc/UTC", "UTC"],
["Etc/GMT", "UTC"],
["Etc/GMT+12", "Etc/GMT+12"],
["Etc/GMT-12", "Etc/GMT-12"],
["Europe/London", "Europe/London"],
["Europe/Isle_of_Man", "Europe/London"],
];
for (const [arg, expected] of values) {
expect(new Temporal.TimeZone(arg).id).toBe(expected);
}
});
test("numeric UTC offset", () => {

View file

@ -8,6 +8,11 @@ describe("correct behavior", () => {
["utc", "UTC"],
["Utc", "UTC"],
["UTC", "UTC"],
["GMT", "UTC"],
["Etc/UTC", "UTC"],
["Etc/GMT", "UTC"],
["Europe/London", "Europe/London"],
["Europe/Isle_of_Man", "Europe/London"],
["+00:00", "+00:00"],
["+00:00:00", "+00:00"],
["+00:00:00.000", "+00:00"],