diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp index 866b96c185..cb22c07da8 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.cpp @@ -412,6 +412,21 @@ Optional to_temporal_time_record(GlobalObject& global_o return result; } +// 4.5.10 TemporalTimeToString ( hour, minute, second, millisecond, microsecond, nanosecond, precision ), https://tc39.es/proposal-temporal/#sec-temporal-temporaltimetostring +String temporal_time_to_string(u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, Variant const& precision) +{ + // 1. Assert: hour, minute, second, millisecond, microsecond and nanosecond are integers. + + // 2. Let hour be hour formatted as a two-digit decimal number, padded to the left with a zero if necessary. + // 3. Let minute be minute formatted as a two-digit decimal number, padded to the left with a zero if necessary. + + // 4. Let seconds be ! FormatSecondsStringPart(second, millisecond, microsecond, nanosecond, precision). + auto seconds = format_seconds_string_part(second, millisecond, microsecond, nanosecond, precision); + + // 5. Return the string-concatenation of hour, the code unit 0x003A (COLON), minute, and seconds. + return String::formatted("{:02}:{:02}{}", hour, minute, seconds); +} + // 4.5.11 CompareTemporalTime ( h1, min1, s1, ms1, mus1, ns1, h2, min2, s2, ms2, mus2, ns2 ), https://tc39.es/proposal-temporal/#sec-temporal-comparetemporaltime i8 compare_temporal_time(u8 hour1, u8 minute1, u8 second1, u16 millisecond1, u16 microsecond1, u16 nanosecond1, u8 hour2, u8 minute2, u8 second2, u16 millisecond2, u16 microsecond2, u16 nanosecond2) { @@ -469,4 +484,107 @@ i8 compare_temporal_time(u8 hour1, u8 minute1, u8 second1, u16 millisecond1, u16 return 0; } +// 4.5.13 RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, increment, unit, roundingMode [ , dayLengthNs ] ), https://tc39.es/proposal-temporal/#sec-temporal-roundtime +DaysAndTime round_time(GlobalObject& global_object, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, u64 increment, StringView unit, StringView rounding_mode, Optional day_length_ns) +{ + // 1. Assert: hour, minute, second, millisecond, microsecond, nanosecond, and increment are integers. + + // 2. Let fractionalSecond be nanosecond × 10−9 + microsecond × 10−6 + millisecond × 10−3 + second. + double fractional_second = nanosecond * 0.000000001 + microsecond * 0.000001 + millisecond * 0.001 + second; + double quantity; + + // 3. If unit is "day", then + if (unit == "day"sv) { + // a. If dayLengthNs is not present, set it to 8.64 × 10^13. + if (!day_length_ns.has_value()) + day_length_ns = 86400000000000; + + // b. Let quantity be (((((hour × 60 + minute) × 60 + second) × 1000 + millisecond) × 1000 + microsecond) × 1000 + nanosecond) / dayLengthNs. + quantity = (((((hour * 60 + minute) * 60 + second) * 1000 + millisecond) * 1000 + microsecond) * 1000 + nanosecond) / *day_length_ns; + } + // 4. Else if unit is "hour", then + else if (unit == "hour"sv) { + // a. Let quantity be (fractionalSecond / 60 + minute) / 60 + hour. + quantity = (fractional_second / 60 + minute) / 60 + hour; + } + // 5. Else if unit is "minute", then + else if (unit == "minute"sv) { + // a. Let quantity be fractionalSecond / 60 + minute. + quantity = fractional_second / 60 + minute; + } + // 6. Else if unit is "second", then + else if (unit == "second"sv) { + // a. Let quantity be fractionalSecond. + quantity = fractional_second; + } + // 7. Else if unit is "millisecond", then + else if (unit == "millisecond"sv) { + // a. Let quantity be nanosecond × 10−6 + microsecond × 10−3 + millisecond. + quantity = nanosecond * 0.000001 + 0.001 * microsecond + millisecond; + } + // 8. Else if unit is "microsecond", then + else if (unit == "microsecond"sv) { + // a. Let quantity be nanosecond × 10−3 + microsecond. + quantity = nanosecond * 0.001 + microsecond; + } + // 9. Else, + else { + // a. Assert: unit is "nanosecond". + VERIFY(unit == "nanosecond"sv); + + // b. Let quantity be nanosecond. + quantity = nanosecond; + } + + // FIXME: This doesn't seem right... + auto* quantity_bigint = js_bigint(global_object.vm(), Crypto::SignedBigInteger::create_from((u64)quantity)); + + // 10. Let result be ! RoundNumberToIncrement(quantity, increment, roundingMode). + auto* result = round_number_to_increment(global_object, *quantity_bigint, increment, rounding_mode); + + auto result_i64 = (i64)result->big_integer().to_double(); + + // If unit is "day", then + if (unit == "day"sv) { + // a. Return the Record { [[Days]]: result, [[Hour]]: 0, [[Minute]]: 0, [[Second]]: 0, [[Millisecond]]: 0, [[Microsecond]]: 0, [[Nanosecond]]: 0 }. + return DaysAndTime { .days = (i32)result_i64, .hour = 0, .minute = 0, .second = 0, .millisecond = 0, .microsecond = 0, .nanosecond = 0 }; + } + + // 12. If unit is "hour", then + if (unit == "hour"sv) { + // a. Return ! BalanceTime(result, 0, 0, 0, 0, 0). + return balance_time(result_i64, 0, 0, 0, 0, 0); + } + + // 13. If unit is "minute", then + if (unit == "minute"sv) { + // a. Return ! BalanceTime(hour, result, 0, 0, 0, 0). + return balance_time(hour, result_i64, 0, 0, 0, 0); + } + + // 14. If unit is "second", then + if (unit == "second"sv) { + // a. Return ! BalanceTime(hour, minute, result, 0, 0, 0). + return balance_time(hour, minute, result_i64, 0, 0, 0); + } + + // 15. If unit is "millisecond", then + if (unit == "millisecond"sv) { + // a. Return ! BalanceTime(hour, minute, second, result, 0, 0). + return balance_time(hour, minute, second, result_i64, 0, 0); + } + + // 16. If unit is "microsecond", then + if (unit == "microsecond"sv) { + // a. Return ! BalanceTime(hour, minute, second, millisecond, result, 0). + return balance_time(hour, minute, second, millisecond, result_i64, 0); + } + + // 17. Assert: unit is "nanosecond". + VERIFY(unit == "nanosecond"sv); + + // 18. Return ! BalanceTime(hour, minute, second, millisecond, microsecond, result). + return balance_time(hour, minute, second, millisecond, microsecond, result_i64); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.h index aa7b78ff6b..d2299c3407 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTime.h @@ -98,6 +98,8 @@ DaysAndTime balance_time(i64 hour, i64 minute, i64 second, i64 millisecond, i64 TemporalTime constrain_time(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond); PlainTime* create_temporal_time(GlobalObject&, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, FunctionObject* new_target = nullptr); Optional to_temporal_time_record(GlobalObject&, Object& temporal_time_like); +String temporal_time_to_string(u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, Variant const& precision); i8 compare_temporal_time(u8 hour1, u8 minute1, u8 second1, u16 millisecond1, u16 microsecond1, u16 nanosecond1, u8 hour2, u8 minute2, u8 second2, u16 millisecond2, u16 microsecond2, u16 nanosecond2); +DaysAndTime round_time(GlobalObject&, u8 hour, u8 minute, u8 second, u16 millisecond, u16 microsecond, u16 nanosecond, u64 increment, StringView unit, StringView rounding_mode, Optional day_length_ns = {}); } diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp index c45711b311..33b289873e 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.cpp @@ -42,6 +42,7 @@ void PlainTimePrototype::initialize(GlobalObject& global_object) define_native_function(vm.names.equals, equals, 1, attr); define_native_function(vm.names.toPlainDateTime, to_plain_date_time, 1, attr); define_native_function(vm.names.getISOFields, get_iso_fields, 0, attr); + define_native_function(vm.names.toString, to_string, 0, attr); define_native_function(vm.names.valueOf, value_of, 0, attr); } @@ -355,6 +356,39 @@ JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::get_iso_fields) return fields; } +// 4.3.20 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 = typed_this(global_object); + if (vm.exception()) + return {}; + + // 3. Set options to ? GetOptionsObject(options). + auto* options = get_options_object(global_object, vm.argument(0)); + if (vm.exception()) + return {}; + + // 4. Let precision be ? ToSecondsStringPrecision(options). + auto precision = to_seconds_string_precision(global_object, *options); + if (vm.exception()) + return {}; + + // 5. Let roundingMode be ? ToTemporalRoundingMode(options, "trunc"). + auto rounding_mode = to_temporal_rounding_mode(global_object, *options, "trunc"sv); + if (vm.exception()) + return {}; + + // 6. Let roundResult be ! RoundTime(temporalTime.[[ISOHour]], temporalTime.[[ISOMinute]], temporalTime.[[ISOSecond]], temporalTime.[[ISOMillisecond]], temporalTime.[[ISOMicrosecond]], temporalTime.[[ISONanosecond]], precision.[[Increment]], precision.[[Unit]], roundingMode). + auto round_result = round_time(global_object, temporal_time->iso_hour(), temporal_time->iso_minute(), temporal_time->iso_second(), temporal_time->iso_millisecond(), temporal_time->iso_microsecond(), temporal_time->iso_nanosecond(), precision->increment, precision->unit, *rounding_mode); + + // FIXME: Hey spec, this cannot fail... + // 7. Return ? TemporalTimeToString(roundResult.[[Hour]], roundResult.[[Minute]], roundResult.[[Second]], roundResult.[[Millisecond]], roundResult.[[Microsecond]], roundResult.[[Nanosecond]], precision.[[Precision]]). + auto string = temporal_time_to_string(round_result.hour, round_result.minute, round_result.second, round_result.millisecond, round_result.microsecond, round_result.nanosecond, precision->precision); + return js_string(vm, move(string)); +} + // 4.3.23 Temporal.PlainTime.prototype.valueOf ( ), https://tc39.es/proposal-temporal/#sec-temporal.plaintime.prototype.valueof JS_DEFINE_NATIVE_FUNCTION(PlainTimePrototype::value_of) { diff --git a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h index 9296b4e4c6..f3e99b6dd1 100644 --- a/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Temporal/PlainTimePrototype.h @@ -30,6 +30,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(equals); JS_DECLARE_NATIVE_FUNCTION(to_plain_date_time); JS_DECLARE_NATIVE_FUNCTION(get_iso_fields); + JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(value_of); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toString.js b/Userland/Libraries/LibJS/Tests/builtins/Temporal/PlainTime/PlainTime.prototype.toString.js new file mode 100644 index 0000000000..cd82fae03f --- /dev/null +++ b/Userland/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 a Temporal.PlainTime"); + }); +});