浏览代码

LibJS: Implement stringification Temporal.PlainTime prototypes

Timothy Flynn 8 月之前
父节点
当前提交
ab3c825fa8

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

@@ -441,6 +441,16 @@ ThrowCompletionOr<TemporalTimeLike> to_temporal_time_record(VM& vm, Object const
     return result;
 }
 
+// 4.5.13 TimeRecordToString ( time, precision ), https://tc39.es/proposal-temporal/#sec-temporal-timerecordtostring
+String time_record_to_string(Time const& time, SecondsStringPrecision::Precision precision)
+{
+    // 1. Let subSecondNanoseconds be time.[[Millisecond]] × 10**6 + time.[[Microsecond]] × 10**3 + time.[[Nanosecond]].
+    auto sub_second_nanoseconds = (static_cast<u64>(time.millisecond) * 1'000'000) + (static_cast<u64>(time.microsecond) * 1000) + static_cast<u64>(time.nanosecond);
+
+    // 2. Return FormatTimeString(time.[[Hour]], time.[[Minute]], time.[[Second]], subSecondNanoseconds, precision).
+    return format_time_string(time.hour, time.minute, time.second, sub_second_nanoseconds, precision);
+}
+
 // 4.5.14 CompareTimeRecord ( time1, time2 ), https://tc39.es/proposal-temporal/#sec-temporal-comparetimerecord
 i8 compare_time_record(Time const& time1, Time const& time2)
 {
@@ -499,4 +509,101 @@ Time add_time(Time const& time, TimeDuration const& time_duration)
     return balance_time(time.hour, time.minute, time.second, time.millisecond, time.microsecond, nanoseconds);
 }
 
+// 4.5.16 RoundTime ( time, increment, unit, roundingMode ), https://tc39.es/proposal-temporal/#sec-temporal-roundtime
+Time round_time(Time const& time, u64 increment, Unit unit, RoundingMode rounding_mode)
+{
+    double quantity = 0;
+
+    switch (unit) {
+    // 1. If unit is DAY or HOUR, then
+    case Unit::Day:
+    case Unit::Hour:
+        // a. Let quantity be ((((time.[[Hour]] × 60 + time.[[Minute]]) × 60 + time.[[Second]]) × 1000 + time.[[Millisecond]]) × 1000 + time.[[Microsecond]]) × 1000 + time.[[Nanosecond]].
+        quantity = ((((time.hour * 60.0 + time.minute) * 60.0 + time.second) * 1000.0 + time.millisecond) * 1000.0 + time.microsecond) * 1000.0 + time.nanosecond;
+        break;
+
+    // 2. Else if unit is MINUTE, then
+    case Unit::Minute:
+        // a. Let quantity be (((time.[[Minute]] × 60 + time.[[Second]]) × 1000 + time.[[Millisecond]]) × 1000 + time.[[Microsecond]]) × 1000 + time.[[Nanosecond]].
+        quantity = (((time.minute * 60.0 + time.second) * 1000.0 + time.millisecond) * 1000.0 + time.microsecond) * 1000.0 + time.nanosecond;
+        break;
+
+    // 3. Else if unit is SECOND, then
+    case Unit::Second:
+        // a. Let quantity be ((time.[[Second]] × 1000 + time.[[Millisecond]]) × 1000 + time.[[Microsecond]]) × 1000 + time.[[Nanosecond]].
+        quantity = ((time.second * 1000.0 + time.millisecond) * 1000.0 + time.microsecond) * 1000.0 + time.nanosecond;
+        break;
+
+    // 4. Else if unit is MILLISECOND, then
+    case Unit::Millisecond:
+        // a. Let quantity be (time.[[Millisecond]] × 1000 + time.[[Microsecond]]) × 1000 + time.[[Nanosecond]].
+        quantity = (time.millisecond * 1000.0 + time.microsecond) * 1000.0 + time.nanosecond;
+        break;
+
+    // 5. Else if unit is MICROSECOND, then
+    case Unit::Microsecond:
+        // a. Let quantity be time.[[Microsecond]] × 1000 + time.[[Nanosecond]].
+        quantity = time.microsecond * 1000.0 + time.nanosecond;
+        break;
+
+    // 6. Else,
+    case Unit::Nanosecond:
+        // a. Assert: unit is NANOSECOND.
+        // b. Let quantity be time.[[Nanosecond]].
+        quantity = time.nanosecond;
+        break;
+
+    default:
+        VERIFY_NOT_REACHED();
+    }
+
+    // 7. Let unitLength be the value in the "Length in Nanoseconds" column of the row of Table 21 whose "Value" column contains unit.
+    auto unit_length = temporal_unit_length_in_nanoseconds(unit).to_u64();
+
+    // 8. Let result be RoundNumberToIncrement(quantity, increment × unitLength, roundingMode) / unitLength.
+    auto result = round_number_to_increment(quantity, increment * unit_length, rounding_mode) / static_cast<double>(unit_length);
+
+    switch (unit) {
+    // 9. If unit is DAY, then
+    case Unit::Day:
+        // a. Return CreateTimeRecord(0, 0, 0, 0, 0, 0, result).
+        return create_time_record(0, 0, 0, 0, 0, 0, result);
+
+    // 10. If unit is HOUR, then
+    case Unit::Hour:
+        // a. Return BalanceTime(result, 0, 0, 0, 0, 0).
+        return balance_time(result, 0, 0, 0, 0, 0);
+
+    // 11. If unit is MINUTE, then
+    case Unit::Minute:
+        // a. Return BalanceTime(time.[[Hour]], result, 0, 0, 0, 0).
+        return balance_time(time.hour, result, 0, 0, 0, 0);
+
+    // 12. If unit is SECOND, then
+    case Unit::Second:
+        // a. Return BalanceTime(time.[[Hour]], time.[[Minute]], result, 0, 0, 0).
+        return balance_time(time.hour, time.minute, result, 0, 0, 0);
+
+    // 13. If unit is MILLISECOND, then
+    case Unit::Millisecond:
+        // a. Return BalanceTime(time.[[Hour]], time.[[Minute]], time.[[Second]], result, 0, 0).
+        return balance_time(time.hour, time.minute, time.second, result, 0, 0);
+
+    // 14. If unit is MICROSECOND, then
+    case Unit::Microsecond:
+        // a. Return BalanceTime(time.[[Hour]], time.[[Minute]], time.[[Second]], time.[[Millisecond]], result, 0).
+        return balance_time(time.hour, time.minute, time.second, time.millisecond, result, 0);
+
+    // 15. Assert: unit is NANOSECOND.
+    case Unit::Nanosecond:
+        // 16. Return BalanceTime(time.[[Hour]], time.[[Minute]], time.[[Second]], time.[[Millisecond]], time.[[Microsecond]], result).
+        return balance_time(time.hour, time.minute, time.second, time.millisecond, time.microsecond, result);
+
+    default:
+        break;
+    }
+
+    VERIFY_NOT_REACHED();
+}
+
 }

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

@@ -59,7 +59,9 @@ Time balance_time(double hour, double minute, double second, double millisecond,
 Time balance_time(double hour, double minute, double second, double millisecond, double microsecond, TimeDuration const& nanosecond);
 ThrowCompletionOr<GC::Ref<PlainTime>> create_temporal_time(VM&, Time const&, GC::Ptr<FunctionObject> new_target = {});
 ThrowCompletionOr<TemporalTimeLike> to_temporal_time_record(VM&, Object const& temporal_time_like, Completeness = Completeness::Complete);
+String time_record_to_string(Time const&, SecondsStringPrecision::Precision);
 i8 compare_time_record(Time const&, Time const&);
 Time add_time(Time const&, TimeDuration const& time_duration);
+Time round_time(Time const&, u64 increment, Unit, RoundingMode);
 
 }

+ 63 - 0
Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp

@@ -33,6 +33,11 @@ void PlainTimePrototype::initialize(Realm& realm)
     define_native_accessor(realm, vm.names.millisecond, millisecond_getter, {}, Attribute::Configurable);
     define_native_accessor(realm, vm.names.microsecond, microsecond_getter, {}, Attribute::Configurable);
     define_native_accessor(realm, vm.names.nanosecond, nanosecond_getter, {}, Attribute::Configurable);
+
+    u8 attr = Attribute::Writable | Attribute::Configurable;
+    define_native_function(realm, vm.names.toString, to_string, 0, attr);
+    define_native_function(realm, vm.names.toLocaleString, to_locale_string, 0, attr);
+    define_native_function(realm, vm.names.toJSON, to_json, 0, attr);
 }
 
 // 4.3.3 get Temporal.PlainTime.prototype.hour, https://tc39.es/proposal-temporal/#sec-get-temporal.plaintime.prototype.hour
@@ -62,4 +67,62 @@ void PlainTimePrototype::initialize(Realm& realm)
 JS_ENUMERATE_PLAIN_TIME_FIELDS
 #undef __JS_ENUMERATE
 
+// 4.3.16 Temporal.PlainTime.prototype.toString ( [ options ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.tostring
+JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::to_string)
+{
+    // 1. Let temporalTime be the this value.
+    // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]).
+    auto temporal_time = TRY(typed_this_object(vm));
+
+    // 3. Let resolvedOptions be ? GetOptionsObject(options).
+    auto resolved_options = TRY(get_options_object(vm, vm.argument(0)));
+
+    // 4. NOTE: The following steps read options and perform independent validation in alphabetical order
+    //    (GetTemporalFractionalSecondDigitsOption reads "fractionalSecondDigits" and GetRoundingModeOption reads "roundingMode").
+
+    // 5. Let digits be ? GetTemporalFractionalSecondDigitsOption(resolvedOptions).
+    auto digits = TRY(get_temporal_fractional_second_digits_option(vm, resolved_options));
+
+    // 6. Let roundingMode be ? GetRoundingModeOption(resolvedOptions, TRUNC).
+    auto rounding_mode = TRY(get_rounding_mode_option(vm, resolved_options, RoundingMode::Trunc));
+
+    // 7. Let smallestUnit be ? GetTemporalUnitValuedOption(resolvedOptions, "smallestUnit", TIME, UNSET).
+    auto smallest_unit = TRY(get_temporal_unit_valued_option(vm, resolved_options, vm.names.smallestUnit, UnitGroup::Time, Unset {}));
+
+    // 8. If smallestUnit is HOUR, throw a RangeError exception.
+    if (auto const* unit = smallest_unit.get_pointer<Unit>(); unit && *unit == Unit::Hour)
+        return vm.throw_completion<RangeError>(ErrorType::OptionIsNotValidValue, temporal_unit_to_string(*unit), vm.names.smallestUnit);
+
+    // 9. Let precision be ToSecondsStringPrecisionRecord(smallestUnit, digits).
+    auto precision = to_seconds_string_precision_record(smallest_unit, digits);
+
+    // 10. Let roundResult be RoundTime(temporalTime.[[Time]], precision.[[Increment]], precision.[[Unit]], roundingMode).
+    auto round_result = round_time(temporal_time->time(), precision.increment, precision.unit, rounding_mode);
+
+    // 11. Return TimeRecordToString(roundResult, precision.[[Precision]]).
+    return PrimitiveString::create(vm, time_record_to_string(round_result, precision.precision));
+}
+
+// 4.3.17 Temporal.PlainTime.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.tolocalestring
+JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::to_locale_string)
+{
+    // 1. Let temporalTime be the this value.
+    // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]).
+    auto temporal_time = TRY(typed_this_object(vm));
+
+    // 3. Return TimeRecordToString(temporalTime.[[Time]], AUTO).
+    return PrimitiveString::create(vm, time_record_to_string(temporal_time->time(), Auto {}));
+}
+
+// 4.3.18 Temporal.PlainTime.prototype.toJSON ( ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.tojson
+JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::to_json)
+{
+    // 1. Let temporalTime be the this value.
+    // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]).
+    auto temporal_time = TRY(typed_this_object(vm));
+
+    // 3. Return TimeRecordToString(temporalTime.[[Time]], AUTO).
+    return PrimitiveString::create(vm, time_record_to_string(temporal_time->time(), Auto {}));
+}
+
 }

+ 3 - 0
Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h

@@ -29,6 +29,9 @@ private:
     JS_DECLARE_NATIVE_FUNCTION(millisecond_getter);
     JS_DECLARE_NATIVE_FUNCTION(microsecond_getter);
     JS_DECLARE_NATIVE_FUNCTION(nanosecond_getter);
+    JS_DECLARE_NATIVE_FUNCTION(to_string);
+    JS_DECLARE_NATIVE_FUNCTION(to_locale_string);
+    JS_DECLARE_NATIVE_FUNCTION(to_json);
 };
 
 }

+ 18 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toJSON.js

@@ -0,0 +1,18 @@
+describe("correct behavior", () => {
+    test("length is 0", () => {
+        expect(Temporal.PlainTime.prototype.toJSON).toHaveLength(0);
+    });
+
+    test("basic functionality", () => {
+        const plainTime = new Temporal.PlainTime(18, 14, 47, 123, 456, 789);
+        expect(plainTime.toJSON()).toBe("18:14:47.123456789");
+    });
+});
+
+describe("errors", () => {
+    test("this value must be a Temporal.PlainTime object", () => {
+        expect(() => {
+            Temporal.PlainTime.prototype.toJSON.call("foo");
+        }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainTime");
+    });
+});

+ 18 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toLocaleString.js

@@ -0,0 +1,18 @@
+describe("correct behavior", () => {
+    test("length is 0", () => {
+        expect(Temporal.PlainTime.prototype.toLocaleString).toHaveLength(0);
+    });
+
+    test("basic functionality", () => {
+        const plainTime = new Temporal.PlainTime(18, 14, 47, 123, 456, 789);
+        expect(plainTime.toLocaleString()).toBe("18:14:47.123456789");
+    });
+});
+
+describe("errors", () => {
+    test("this value must be a Temporal.PlainTime object", () => {
+        expect(() => {
+            Temporal.PlainTime.prototype.toLocaleString.call("foo");
+        }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainTime");
+    });
+});

+ 59 - 0
Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toString.js

@@ -0,0 +1,59 @@
+describe("correct behavior", () => {
+    test("length is 0", () => {
+        expect(Temporal.PlainTime.prototype.toString).toHaveLength(0);
+    });
+
+    test("basic functionality", () => {
+        const plainTime = new Temporal.PlainTime(18, 14, 47, 123, 456, 789);
+        expect(plainTime.toString()).toBe("18:14:47.123456789");
+    });
+
+    test("fractionalSecondDigits option", () => {
+        const plainTime = new Temporal.PlainTime(18, 14, 47, 123, 456);
+        const values = [
+            ["auto", "18:14:47.123456"],
+            [0, "18:14:47"],
+            [1, "18:14:47.1"],
+            [2, "18:14:47.12"],
+            [3, "18:14:47.123"],
+            [4, "18:14:47.1234"],
+            [5, "18:14:47.12345"],
+            [6, "18:14:47.123456"],
+            [7, "18:14:47.1234560"],
+            [8, "18:14:47.12345600"],
+            [9, "18:14:47.123456000"],
+        ];
+        for (const [fractionalSecondDigits, expected] of values) {
+            const options = { fractionalSecondDigits };
+            expect(plainTime.toString(options)).toBe(expected);
+        }
+
+        // Ignored when smallestUnit is given
+        expect(plainTime.toString({ smallestUnit: "minute", fractionalSecondDigits: 9 })).toBe(
+            "18:14"
+        );
+    });
+
+    test("smallestUnit option", () => {
+        const plainTime = new Temporal.PlainTime(18, 14, 47, 123, 456, 789);
+        const values = [
+            ["minute", "18:14"],
+            ["second", "18:14:47"],
+            ["millisecond", "18:14:47.123"],
+            ["microsecond", "18:14:47.123456"],
+            ["nanosecond", "18:14:47.123456789"],
+        ];
+        for (const [smallestUnit, expected] of values) {
+            const options = { smallestUnit };
+            expect(plainTime.toString(options)).toBe(expected);
+        }
+    });
+});
+
+describe("errors", () => {
+    test("this value must be a Temporal.PlainTime object", () => {
+        expect(() => {
+            Temporal.PlainTime.prototype.toString.call("foo");
+        }).toThrowWithMessage(TypeError, "Not an object of type Temporal.PlainTime");
+    });
+});