Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add years and months to the human date formatter #425

Merged
merged 12 commits into from
Oct 9, 2024
Merged
2 changes: 1 addition & 1 deletion book/src/conversion-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 an angle to degrees, minutes, seconds (48° 46′ 32″)
Expand Down
2 changes: 1 addition & 1 deletion book/src/date-and-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ now() -> unixtime
# What is the date corresponding to a given UNIX timestamp?
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
```

Expand Down
24 changes: 16 additions & 8 deletions examples/tests/human.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,27 @@ 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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this test missing now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of #589 😅 , but with your answer there, I think I could improve the code here to have it work too 🙂 .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, fixed in 89a141b

assert_eq((2 weeks -> human), "14 days")
assert_eq((2.5 weeks -> human), "17 days + 12 hours")

assert_eq((1 sidereal_day -> human), "23 hours + 56 minutes + 4.0905 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((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 + 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)")
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")
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")
77 changes: 48 additions & 29 deletions numbat/modules/datetime/human.nbt
Original file line number Diff line number Diff line change
@@ -1,39 +1,58 @@
use core::functions
use core::strings
use units::si
use units::time
use datetime::functions

fn _human_num_days(time: Time) -> Scalar = floor(time / days)

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(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 |> 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

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"
then _human_recurse(t - (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")
else if time_unit == "minute"
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 _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 "")

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 |> round_in(month/10)) -> _precise_human_months)
else if time < 100 years
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)

# 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 = _human_manage_past(abs(time) -> _abs_human, time)
Loading