Skip to content

Commit 721ed32

Browse files
committed
Human-readable time durations
This allows us to do things like >>> 1 sidereal_day // human = 23 hours + 56 minutes + 4.090500 seconds [String] Or >>> ten million seconds // human = 115 days + 17 hours + 46 minutes + 40 seconds [String]
1 parent 23d8a27 commit 721ed32

File tree

7 files changed

+96
-21
lines changed

7 files changed

+96
-21
lines changed

examples/datetime_human_tests.nbt

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
assert((0 second // human) == "0 seconds")
2+
assert((1 second // human) == "1 second")
3+
assert((5 second // human) == "5 seconds")
4+
assert((1.5 second // human) == "1.500 seconds")
5+
6+
assert((60 seconds // human) == "1 minute")
7+
assert((73 seconds // human) == "1 minute + 13 seconds")
8+
assert((120 seconds // human) == "2 minutes")
9+
assert((60.1 seconds // human) == "1 minute + 0.100 seconds")
10+
assert((1 minute // human) == "1 minute")
11+
assert((1.25 minute // human) == "1 minute + 15 seconds")
12+
assert((2.5 minute // human) == "2 minutes + 30 seconds")
13+
14+
assert((1 hour // human) == "1 hour")
15+
assert((1.5 hour // human) == "1 hour + 30 minutes")
16+
assert((2 hour // human) == "2 hours")
17+
assert((1 hour + 1 sec // human) == "1 hour + 1 second")
18+
19+
assert((1 day // human) == "1 day")
20+
assert((1.37 day // human) == "1 day + 8 hours + 52 minutes + 48 seconds")
21+
22+
assert((1 week // human) == "7 days")
23+
assert((1.5 weeks // human) == "10 days + 12 hours")
24+
assert((2 weeks // human) == "14 days")
25+
26+
assert((1 sidereal_day // human) == "23 hours + 56 minutes + 4.090500 seconds")
27+
28+
assert((10000 days // human) == "10000 days")
29+
assert((50 million days // human) == "50_000_000 days")
30+
31+
assert((1e12 days // human) == "1_000_000_000_000 days")
32+
assert((1e15 days // human) == "1.0e+15 days")
33+
34+
assert((1 ms // human) == "0.001 seconds")
35+
assert((1 µs // human) == "0.000001 seconds")
36+
assert((1 ns // human) == "0.000000001 seconds")
37+
assert((1234 ns // human) == "0.000001234 seconds")
38+
assert((1s + 1234 ns // human) == "1.000001234 seconds")

examples/format_time.nbt

-9
This file was deleted.

numbat/modules/datetime/human.nbt

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use core::functions
2+
use core::strings
3+
use units::si
4+
use datetime::functions
5+
6+
fn _human_num_days(time: Time) -> Scalar = floor(time / day)
7+
8+
fn _human_join(a: String, b: String) -> String =
9+
if str_slice(a, 0, 2) == "0 " then b else if str_slice(b, 0, 2) == "0 " then a else "{a} + {b}"
10+
11+
fn _remove_plural_suffix(str: String) -> String =
12+
if str_slice(str, 0, 2) == "1 " then str_slice(str, 0, str_length(str) - 1) else str
13+
14+
fn _human_seconds(dt: DateTime) -> String =
15+
_remove_plural_suffix(format_datetime("%-S%.f seconds", dt))
16+
17+
fn _human_minutes(dt: DateTime) -> String =
18+
_remove_plural_suffix(format_datetime("%-M minutes", dt))
19+
20+
fn _human_hours(dt: DateTime) -> String =
21+
_remove_plural_suffix(format_datetime("%-H hours", dt))
22+
23+
fn _human_days(num_days: Scalar) -> String =
24+
_remove_plural_suffix("{num_days} days")
25+
26+
fn _human_readable_duration(time: Time, dt: DateTime, num_days: Scalar) -> String =
27+
_human_join(_human_join(_human_join(_human_days(_human_num_days(time)), _human_hours(dt)), _human_minutes(dt)), _human_seconds(dt))
28+
29+
# Implementation details:
30+
# we skip hours/minutes/seconds for durations larger than 1000 days because:
31+
# (a) we run into floating point precision problems at the nanosecond level at this point
32+
# (b) for much larger numbers, we can't convert to DateTimes anymore
33+
fn human(time: Time) =
34+
if _human_num_days(time) > 1000
35+
then "{_human_num_days(time)} days"
36+
else _human_readable_duration(time, parse_datetime("0001-01-01T00:00:00Z") + time, _human_num_days(time))

numbat/modules/prelude.nbt

+1
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ use physics::constants
3030
use physics::temperature_conversion
3131

3232
use datetime::functions
33+
use datetime::human

numbat/src/bytecode_interpreter.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ impl BytecodeInterpreter {
130130
Op::DiffDateTime
131131
} else {
132132
match operator {
133-
BinaryOperator::Add => Op::AddDateTime,
134-
BinaryOperator::Sub => Op::SubDateTime,
133+
BinaryOperator::Add => Op::AddToDateTime,
134+
BinaryOperator::Sub => Op::SubFromDateTime,
135135
BinaryOperator::ConvertTo => Op::ConvertDateTime,
136136
_ => unreachable!("{operator:?} is not valid with a DateTime"), // should be unreachable, because the typechecker will error first
137137
}

numbat/src/interpreter.rs

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ pub enum RuntimeError {
4141
DateParsingError(chrono::ParseError),
4242
#[error("Unknown timezone: {0}")]
4343
UnknownTimezone(String),
44+
#[error("Exceeded maximum size for time durations")]
45+
DurationOutOfRange,
46+
#[error("DateTime out of range")]
47+
DateTimeOutOfRange,
4448
}
4549

4650
#[derive(Debug, PartialEq, Eq)]

numbat/src/vm.rs

+15-10
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,9 @@ pub enum Op {
7373
LogicalNeg,
7474

7575
/// Similar to Add, but has DateTime on the LHS and a quantity on the RHS
76-
AddDateTime,
76+
AddToDateTime,
7777
/// Similar to Sub, but has DateTime on the LHS and a quantity on the RHS
78-
SubDateTime,
78+
SubFromDateTime,
7979
/// Computes the difference between two DateTimes
8080
DiffDateTime,
8181
/// Converts a DateTime value to another timezone
@@ -122,9 +122,9 @@ impl Op {
122122
Op::Negate
123123
| Op::Factorial
124124
| Op::Add
125-
| Op::AddDateTime
125+
| Op::AddToDateTime
126126
| Op::Subtract
127-
| Op::SubDateTime
127+
| Op::SubFromDateTime
128128
| Op::DiffDateTime
129129
| Op::ConvertDateTime
130130
| Op::Multiply
@@ -157,9 +157,9 @@ impl Op {
157157
Op::Negate => "Negate",
158158
Op::Factorial => "Factorial",
159159
Op::Add => "Add",
160-
Op::AddDateTime => "AddDateTime",
160+
Op::AddToDateTime => "AddDateTime",
161161
Op::Subtract => "Subtract",
162-
Op::SubDateTime => "SubDateTime",
162+
Op::SubFromDateTime => "SubDateTime",
163163
Op::DiffDateTime => "DiffDateTime",
164164
Op::ConvertDateTime => "ConvertDateTime",
165165
Op::Multiply => "Multiply",
@@ -649,23 +649,28 @@ impl Vm {
649649
};
650650
self.push_quantity(result.map_err(RuntimeError::QuantityError)?);
651651
}
652-
op @ (Op::AddDateTime | Op::SubDateTime) => {
652+
op @ (Op::AddToDateTime | Op::SubFromDateTime) => {
653653
let rhs = self.pop_quantity();
654654
let lhs = self.pop_datetime();
655655

656656
// for time, the base unit is in seconds
657657
let base = rhs.to_base_unit_representation();
658658
let seconds_f = base.unsafe_value().to_f64();
659659

660-
let duration = chrono::Duration::seconds(seconds_f.trunc() as i64)
660+
let duration = chrono::Duration::try_seconds(seconds_f.trunc() as i64)
661+
.ok_or(RuntimeError::DurationOutOfRange)?
661662
+ chrono::Duration::nanoseconds(
662663
(seconds_f.fract() * 1_000_000_000f64).round() as i64,
663664
);
664665

665666
self.push(Value::DateTime(
666667
match op {
667-
Op::AddDateTime => lhs + duration,
668-
Op::SubDateTime => lhs - duration,
668+
Op::AddToDateTime => lhs
669+
.checked_add_signed(duration)
670+
.ok_or(RuntimeError::DateTimeOutOfRange)?,
671+
Op::SubFromDateTime => lhs
672+
.checked_sub_signed(duration)
673+
.ok_or(RuntimeError::DateTimeOutOfRange)?,
669674
_ => unreachable!(),
670675
},
671676
chrono::Local::now().offset().fix(),

0 commit comments

Comments
 (0)