From 8d9b6e64b35e75e49cd245b7862c6152bb709916 Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Sun, 12 May 2024 17:18:05 +0200 Subject: [PATCH 1/9] Add years and months to human date formatter --- book/src/conversion-functions.md | 2 +- book/src/date-and-time.md | 2 +- examples/datetime_human_tests.nbt | 42 +++++++++++++++++++++ numbat/modules/datetime/human.nbt | 61 ++++++++++++++++--------------- 4 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 examples/datetime_human_tests.nbt diff --git a/book/src/conversion-functions.md b/book/src/conversion-functions.md index 6dfae0d3..ffcaece2 100644 --- a/book/src/conversion-functions.md +++ b/book/src/conversion-functions.md @@ -12,7 +12,7 @@ now() -> unixtime # Convert a date and time to a different timezone now() -> tz("Asia/Kathmandu") -# Convert a duration to days, hours, minutes, seconds +# Convert a duration to years, months, days, hours, minutes, seconds 10 million seconds -> human # Convert a number to its binary representation diff --git a/book/src/date-and-time.md b/book/src/date-and-time.md index 8512766c..ab86c7e1 100644 --- a/book/src/date-and-time.md +++ b/book/src/date-and-time.md @@ -27,7 +27,7 @@ now() -> unixtime # What is the date corresponding to the UNIX timestamp 1707568901? from_unixtime(1707568901) -# How long are one million seconds in days, hours, minutes, seconds +# How long are one million seconds in years, months, days, hours, minutes, seconds 1 million seconds -> human ``` diff --git a/examples/datetime_human_tests.nbt b/examples/datetime_human_tests.nbt new file mode 100644 index 00000000..ad59365c --- /dev/null +++ b/examples/datetime_human_tests.nbt @@ -0,0 +1,42 @@ +assert((0 second -> human) == "0 seconds") +assert((1 second -> human) == "1 second") +assert((5 second -> human) == "5 seconds") +assert((1.5 second -> human) == "1.5 seconds") + +assert((60 seconds -> human) == "1 minute") +assert((73 seconds -> human) == "1 minute + 13 seconds") +assert((120 seconds -> human) == "2 minutes") +assert((60.1 seconds -> human) == "1 minute + 0.1 seconds") +assert((1 minute -> human) == "1 minute") +assert((1.25 minute -> human) == "1 minute + 15 seconds") +assert((2.5 minute -> human) == "2 minutes + 30 seconds") + +assert((1 hour -> human) == "1 hour") +assert((1.5 hour -> human) == "1 hour + 30 minutes") +assert((2 hour -> human) == "2 hours") +assert((1 hour + 1 sec -> human) == "1 hour + 1 second") + +assert((1 day -> human) == "1 day") +assert((1.37 day -> human) == "1 day + 8 hours + 52 minutes + 48 seconds") + +assert((1 week -> human) == "7 days") +assert((1.5 weeks -> human) == "10 days + 12 hours") +assert((2 weeks -> human) == "14 days") + +assert((2 month -> human) == "2 months") +assert((3 yr + 2 month -> human) == "3 years + 2 months") +assert((10 yr + 2 s -> human) == "10 years + 2 seconds") + +assert((1 sidereal_day -> human) == "23 hours + 56 minutes + 4.091 seconds") + +assert((10000 days -> human) == "27 years + 4 months + 16 days + 17 hours + 7 minutes + 28.583 seconds") +assert((50 million days -> human) == "136_895 years + 5 months + 18 days + 11 hours + 25 minutes + 9.591 seconds") + +assert((1e12 days -> human) == "2_737_909_345 years + 12 days + 18 hours + 17 minutes + 24.141 seconds") +assert((1e15 days -> human) == "2_737_909_345_034 years + 11 months + 8 days + 22 hours + 40 minutes + 18.702 seconds") + +assert((1 ms -> human) == "0.001 seconds") +assert((1 µs -> human) == "0.000001 seconds") +assert((1 ns -> human) == "1.0e-9 seconds") +assert((1234 ns -> human) == "0.000001234 seconds") +assert((1s + 1234 ns -> human) == "1 second") diff --git a/numbat/modules/datetime/human.nbt b/numbat/modules/datetime/human.nbt index 62a8a5a5..a2070429 100644 --- a/numbat/modules/datetime/human.nbt +++ b/numbat/modules/datetime/human.nbt @@ -3,37 +3,40 @@ use core::strings use units::si use datetime::functions -fn _human_num_days(time: Time) -> Scalar = floor(time / day) - fn _human_join(a: String, b: String) -> String = - if str_slice(a, 0, 2) == "0 " then b else if str_slice(b, 0, 2) == "0 " then a else "{a} + {b}" - -fn _remove_plural_suffix(str: String) -> String = - if str_slice(str, 0, 2) == "1 " then str_slice(str, 0, str_length(str) - 1) else str - -fn _human_seconds(dt: DateTime) -> String = - _remove_plural_suffix(format_datetime("%-S%.f seconds", dt)) - -fn _human_minutes(dt: DateTime) -> String = - _remove_plural_suffix(format_datetime("%-M minutes", dt)) - -fn _human_hours(dt: DateTime) -> String = - _remove_plural_suffix(format_datetime("%-H hours", dt)) - -fn _human_days(num_days: Scalar) -> String = - _remove_plural_suffix("{num_days} days") - -fn _human_readable_duration(time: Time, dt: DateTime, num_days: Scalar) -> String = - _human_join(_human_join(_human_join(_human_days(_human_num_days(time)), _human_hours(dt)), _human_minutes(dt)), _human_seconds(dt)) + if a == "" then b else if b == "" then a else "{a} + {b}" + +fn _prettier(str: String) -> String = + if str_slice(str, 0, 2) == "0 " then "" + else if str_slice(str, 0, 2) == "1 " then str_slice(str, 0, str_length(str) - 1) + else str + +fn _human_years(time: Time) -> String = "{(time -> years) / year // floor} years" -> _prettier +fn _human_months(time: Time) -> String = "{(time -> months) / month // floor} months" -> _prettier +fn _human_days(time: Time) -> String = "{(time -> days) / day // floor} days" -> _prettier +fn _human_hours(time: Time) -> String = "{(time -> hours) / hour // floor} hours" -> _prettier +fn _human_minutes(time: Time) -> String = "{(time -> minutes) / minute // floor} minutes" -> _prettier +fn _human_seconds(time: Time) -> String = str_replace("{(time -> seconds) / second} seconds", ".0 ", " ") -> _prettier + +fn _human_recurse(t: Time, result: String, time_unit: String) -> String = + if time_unit == "year" + then _human_recurse(t - (t -> year // floor) -> ms // round, _human_join(result, t -> _human_years), "month") + else if time_unit == "month" + then _human_recurse(t - (t -> month // floor) -> ms // round, _human_join(result, t -> _human_months), "day") + else if time_unit == "day" + then _human_recurse(t - (t -> day // floor) -> ms // round, _human_join(result, t -> _human_days), "hour") + else if time_unit == "hour" + then _human_recurse(t - (t -> hour // floor) -> ms // round, _human_join(result, t -> _human_hours), "minute") + else if time_unit == "minute" + then _human_recurse(t - (t -> min // floor) -> ms // round, _human_join(result, t -> _human_minutes), "second") + else _human_join(result, (t -> ms // round) -> _human_seconds) +# The seconds are roundother calculsed for durations larger than 1 month because +# we run into floating point precision problems at the nanosecond level at this point -# Implementation details: -# we skip hours/minutes/seconds for durations larger than 1000 days because: -# (a) we run into floating point precision problems at the nanosecond level at this point -# (b) for much larger numbers, we can't convert to DateTimes anymore @name("Human-readable time duration") @url("https://numbat.dev/doc/date-and-time.html") @description("Converts a time duration to a human-readable string in days, hours, minutes and seconds.") -fn human(time: Time) = - if _human_num_days(time) > 1000 - then "{_human_num_days(time)} days" - else _human_readable_duration(time, datetime("0001-01-01T00:00:00Z") + time, _human_num_days(time)) +fn human(time: Time) -> String = + if time == 0 s then "0 seconds" + else if time < 60 s then time -> _human_seconds + else _human_recurse(time, "", "year") From 13591ebbfe615aa40b0003c0087af2a6408e1c63 Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Mon, 13 May 2024 21:03:23 +0200 Subject: [PATCH 2/9] Fix error on modules_are_self_consistent --- numbat/modules/datetime/human.nbt | 1 + 1 file changed, 1 insertion(+) diff --git a/numbat/modules/datetime/human.nbt b/numbat/modules/datetime/human.nbt index a2070429..e8b11206 100644 --- a/numbat/modules/datetime/human.nbt +++ b/numbat/modules/datetime/human.nbt @@ -1,6 +1,7 @@ use core::functions use core::strings use units::si +use units::time use datetime::functions fn _human_join(a: String, b: String) -> String = From ec0f441ee28009a9ed6b6edf4586621e785c9894 Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Mon, 13 May 2024 21:03:52 +0200 Subject: [PATCH 3/9] Manage negative times --- examples/datetime_human_tests.nbt | 3 +++ numbat/modules/datetime/human.nbt | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/datetime_human_tests.nbt b/examples/datetime_human_tests.nbt index ad59365c..c8a4fd99 100644 --- a/examples/datetime_human_tests.nbt +++ b/examples/datetime_human_tests.nbt @@ -40,3 +40,6 @@ assert((1 µs -> human) == "0.000001 seconds") assert((1 ns -> human) == "1.0e-9 seconds") assert((1234 ns -> human) == "0.000001234 seconds") assert((1s + 1234 ns -> human) == "1 second") + +assert((-1 second -> human) == "1 second ago") +assert((-7.89 hour -> human) == "7 hours + 53 minutes + 24 seconds ago") diff --git a/numbat/modules/datetime/human.nbt b/numbat/modules/datetime/human.nbt index e8b11206..c5410ab3 100644 --- a/numbat/modules/datetime/human.nbt +++ b/numbat/modules/datetime/human.nbt @@ -19,6 +19,8 @@ fn _human_hours(time: Time) -> String = "{(time -> hours) / hour // floor fn _human_minutes(time: Time) -> String = "{(time -> minutes) / minute // floor} minutes" -> _prettier fn _human_seconds(time: Time) -> String = str_replace("{(time -> seconds) / second} seconds", ".0 ", " ") -> _prettier +fn _human_manage_past(str: String, time: Time) = str_append(str, if time < 0 s then " ago" else "") + fn _human_recurse(t: Time, result: String, time_unit: String) -> String = if time_unit == "year" then _human_recurse(t - (t -> year // floor) -> ms // round, _human_join(result, t -> _human_years), "month") @@ -39,5 +41,5 @@ fn _human_recurse(t: Time, result: String, time_unit: String) -> String = @description("Converts a time duration to a human-readable string in days, hours, minutes and seconds.") fn human(time: Time) -> String = if time == 0 s then "0 seconds" - else if time < 60 s then time -> _human_seconds - else _human_recurse(time, "", "year") + else if abs(time) < 60 s then _human_manage_past(abs(time) -> _human_seconds, time) + else _human_manage_past(_human_recurse(abs(time), "", "year"), time) From 0e2c58e41f8353ef2b0cb7c441d50bcb0f9dea14 Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Sat, 13 Jul 2024 18:22:44 +0200 Subject: [PATCH 4/9] Change human format to "1156 days + 14 hours + ... (approx. 3 years + 2 months)") --- examples/datetime_human_tests.nbt | 14 +++++++------- numbat/modules/datetime/human.nbt | 17 +++++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/examples/datetime_human_tests.nbt b/examples/datetime_human_tests.nbt index c8a4fd99..535ffc91 100644 --- a/examples/datetime_human_tests.nbt +++ b/examples/datetime_human_tests.nbt @@ -23,17 +23,17 @@ assert((1 week -> human) == "7 days") assert((1.5 weeks -> human) == "10 days + 12 hours") assert((2 weeks -> human) == "14 days") -assert((2 month -> human) == "2 months") -assert((3 yr + 2 month -> human) == "3 years + 2 months") -assert((10 yr + 2 s -> human) == "10 years + 2 seconds") +assert((2 month -> human) == "60 days + 20 hours + 58 minutes + 7.509 seconds (approx. 2 months)") +assert((3 yr + 2 month -> human) == "1156 days + 14 hours + 24 minutes + 22.664 seconds (approx. 3 years + 2 months)") +assert((10 yr + 2 s -> human) == "3652 days + 10 hours + 7 minutes + 32.518 seconds (approx. 10 years + 2 seconds)") assert((1 sidereal_day -> human) == "23 hours + 56 minutes + 4.091 seconds") -assert((10000 days -> human) == "27 years + 4 months + 16 days + 17 hours + 7 minutes + 28.583 seconds") -assert((50 million days -> human) == "136_895 years + 5 months + 18 days + 11 hours + 25 minutes + 9.591 seconds") +assert((10000 days -> human) == "10000 days (approx. 27 years + 4 months + 16 days + 17 hours + 7 minutes + 28.583 seconds)") +assert((50 million days -> human) == "50_000_000 days (approx. 136_895 years + 5 months + 18 days + 11 hours + 25 minutes + 9.591 seconds)") -assert((1e12 days -> human) == "2_737_909_345 years + 12 days + 18 hours + 17 minutes + 24.141 seconds") -assert((1e15 days -> human) == "2_737_909_345_034 years + 11 months + 8 days + 22 hours + 40 minutes + 18.702 seconds") +assert((1e12 days -> human) == "1_000_000_000_000 days (approx. 2_737_909_345 years + 12 days + 18 hours + 17 minutes + 24.141 seconds)") +assert((1e15 days -> human) == "1.0e+15 days (approx. 2_737_909_345_034 years + 11 months + 8 days + 22 hours + 40 minutes + 18.702 seconds)") assert((1 ms -> human) == "0.001 seconds") assert((1 µs -> human) == "0.000001 seconds") diff --git a/numbat/modules/datetime/human.nbt b/numbat/modules/datetime/human.nbt index c5410ab3..d206da00 100644 --- a/numbat/modules/datetime/human.nbt +++ b/numbat/modules/datetime/human.nbt @@ -19,8 +19,6 @@ fn _human_hours(time: Time) -> String = "{(time -> hours) / hour // floor fn _human_minutes(time: Time) -> String = "{(time -> minutes) / minute // floor} minutes" -> _prettier fn _human_seconds(time: Time) -> String = str_replace("{(time -> seconds) / second} seconds", ".0 ", " ") -> _prettier -fn _human_manage_past(str: String, time: Time) = str_append(str, if time < 0 s then " ago" else "") - fn _human_recurse(t: Time, result: String, time_unit: String) -> String = if time_unit == "year" then _human_recurse(t - (t -> year // floor) -> ms // round, _human_join(result, t -> _human_years), "month") @@ -33,13 +31,20 @@ fn _human_recurse(t: Time, result: String, time_unit: String) -> String = else if time_unit == "minute" then _human_recurse(t - (t -> min // floor) -> ms // round, _human_join(result, t -> _human_minutes), "second") else _human_join(result, (t -> ms // round) -> _human_seconds) -# The seconds are roundother calculsed for durations larger than 1 month because -# we run into floating point precision problems at the nanosecond level at this point + +fn _human_manage_past(str: String, time: Time) = str_append(str, if time < 0 s then " ago" else "") + +fn _human_with_biggest_unit(time: Time, biggest_unit: String) -> String = + _human_manage_past(_human_recurse(abs(time), "", biggest_unit), time) + +fn _human_for_long_duration(human_days: String, human_years: String) -> String = + "{human_days} (approx. {human_years})" @name("Human-readable time duration") @url("https://numbat.dev/doc/date-and-time.html") @description("Converts a time duration to a human-readable string in days, hours, minutes and seconds.") fn human(time: Time) -> String = if time == 0 s then "0 seconds" - else if abs(time) < 60 s then _human_manage_past(abs(time) -> _human_seconds, time) - else _human_manage_past(_human_recurse(abs(time), "", "year"), time) + else if abs(time) < 60 seconds then _human_manage_past(abs(time) -> _human_seconds, time) + else if abs(time) < 2 months then _human_with_biggest_unit(time, "day") + else _human_for_long_duration(_human_with_biggest_unit(time, "day"), _human_with_biggest_unit(time, "year")) From 78d94ccee0c584fe56788ed360f5b7a96311f7d6 Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Sat, 13 Jul 2024 19:02:20 +0200 Subject: [PATCH 5/9] Impact test with new format --- examples/datetime_human_tests.nbt | 45 ------------------------------- examples/tests/human.nbt | 25 ++++++++++------- 2 files changed, 16 insertions(+), 54 deletions(-) delete mode 100644 examples/datetime_human_tests.nbt diff --git a/examples/datetime_human_tests.nbt b/examples/datetime_human_tests.nbt deleted file mode 100644 index 535ffc91..00000000 --- a/examples/datetime_human_tests.nbt +++ /dev/null @@ -1,45 +0,0 @@ -assert((0 second -> human) == "0 seconds") -assert((1 second -> human) == "1 second") -assert((5 second -> human) == "5 seconds") -assert((1.5 second -> human) == "1.5 seconds") - -assert((60 seconds -> human) == "1 minute") -assert((73 seconds -> human) == "1 minute + 13 seconds") -assert((120 seconds -> human) == "2 minutes") -assert((60.1 seconds -> human) == "1 minute + 0.1 seconds") -assert((1 minute -> human) == "1 minute") -assert((1.25 minute -> human) == "1 minute + 15 seconds") -assert((2.5 minute -> human) == "2 minutes + 30 seconds") - -assert((1 hour -> human) == "1 hour") -assert((1.5 hour -> human) == "1 hour + 30 minutes") -assert((2 hour -> human) == "2 hours") -assert((1 hour + 1 sec -> human) == "1 hour + 1 second") - -assert((1 day -> human) == "1 day") -assert((1.37 day -> human) == "1 day + 8 hours + 52 minutes + 48 seconds") - -assert((1 week -> human) == "7 days") -assert((1.5 weeks -> human) == "10 days + 12 hours") -assert((2 weeks -> human) == "14 days") - -assert((2 month -> human) == "60 days + 20 hours + 58 minutes + 7.509 seconds (approx. 2 months)") -assert((3 yr + 2 month -> human) == "1156 days + 14 hours + 24 minutes + 22.664 seconds (approx. 3 years + 2 months)") -assert((10 yr + 2 s -> human) == "3652 days + 10 hours + 7 minutes + 32.518 seconds (approx. 10 years + 2 seconds)") - -assert((1 sidereal_day -> human) == "23 hours + 56 minutes + 4.091 seconds") - -assert((10000 days -> human) == "10000 days (approx. 27 years + 4 months + 16 days + 17 hours + 7 minutes + 28.583 seconds)") -assert((50 million days -> human) == "50_000_000 days (approx. 136_895 years + 5 months + 18 days + 11 hours + 25 minutes + 9.591 seconds)") - -assert((1e12 days -> human) == "1_000_000_000_000 days (approx. 2_737_909_345 years + 12 days + 18 hours + 17 minutes + 24.141 seconds)") -assert((1e15 days -> human) == "1.0e+15 days (approx. 2_737_909_345_034 years + 11 months + 8 days + 22 hours + 40 minutes + 18.702 seconds)") - -assert((1 ms -> human) == "0.001 seconds") -assert((1 µs -> human) == "0.000001 seconds") -assert((1 ns -> human) == "1.0e-9 seconds") -assert((1234 ns -> human) == "0.000001234 seconds") -assert((1s + 1234 ns -> human) == "1 second") - -assert((-1 second -> human) == "1 second ago") -assert((-7.89 hour -> human) == "7 hours + 53 minutes + 24 seconds ago") diff --git a/examples/tests/human.nbt b/examples/tests/human.nbt index 4d51538d..7e02105d 100644 --- a/examples/tests/human.nbt +++ b/examples/tests/human.nbt @@ -1,12 +1,12 @@ assert_eq((0 second -> human), "0 seconds") assert_eq((1 second -> human), "1 second") assert_eq((5 second -> human), "5 seconds") -assert_eq((1.5 second -> human), "1.500 seconds") +assert_eq((1.5 second -> human), "1.5 seconds") assert_eq((60 seconds -> human), "1 minute") assert_eq((73 seconds -> human), "1 minute + 13 seconds") assert_eq((120 seconds -> human), "2 minutes") -assert_eq((60.1 seconds -> human), "1 minute + 0.100 seconds") +assert_eq((60.1 seconds -> human), "1 minute + 0.1 seconds") assert_eq((1 minute -> human), "1 minute") assert_eq((1.25 minute -> human), "1 minute + 15 seconds") assert_eq((2.5 minute -> human), "2 minutes + 30 seconds") @@ -23,16 +23,23 @@ assert_eq((1 week -> human), "7 days") assert_eq((1.5 weeks -> human), "10 days + 12 hours") assert_eq((2 weeks -> human), "14 days") -assert_eq((1 sidereal_day -> human), "23 hours + 56 minutes + 4.090500 seconds") +assert_eq((2 month -> human), "60 days + 20 hours + 58 minutes + 7.509 seconds (approx. 2 months)") +assert_eq((3 yr + 2 month -> human), "1156 days + 14 hours + 24 minutes + 22.664 seconds (approx. 3 years + 2 months)") +assert_eq((10 yr + 2 s -> human), "3652 days + 10 hours + 7 minutes + 32.518 seconds (approx. 10 years + 2 seconds)") -assert_eq((10000 days -> human), "10000 days") -assert_eq((50 million days -> human), "50_000_000 days") +assert_eq((1 sidereal_day -> human), "23 hours + 56 minutes + 4.091 seconds") -assert_eq((1e12 days -> human), "1_000_000_000_000 days") -assert_eq((1e15 days -> human), "1.0e+15 days") +assert_eq((10000 days -> human), "10000 days (approx. 27 years + 4 months + 16 days + 17 hours + 7 minutes + 28.583 seconds)") +assert_eq((50 million days -> human), "50_000_000 days (approx. 136_895 years + 5 months + 18 days + 11 hours + 25 minutes + 9.591 seconds)") + +assert_eq((1e12 days -> human), "1_000_000_000_000 days (approx. 2_737_909_345 years + 12 days + 18 hours + 17 minutes + 24.141 seconds)") +assert_eq((1e15 days -> human), "1.0e+15 days (approx. 2_737_909_345_034 years + 11 months + 8 days + 22 hours + 40 minutes + 18.702 seconds)") assert_eq((1 ms -> human), "0.001 seconds") assert_eq((1 µs -> human), "0.000001 seconds") -assert_eq((1 ns -> human), "0.000000001 seconds") +assert_eq((1 ns -> human), "1.0e-9 seconds") assert_eq((1234 ns -> human), "0.000001234 seconds") -assert_eq((1s + 1234 ns -> human), "1.000001234 seconds") +assert_eq((1s + 1234 ns -> human), "1 second") + +assert_eq((-1 second -> human), "1 second ago") +assert_eq((-7.89 hour -> human), "7 hours + 53 minutes + 24 seconds ago") From b09e7bac39f2f76f029fdf793bcd43471f49a86c Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Sun, 29 Sep 2024 16:58:20 +0200 Subject: [PATCH 6/9] Reduce clutter in human time time < 2 months show all 10 days + 2 hours + ... 2 months < time < 1 year only months (1 decimal) 65.12 days (approx. 2.2 months) 1 year < time < 100 years only years and months 710 days (approx. 1 year + 11 months) 100 year < time only years 71000 days (approx. 194 years) --- examples/tests/human.nbt | 15 +++++------ numbat/modules/datetime/human.nbt | 41 ++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/examples/tests/human.nbt b/examples/tests/human.nbt index 7e02105d..a553c7eb 100644 --- a/examples/tests/human.nbt +++ b/examples/tests/human.nbt @@ -23,17 +23,18 @@ assert_eq((1 week -> human), "7 days") assert_eq((1.5 weeks -> human), "10 days + 12 hours") assert_eq((2 weeks -> human), "14 days") -assert_eq((2 month -> human), "60 days + 20 hours + 58 minutes + 7.509 seconds (approx. 2 months)") -assert_eq((3 yr + 2 month -> human), "1156 days + 14 hours + 24 minutes + 22.664 seconds (approx. 3 years + 2 months)") -assert_eq((10 yr + 2 s -> human), "3652 days + 10 hours + 7 minutes + 32.518 seconds (approx. 10 years + 2 seconds)") +assert_eq((2 month -> human), "60.8737 days (approx. 2 months)") +assert_eq((2 month + 12 day -> human), "72.8737 days (approx. 2.4 months)") +assert_eq((3 yr + 2 month -> human), "1156.6 days (approx. 3 years + 2 months)") +assert_eq((10 yr + 2 s -> human), "3652.42 days (approx. 10 years)") assert_eq((1 sidereal_day -> human), "23 hours + 56 minutes + 4.091 seconds") -assert_eq((10000 days -> human), "10000 days (approx. 27 years + 4 months + 16 days + 17 hours + 7 minutes + 28.583 seconds)") -assert_eq((50 million days -> human), "50_000_000 days (approx. 136_895 years + 5 months + 18 days + 11 hours + 25 minutes + 9.591 seconds)") +assert_eq((10000 days -> human), "10000 days (approx. 27 years + 4 months)") +assert_eq((50 million days -> human), "50_000_000 days (approx. 136_895 years)") -assert_eq((1e12 days -> human), "1_000_000_000_000 days (approx. 2_737_909_345 years + 12 days + 18 hours + 17 minutes + 24.141 seconds)") -assert_eq((1e15 days -> human), "1.0e+15 days (approx. 2_737_909_345_034 years + 11 months + 8 days + 22 hours + 40 minutes + 18.702 seconds)") +assert_eq((1e12 days -> human), "1_000_000_000_000 days (approx. 2_737_909_345 years)") +assert_eq((1e15 days -> human), "1.0e+15 days (approx. 2_737_909_345_034 years)") assert_eq((1 ms -> human), "0.001 seconds") assert_eq((1 µs -> human), "0.000001 seconds") diff --git a/numbat/modules/datetime/human.nbt b/numbat/modules/datetime/human.nbt index d206da00..d509b097 100644 --- a/numbat/modules/datetime/human.nbt +++ b/numbat/modules/datetime/human.nbt @@ -12,39 +12,50 @@ fn _prettier(str: String) -> String = else if str_slice(str, 0, 2) == "1 " then str_slice(str, 0, str_length(str) - 1) else str +fn _remove_trailing_zero(str: String) -> String = + str_replace(str, ".0 ", " ") + fn _human_years(time: Time) -> String = "{(time -> years) / year // floor} years" -> _prettier fn _human_months(time: Time) -> String = "{(time -> months) / month // floor} months" -> _prettier fn _human_days(time: Time) -> String = "{(time -> days) / day // floor} days" -> _prettier fn _human_hours(time: Time) -> String = "{(time -> hours) / hour // floor} hours" -> _prettier fn _human_minutes(time: Time) -> String = "{(time -> minutes) / minute // floor} minutes" -> _prettier -fn _human_seconds(time: Time) -> String = str_replace("{(time -> seconds) / second} seconds", ".0 ", " ") -> _prettier + +fn _precise_human_months(time: Time) -> String = "{(time -> months) / month } months" -> _remove_trailing_zero -> _prettier +fn _precise_human_days(time: Time) -> String = "{(time -> days) / day } days" -> _remove_trailing_zero -> _prettier +fn _precise_human_seconds(time: Time) -> String = "{(time -> seconds) / second} seconds" -> _remove_trailing_zero -> _prettier fn _human_recurse(t: Time, result: String, time_unit: String) -> String = - if time_unit == "year" - then _human_recurse(t - (t -> year // floor) -> ms // round, _human_join(result, t -> _human_years), "month") - else if time_unit == "month" - then _human_recurse(t - (t -> month // floor) -> ms // round, _human_join(result, t -> _human_months), "day") - else if time_unit == "day" + if time_unit == "day" then _human_recurse(t - (t -> day // floor) -> ms // round, _human_join(result, t -> _human_days), "hour") else if time_unit == "hour" then _human_recurse(t - (t -> hour // floor) -> ms // round, _human_join(result, t -> _human_hours), "minute") else if time_unit == "minute" then _human_recurse(t - (t -> min // floor) -> ms // round, _human_join(result, t -> _human_minutes), "second") - else _human_join(result, (t -> ms // round) -> _human_seconds) + else _human_join(result, (t -> ms // round) -> _precise_human_seconds) -fn _human_manage_past(str: String, time: Time) = str_append(str, if time < 0 s then " ago" else "") +fn _human_approx_recurse(t: Time, result: String, time_unit: String) -> String = + if time_unit == "year" + then _human_approx_recurse(t - (t -> year // floor) -> ms // round, _human_join(result, t -> _human_years), "month") + else _human_join(result, t -> _human_months) -fn _human_with_biggest_unit(time: Time, biggest_unit: String) -> String = - _human_manage_past(_human_recurse(abs(time), "", biggest_unit), time) +fn _human_manage_past(str: String, time: Time) = str_append(str, if time < 0 s then " ago" else "") fn _human_for_long_duration(human_days: String, human_years: String) -> String = "{human_days} (approx. {human_years})" +fn _abs_human(time: Time) -> String = + if time == 0 s then "0 seconds" + else if time < 60 seconds then time -> _precise_human_seconds + else if time < 2 months then _human_recurse(time, "", "day") + else if time < 1 year + then _human_for_long_duration(time -> _precise_human_days, ((time -> months) * 10 // round) / 10 -> _precise_human_months) + else if time < 100 years + then _human_for_long_duration(time -> _precise_human_days, _human_approx_recurse(time, "", "year")) + else + _human_for_long_duration(time -> _precise_human_days, time -> _human_years) + @name("Human-readable time duration") @url("https://numbat.dev/doc/date-and-time.html") @description("Converts a time duration to a human-readable string in days, hours, minutes and seconds.") -fn human(time: Time) -> String = - if time == 0 s then "0 seconds" - else if abs(time) < 60 seconds then _human_manage_past(abs(time) -> _human_seconds, time) - else if abs(time) < 2 months then _human_with_biggest_unit(time, "day") - else _human_for_long_duration(_human_with_biggest_unit(time, "day"), _human_with_biggest_unit(time, "year")) +fn human(time: Time) -> String = _human_manage_past(abs(time) -> _abs_human, time) From bbe7d1972b8aa67d123548190b1703b9b24c8108 Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Sun, 29 Sep 2024 18:01:24 +0200 Subject: [PATCH 7/9] Use variable in funtion to simplify --- numbat/modules/datetime/human.nbt | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/numbat/modules/datetime/human.nbt b/numbat/modules/datetime/human.nbt index 6e393b6c..9bd63fec 100644 --- a/numbat/modules/datetime/human.nbt +++ b/numbat/modules/datetime/human.nbt @@ -8,27 +8,20 @@ fn _human_join(a: String, b: String) -> String = if a == "" then b else if b == "" then a else "{a} + {b}" fn _prettier(str: String) -> String = - if str_slice(str, 0, 2) == "0 " then "" - else if str_slice(str, 0, 2) == "1 " then str_slice(str, 0, str_length(str) - 1) - else str - -fn _remove_trailing_zero(str: String) -> String = - str_replace(str, ".0 ", " ") + if str_slice(clean_str, 0, 2) == "0 " then "" + else if str_slice(clean_str, 0, 2) == "1 " then str_slice(clean_str, 0, str_length(clean_str) - 1) + else clean_str + where clean_str = str_replace(str, ".0 ", " ") fn _human_years(time: Time) -> String = "{(time -> years) / year |> floor} years" -> _prettier fn _human_months(time: Time) -> String = "{(time -> months) / month |> floor} months" -> _prettier fn _human_days(time: Time) -> String = "{(time -> days) / day |> floor} days" -> _prettier fn _human_hours(time: Time) -> String = "{(time -> hours) / hour |> floor} hours" -> _prettier fn _human_minutes(time: Time) -> String = "{(time -> minutes) / minute |> floor} minutes" -> _prettier -# fn _human_years(time: Time) -> String = "{time |> floor_in(year)} years" -> _prettier -# fn _human_months(time: Time) -> String = "{time |> floor_in(month)} months" -> _prettier -# fn _human_days(time: Time) -> String = "{time |> floor_in(day)} days" -> _prettier -# fn _human_hours(time: Time) -> String = "{time |> floor_in(hour)} hours" -> _prettier -# fn _human_minutes(time: Time) -> String = "{time |> floor_in(minute)} minutes" -> _prettier -fn _precise_human_months(time: Time) -> String = "{(time -> months) / month } months" -> _remove_trailing_zero -> _prettier -fn _precise_human_days(time: Time) -> String = "{(time -> days) / day } days" -> _remove_trailing_zero -> _prettier -fn _precise_human_seconds(time: Time) -> String = "{(time -> seconds) / second} seconds" -> _remove_trailing_zero -> _prettier +fn _precise_human_months(time: Time) -> String = "{(time -> months) / month } months" -> _prettier +fn _precise_human_days(time: Time) -> String = "{(time -> days) / day } days" -> _prettier +fn _precise_human_seconds(time: Time) -> String = "{(time -> seconds) / second} seconds" -> _prettier fn _human_recurse(t: Time, result: String, time_unit: String) -> String = if time_unit == "day" From a6c2f0f2ecc53ace931255762112a704b60550ca Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Sun, 29 Sep 2024 18:08:17 +0200 Subject: [PATCH 8/9] Use variable in funtion to simplify --- examples/tests/human.nbt | 2 +- numbat/modules/datetime/human.nbt | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/tests/human.nbt b/examples/tests/human.nbt index 3095b243..9e234b68 100644 --- a/examples/tests/human.nbt +++ b/examples/tests/human.nbt @@ -30,7 +30,7 @@ assert_eq((10 yr + 2 s -> human), "3652.42 days (approx. 10 years)") assert_eq((1 sidereal_day -> human), "23 hours + 56 minutes + 4.091 seconds") -assert_eq((10000 days -> human), "10000 days (approx. 27 years + 4 months)") +assert_eq((10000 days -> human), "10000 days (approx. 27 years + 5 months)") assert_eq((50 million days -> human), "50_000_000 days (approx. 136_895 years)") assert_eq((1e12 days -> human), "1_000_000_000_000 days (approx. 2_737_909_345 years)") diff --git a/numbat/modules/datetime/human.nbt b/numbat/modules/datetime/human.nbt index 9bd63fec..fcc5fa02 100644 --- a/numbat/modules/datetime/human.nbt +++ b/numbat/modules/datetime/human.nbt @@ -14,7 +14,8 @@ fn _prettier(str: String) -> String = where clean_str = str_replace(str, ".0 ", " ") fn _human_years(time: Time) -> String = "{(time -> years) / year |> floor} years" -> _prettier -fn _human_months(time: Time) -> String = "{(time -> months) / month |> floor} months" -> _prettier +fn _human_months(time: Time) -> String = "{(time -> months) / month |> round} months" -> _prettier + fn _human_days(time: Time) -> String = "{(time -> days) / day |> floor} days" -> _prettier fn _human_hours(time: Time) -> String = "{(time -> hours) / hour |> floor} hours" -> _prettier fn _human_minutes(time: Time) -> String = "{(time -> minutes) / minute |> floor} minutes" -> _prettier @@ -32,10 +33,8 @@ fn _human_recurse(t: Time, result: String, time_unit: String) -> String = then _human_recurse(t - (t |> floor_in(min)), _human_join(result, t -> _human_minutes), "second") else _human_join(result, (t |> round_in(ms)) -> _precise_human_seconds) -fn _human_approx_recurse(t: Time, result: String, time_unit: String) -> String = - if time_unit == "year" - then _human_approx_recurse(t - (t |> floor_in(year)) |> round_in(ms), _human_join(result, t -> _human_years), "month") - else _human_join(result, t -> _human_months) +fn _year_month_approx(t: Time) -> String = _human_join(the_years -> _human_years, t - the_years -> _human_months) + where the_years = t |> floor_in(year) fn _human_manage_past(str: String, time: Time) = str_append(str, if time < 0 s then " ago" else "") @@ -49,7 +48,7 @@ fn _abs_human(time: Time) -> String = else if time < 1 year then _human_for_long_duration(time -> _precise_human_days, (time |> round_in(month/10)) -> _precise_human_months) else if time < 100 years - then _human_for_long_duration(time -> _precise_human_days, _human_approx_recurse(time, "", "year")) + then _human_for_long_duration(time -> _precise_human_days, _year_month_approx(time)) else _human_for_long_duration(time -> _precise_human_days, time -> _human_years) From 89a141b6400afe7a4a10a1d525c7a7a8feac6be2 Mon Sep 17 00:00:00 2001 From: Valentin Le Tallec Date: Sun, 29 Sep 2024 20:31:20 +0200 Subject: [PATCH 9/9] fix floating point precision problem --- examples/tests/human.nbt | 1 + numbat/modules/datetime/human.nbt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/tests/human.nbt b/examples/tests/human.nbt index 9e234b68..e3e91536 100644 --- a/examples/tests/human.nbt +++ b/examples/tests/human.nbt @@ -20,6 +20,7 @@ assert_eq((1 day -> human), "1 day") assert_eq((1.37 day -> human), "1 day + 8 hours + 52 minutes + 48 seconds") assert_eq((1 week -> human), "7 days") +assert_eq((1.5 weeks -> human), "10 days + 12 hours") assert_eq((2 weeks -> human), "14 days") assert_eq((2.5 weeks -> human), "17 days + 12 hours") diff --git a/numbat/modules/datetime/human.nbt b/numbat/modules/datetime/human.nbt index fcc5fa02..23d94396 100644 --- a/numbat/modules/datetime/human.nbt +++ b/numbat/modules/datetime/human.nbt @@ -26,11 +26,11 @@ fn _precise_human_seconds(time: Time) -> String = "{(time -> seconds) / second} fn _human_recurse(t: Time, result: String, time_unit: String) -> String = if time_unit == "day" - then _human_recurse(t - (t |> floor_in(day)), _human_join(result, t -> _human_days), "hour") + then _human_recurse((t -> day) - (t |> floor_in(day)), _human_join(result, t -> _human_days), "hour") else if time_unit == "hour" - then _human_recurse(t - (t |> floor_in(hour)), _human_join(result, t -> _human_hours), "minute") + then _human_recurse((t -> hour) - (t |> floor_in(hour)), _human_join(result, t -> _human_hours), "minute") else if time_unit == "minute" - then _human_recurse(t - (t |> floor_in(min)), _human_join(result, t -> _human_minutes), "second") + then _human_recurse((t -> min) - (t |> floor_in(min)), _human_join(result, t -> _human_minutes), "second") else _human_join(result, (t |> round_in(ms)) -> _precise_human_seconds) fn _year_month_approx(t: Time) -> String = _human_join(the_years -> _human_years, t - the_years -> _human_months)