numbat 1.23.0

A statically typed programming language for scientific computations with first class support for physical dimensions and units.
Documentation
use core::functions
use core::lists
use core::strings
use core::quantities
use units::si
use units::time
use datetime::functions
use units::mixed

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(0, 2, clean_str) == "0 " then ""
    else if str_slice(0, 2, clean_str) == "1 " then str_slice( 0, str_length(clean_str) - 1, clean_str)
    else clean_str
  where clean_str = str_replace(".0 ", " ", str)

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_unit(time: Time) -> String =
  if      time_unit >= year    then _human_years(time)
  else if time_unit >= month   then _human_months(time)
  else if time_unit >= day     then _human_days(time)
  else if time_unit >= hour    then _human_hours(time)
  else if time_unit >= minute  then _human_minutes(time)
  else if time      != 0 s     then _precise_human_seconds(time |> round_in(ms))
  else                              ""
  where time_unit = if (time == 0) then 0 s else unit_of(time)

fn _round_mixed_in<D: Dim>(base: D, value: List<D>) -> List<D> =
  value |> sum |> round_in(base) |> _unit_list(units)
    where units: List<D> = value |> filter(is_nonzero) |> map(unit_of)

fn _human_time(base: Time, time_segments: List<Time>) -> String = 
  time_segments |> _round_mixed_in(base) |> map(_human_unit) |> foldl(_human_join, "")

fn _human_for_long_duration(human_days: String, human_years: String) -> String =
  "{human_days} (approx. {human_years})"

fn _abs_human(time: Time) -> String =
  if      abs_time ==  0 seconds then "0 seconds"
  else if abs_time <  60 seconds then abs_time -> _precise_human_seconds
  else if abs_time <   2 months  then ((abs_time -> seconds) |> unit_list([day, hour, minute, second]) |> _human_time(0.1 ms))
  else if abs_time <   1 years   then _human_for_long_duration(abs_time -> _precise_human_days, (abs_time |> round_in(month/10)) -> _precise_human_months)
  else if abs_time < 100 years
   then _human_for_long_duration(abs_time -> _precise_human_days, ((abs_time -> months) |> unit_list([year, month]) |> _human_time(month/10)))
  else
    _human_for_long_duration(abs_time -> _precise_human_days, abs_time -> _human_years)
  where abs_time: Time = abs(time)

@name("Human-readable time duration")
@url("https://numbat.dev/docs/basics/date-and-time/")
@description("Converts a time duration to a human-readable string in days, hours, minutes and seconds.")
@example("century/1e6 -> human", "How long is a microcentury?")
fn human(time: Time) -> String = 
  if time < 0 s 
  then str_append(_abs_human(time),  " ago") 
  else _abs_human(time)