diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp index d586398c4ba..010f28b4652 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp +++ b/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp @@ -441,6 +441,16 @@ ThrowCompletionOr 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(time.millisecond) * 1'000'000) + (static_cast(time.microsecond) * 1000) + static_cast(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(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(); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTime.h b/Libraries/LibJS/Runtime/Temporal/PlainTime.h index 7267c2c047d..000defe00c6 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTime.h +++ b/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> create_temporal_time(VM&, Time const&, GC::Ptr new_target = {}); ThrowCompletionOr 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); } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp b/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp index d9c2d7481c7..bc59ede9f18 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp +++ b/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::Hour) + return vm.throw_completion(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 {})); +} + } diff --git a/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h b/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h index 1615ff38508..eaec3b68e9c 100644 --- a/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h +++ b/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); }; } diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toJSON.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toJSON.js new file mode 100644 index 00000000000..2a94c3d2b3a --- /dev/null +++ b/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"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toLocaleString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toLocaleString.js new file mode 100644 index 00000000000..f0b2ab1a0b0 --- /dev/null +++ b/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"); + }); +}); diff --git a/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toString.js b/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toString.js new file mode 100644 index 00000000000..3f5ccd9fa8c --- /dev/null +++ b/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"); + }); +});