plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-std/Time.plum
module std
import std/Number
import std/Str
import std/Bool
# A raw, host-provided wall-clock reading: milliseconds since the Unix epoch.
extern fun rawNowMillis() -> Int
# A single point in time, stored as milliseconds since the Unix epoch.
enum Time(ToStr) =
| Time(value: Int)
fun toMillis(self) -> Int =
return self.value
fun toSeconds(self) -> Int =
return floorDiv(self.value, 1000)
fun add(self, d: Duration) -> Time =
return Time(value: self.value + d.value)
fun sub(self, d: Duration) -> Time =
return Time(value: self.value - d.value)
# The `Duration` elapsed between `other` and `self` (`self - other`) —
# positive if `self` is later than `other`.
fun since(self, other: Time) -> Duration =
return Duration(value: self.value - other.value)
fun isBefore(self, other: Time) -> Bool =
return self.value < other.value
fun isAfter(self, other: Time) -> Bool =
return self.value > other.value
fun equals(self, other: Time) -> Bool =
return self.value == other.value
# The proleptic-Gregorian calendar date this instant falls on, in UTC.
fun date(self) -> Civil =
return civilFromDays(floorDiv(self.value, MILLIS_PER_DAY))
fun year(self) -> Int =
return self.date().year
fun month(self) -> Int =
return self.date().month
fun day(self) -> Int =
return self.date().day
fun hour(self) -> Int =
return floorMod(floorDiv(self.value, 3600000), 24)
fun minute(self) -> Int =
return floorMod(floorDiv(self.value, 60000), 60)
fun second(self) -> Int =
return floorMod(floorDiv(self.value, 1000), 60)
fun millisecond(self) -> Int =
return floorMod(self.value, 1000)
# Day of the week: 0 = Sunday, ..., 6 = Saturday (1970-01-01 was a
# Thursday, hence the `+ 4` offset from the epoch day).
fun weekday(self) -> Int =
return floorMod(floorDiv(self.value, MILLIS_PER_DAY) + 4, 7)
# ISO 8601 / RFC 3339, always in UTC (`Z`): `YYYY-MM-DDTHH:MM:SS.mmmZ`.
fun toStr(self) -> Str =
d := self.date()
return d.year.toStr().padStart("0", 4) + "-" + d.month.toStr().padStart("0", 2)
+ "-" + d.day.toStr().padStart("0", 2) + "T" + self.hour().toStr().padStart("0", 2)
+ ":" + self.minute().toStr().padStart("0", 2) + ":" + self.second().toStr().padStart("0", 2)
+ "." + self.millisecond().toStr().padStart("0", 3) + "Z"
fun now() -> Time =
return Time(value: rawNowMillis())
# A Gregorian calendar date, decomposed from a day count by `civilFromDays`.
enum Civil =
| Civil(year: Int, month: Int, day: Int)
MILLIS_PER_DAY = 86400000
# Days-since-epoch to proleptic Gregorian year/month/day, via Howard
# Hinnant's `civil_from_days` (http://howardhinnant.github.io/date_algorithms.html) —
# pure integer math, valid for the whole range representable by `Int`, and
# exact (no floating-point rounding).
fun civilFromDays(z: Int) -> Civil =
zz := z + 719468
era := zz >= 0 ? zz / 146097 : { zz - 146096 } / 146097
doe := zz - era * 146097
yoe := { doe - doe / 1460 + doe / 36524 - doe / 146096 } / 365
y := yoe + era * 400
doy := doe - { 365 * yoe + yoe / 4 - yoe / 100 }
mp := { 5 * doy + 2 } / 153
d := doy - { 153 * mp + 2 } / 5 + 1
m := mp < 10 ? mp + 3 : mp - 9
return Civil(year: m <= 2 ? y + 1 : y, month: m, day: d)
# Integer division rounding toward negative infinity, unlike `/`'s
# round-toward-zero — needed so times before the epoch (negative `value`)
# still decompose into the correct calendar day/hour/etc rather than one
# past it.
fun floorDiv(a: Int, b: Int) -> Int =
q := a / b
r := a % b
return { { r < 0 && b > 0 } || { r > 0 && b < 0 } } ? q - 1 : q
fun floorMod(a: Int, b: Int) -> Int =
r := a % b
return { { r < 0 && b > 0 } || { r > 0 && b < 0 } } ? r + b : r
# A span of time, stored as milliseconds. Negative values are meaningful
# (e.g. the result of `Time.since` when `self` is earlier than `other`).
enum Duration =
| Duration(value: Int)
fun millis(n: Int) -> Duration =
return Duration(value: n)
fun seconds(n: Int) -> Duration =
return Duration(value: n * 1000)
fun minutes(n: Int) -> Duration =
return Duration(value: n * 60000)
fun hours(n: Int) -> Duration =
return Duration(value: n * 3600000)
fun days(n: Int) -> Duration =
return Duration(value: n * MILLIS_PER_DAY)
fun toMillis(self) -> Int =
return self.value
fun toSeconds(self) -> Float =
return Float(self.value) / 1000.0f
fun add(self, other: Duration) -> Duration =
return Duration(value: self.value + other.value)
fun sub(self, other: Duration) -> Duration =
return Duration(value: self.value - other.value)
fun negate(self) -> Duration =
return Duration(value: -self.value)
fun abs(self) -> Duration =
return Duration(value: self.value < 0 ? -self.value : self.value)
fun isZero(self) -> Bool =
return self.value == 0
fun isBefore(self, other: Duration) -> Bool =
return self.value < other.value
fun isAfter(self, other: Duration) -> Bool =
return self.value > other.value
fun equals(self, other: Duration) -> Bool =
return self.value == other.value
test "Time.toStr formats the epoch as ISO 8601 in UTC"
assert Time(value: 0).toStr() == "1970-01-01T00:00:00.000Z"
test "Time calendar/clock accessors decompose a known instant correctly"
t := Time(value: 1704067199000) # 2023-12-31T23:59:59.000Z
assert t.year() == 2023
assert t.month() == 12
assert t.day() == 31
assert t.hour() == 23
assert t.minute() == 59
assert t.second() == 59
assert t.weekday() == 0 # Sunday
assert t.toStr() == "2023-12-31T23:59:59.000Z"
test "Time accessors handle instants before the epoch correctly"
t := Time(value: -1000) # one second before the epoch
assert t.toStr() == "1969-12-31T23:59:59.000Z"
test "Duration arithmetic and unit constructors compute correctly"
assert Duration.seconds(90).toMillis() == 90000
assert Duration.minutes(2).toMillis() == 120000
assert Duration.hours(1).toMillis() == 3600000
assert Duration.days(1).toMillis() == 86400000
assert Duration.seconds(90).negate().toMillis() == -90000
assert Duration.seconds(90).negate().abs().toMillis() == 90000
assert Duration(value: 0).isZero() == True
assert Duration.seconds(1).isBefore(Duration.seconds(2)) == True
assert Duration.seconds(2).isAfter(Duration.seconds(1)) == True