Przeglądaj źródła

Merge pull request #425 from ValentinLeTallec/master

Add years and months to the human date formatter
David Peter 1 rok temu
rodzic
commit
a4eadb6da2

+ 1 - 1
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 an angle to degrees, minutes, seconds (48° 46′ 32″)

+ 1 - 1
book/src/date-and-time.md

@@ -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
 ```
 

+ 1 - 1
book/src/list-functions-datetime.md

@@ -242,7 +242,7 @@ fn human(time: Time) -> String
 How long is a microcentury?
 <pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=century%2F1e6%20%2D%3E%20human')""></button></div><code class="language-nbt hljs numbat">>>> century/1e6 -> human
 
-    = "52 minutes + 35.692505184 seconds"    [String]
+    = "52 minutes + 35.693 seconds"    [String]
 </code></pre>
 
 </details>

+ 16 - 7
examples/tests/human.nbt

@@ -22,17 +22,26 @@ assert_eq((1.37 day         -> human), "1 day + 8 hours + 52 minutes + 48 second
 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")
 
-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")

+ 48 - 29
numbat/modules/datetime/human.nbt

@@ -1,40 +1,59 @@
 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 -> day) - (t |> floor_in(day)), _human_join(result, t -> _human_days),    "hour")
+  else if time_unit == "hour"
+    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 -> 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)
+  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.")
 @example("century/1e6 -> human", "How long is a microcentury?")
-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)