瀏覽代碼

LibJS: Actually implement get_iana_time_zone_offset_nanoseconds()

Instead of hard-coding an UTC offset of zero seconds, which worked for
the sole UTC time zone, we can now get the proper offset from the TZDB!
Linus Groh 3 年之前
父節點
當前提交
355fbcb702

+ 26 - 3
Userland/Libraries/LibJS/Runtime/Temporal/TimeZone.cpp

@@ -5,6 +5,7 @@
  */
 
 #include <AK/DateTimeLexer.h>
+#include <AK/Time.h>
 #include <LibCrypto/BigInt/UnsignedBigInteger.h>
 #include <LibJS/Runtime/AbstractOperations.h>
 #include <LibJS/Runtime/Date.h>
@@ -170,12 +171,34 @@ MarkedValueList get_iana_time_zone_epoch_value(GlobalObject& global_object, [[ma
 }
 
 // 11.6.5 GetIANATimeZoneOffsetNanoseconds ( epochNanoseconds, timeZoneIdentifier ), https://tc39.es/proposal-temporal/#sec-temporal-getianatimezoneoffsetnanoseconds
-i64 get_iana_time_zone_offset_nanoseconds([[maybe_unused]] BigInt const& epoch_nanoseconds, [[maybe_unused]] String const& time_zone_identifier)
+i64 get_iana_time_zone_offset_nanoseconds(BigInt const& epoch_nanoseconds, String const& time_zone_identifier)
 {
     // The abstract operation GetIANATimeZoneOffsetNanoseconds is an implementation-defined algorithm that returns an integer representing the offset of the IANA time zone identified by timeZoneIdentifier from UTC, at the instant corresponding to epochNanoseconds.
     // Given the same values of epochNanoseconds and timeZoneIdentifier, the result must be the same for the lifetime of the surrounding agent.
-    // TODO: Implement this
-    return 0;
+
+    // Only called with validated TimeZone [[Identifier]] as argument.
+    auto time_zone = ::TimeZone::time_zone_from_string(time_zone_identifier);
+    VERIFY(time_zone.has_value());
+
+    // Since Time::from_seconds() and Time::from_nanoseconds() both take an i64, converting to
+    // seconds first gives us a greater range. The TZDB doesn't have sub-second offsets.
+    auto seconds = epoch_nanoseconds.big_integer().divided_by("1000000000"_bigint).quotient;
+
+    // The provided epoch (nano)seconds value is potentially out of range for AK::Time and subsequently
+    // get_time_zone_offset(). We can safely assume that the TZDB has no useful information that far
+    // into the past and future anyway, so clamp it to the i64 range.
+    Time time;
+    if (seconds < Crypto::SignedBigInteger::create_from(NumericLimits<i64>::min()))
+        time = Time::min();
+    else if (seconds > Crypto::SignedBigInteger::create_from(NumericLimits<i64>::max()))
+        time = Time::max();
+    else
+        time = Time::from_seconds(*seconds.to_base(10).to_int<i64>());
+
+    auto offset_seconds = ::TimeZone::get_time_zone_offset(*time_zone, time);
+    VERIFY(offset_seconds.has_value());
+
+    return *offset_seconds * 1'000'000'000;
 }
 
 // 11.6.6 GetIANATimeZoneNextTransition ( epochNanoseconds, timeZoneIdentifier ), https://tc39.es/proposal-temporal/#sec-temporal-getianatimezonenexttransition

+ 37 - 3
Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.getOffsetNanosecondsFor.js

@@ -4,9 +4,43 @@ describe("correct behavior", () => {
     });
 
     test("basic functionality", () => {
-        const timeZone = new Temporal.TimeZone("UTC");
-        const instant = new Temporal.Instant(0n);
-        expect(timeZone.getOffsetNanosecondsFor(instant)).toBe(0);
+        // Adapted from TestTimeZone.cpp's TEST_CASE(get_time_zone_offset).
+
+        function offset(sign, hours, minutes, seconds) {
+            return sign * (hours * 3600 + minutes * 60 + seconds) * 1_000_000_000;
+        }
+
+        function testOffset(timeZone, time, expectedOffset) {
+            const instant = new Temporal.Instant(BigInt(time) * 1_000_000_000n);
+            const actualOffset = new Temporal.TimeZone(timeZone).getOffsetNanosecondsFor(instant);
+            expect(actualOffset).toBe(expectedOffset);
+        }
+
+        testOffset("America/Chicago", -2717668237, offset(-1, 5, 50, 36)); // Sunday, November 18, 1883 12:09:23 PM
+        testOffset("America/Chicago", -2717668236, offset(-1, 6, 0, 0)); // Sunday, November 18, 1883 12:09:24 PM
+        testOffset("America/Chicago", -1067810460, offset(-1, 6, 0, 0)); // Sunday, March 1, 1936 1:59:00 AM
+        testOffset("America/Chicago", -1067810400, offset(-1, 5, 0, 0)); // Sunday, March 1, 1936 2:00:00 AM
+        testOffset("America/Chicago", -1045432860, offset(-1, 5, 0, 0)); // Sunday, November 15, 1936 1:59:00 AM
+        testOffset("America/Chicago", -1045432800, offset(-1, 6, 0, 0)); // Sunday, November 15, 1936 2:00:00 AM
+
+        testOffset("Europe/London", -3852662401, offset(-1, 0, 1, 15)); // Tuesday, November 30, 1847 11:59:59 PM
+        testOffset("Europe/London", -3852662400, offset(+1, 0, 0, 0)); // Wednesday, December 1, 1847 12:00:00 AM
+        testOffset("Europe/London", -37238401, offset(+1, 0, 0, 0)); // Saturday, October 26, 1968 11:59:59 PM
+        testOffset("Europe/London", -37238400, offset(+1, 1, 0, 0)); // Sunday, October 27, 1968 12:00:00 AM
+        testOffset("Europe/London", 57722399, offset(+1, 1, 0, 0)); // Sunday, October 31, 1971 1:59:59 AM
+        testOffset("Europe/London", 57722400, offset(+1, 0, 0, 0)); // Sunday, October 31, 1971 2:00:00 AM
+
+        testOffset("UTC", -1641846268, offset(+1, 0, 0, 0));
+        testOffset("UTC", 0, offset(+1, 0, 0, 0));
+        testOffset("UTC", 1641846268, offset(+1, 0, 0, 0));
+
+        testOffset("Etc/GMT+4", -1641846268, offset(-1, 4, 0, 0));
+        testOffset("Etc/GMT+5", 0, offset(-1, 5, 0, 0));
+        testOffset("Etc/GMT+6", 1641846268, offset(-1, 6, 0, 0));
+
+        testOffset("Etc/GMT-12", -1641846268, offset(+1, 12, 0, 0));
+        testOffset("Etc/GMT-13", 0, offset(+1, 13, 0, 0));
+        testOffset("Etc/GMT-14", 1641846268, offset(+1, 14, 0, 0));
     });
 
     test("custom offset", () => {

+ 16 - 9
Userland/Libraries/LibJS/Tests/builtins/Temporal/TimeZone/TimeZone.prototype.getOffsetStringFor.js

@@ -4,15 +4,22 @@ describe("correct behavior", () => {
     });
 
     test("basic functionality", () => {
-        const timeZone = new Temporal.TimeZone("UTC");
-        const instant = new Temporal.Instant(0n);
-        expect(timeZone.getOffsetStringFor(instant)).toBe("+00:00");
-    });
-
-    test("custom offset", () => {
-        const timeZone = new Temporal.TimeZone("+01:30");
-        const instant = new Temporal.Instant(0n);
-        expect(timeZone.getOffsetStringFor(instant)).toBe("+01:30");
+        const values = [
+            ["UTC", "+00:00"],
+            ["GMT", "+00:00"],
+            ["Etc/GMT+12", "-12:00"],
+            ["Etc/GMT-12", "+12:00"],
+            ["Europe/London", "+00:00"],
+            ["Europe/Berlin", "+01:00"],
+            ["America/New_York", "-05:00"],
+            ["America/Los_Angeles", "-08:00"],
+            ["+00:00", "+00:00"],
+            ["+01:30", "+01:30"],
+        ];
+        for (const [arg, expected] of values) {
+            const instant = new Temporal.Instant(1600000000000000000n);
+            expect(new Temporal.TimeZone(arg).getOffsetStringFor(instant)).toBe(expected);
+        }
     });
 });