plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-std/Time.plum
| 141de54 | 1 | module std |
| ca5fd6f | 2 | import std/Number |
| b7071c9 | 3 | import std/Str |
| 73b5e55 | 4 | import std/Bool |
| 19b8452 | 5 | |
| a271f34 | 6 | # A raw, host-provided wall-clock reading: milliseconds since the Unix epoch. |
| a271f34 | 7 | extern fun rawNowMillis() -> Int |
| a271f34 | 8 | |
| a271f34 | 9 | # A single point in time, stored as milliseconds since the Unix epoch. |
| 5f2f962 | 10 | enum Time(ToStr) = |
| 5f2f962 | 11 | | Time(value: Int) |
| 141de54 | 12 | |
| a271f34 | 13 | fun toMillis(self) -> Int = |
| a271f34 | 14 | return self.value |
| a271f34 | 15 | |
| 2ec05c8 | 16 | fun toSeconds(self) -> Int = |
| 2ec05c8 | 17 | return floorDiv(self.value, 1000) |
| 2ec05c8 | 18 | |
| a271f34 | 19 | fun add(self, d: Duration) -> Time = |
| a271f34 | 20 | return Time(value: self.value + d.value) |
| a271f34 | 21 | |
| a271f34 | 22 | fun sub(self, d: Duration) -> Time = |
| a271f34 | 23 | return Time(value: self.value - d.value) |
| a271f34 | 24 | |
| a271f34 | 25 | # The `Duration` elapsed between `other` and `self` (`self - other`) — |
| a271f34 | 26 | # positive if `self` is later than `other`. |
| a271f34 | 27 | fun since(self, other: Time) -> Duration = |
| a271f34 | 28 | return Duration(value: self.value - other.value) |
| a271f34 | 29 | |
| a271f34 | 30 | fun isBefore(self, other: Time) -> Bool = |
| a271f34 | 31 | return self.value < other.value |
| a271f34 | 32 | |
| a271f34 | 33 | fun isAfter(self, other: Time) -> Bool = |
| a271f34 | 34 | return self.value > other.value |
| a271f34 | 35 | |
| a271f34 | 36 | fun equals(self, other: Time) -> Bool = |
| a271f34 | 37 | return self.value == other.value |
| a271f34 | 38 | |
| 2ec05c8 | 39 | # The proleptic-Gregorian calendar date this instant falls on, in UTC. |
| 2ec05c8 | 40 | fun date(self) -> Civil = |
| 2ec05c8 | 41 | return civilFromDays(floorDiv(self.value, MILLIS_PER_DAY)) |
| 2ec05c8 | 42 | |
| 2ec05c8 | 43 | fun year(self) -> Int = |
| 2ec05c8 | 44 | return self.date().year |
| 2ec05c8 | 45 | |
| 2ec05c8 | 46 | fun month(self) -> Int = |
| 2ec05c8 | 47 | return self.date().month |
| 2ec05c8 | 48 | |
| 2ec05c8 | 49 | fun day(self) -> Int = |
| 2ec05c8 | 50 | return self.date().day |
| 2ec05c8 | 51 | |
| 2ec05c8 | 52 | fun hour(self) -> Int = |
| 2ec05c8 | 53 | return floorMod(floorDiv(self.value, 3600000), 24) |
| 2ec05c8 | 54 | |
| 2ec05c8 | 55 | fun minute(self) -> Int = |
| 2ec05c8 | 56 | return floorMod(floorDiv(self.value, 60000), 60) |
| 2ec05c8 | 57 | |
| 2ec05c8 | 58 | fun second(self) -> Int = |
| 2ec05c8 | 59 | return floorMod(floorDiv(self.value, 1000), 60) |
| 2ec05c8 | 60 | |
| 2ec05c8 | 61 | fun millisecond(self) -> Int = |
| 2ec05c8 | 62 | return floorMod(self.value, 1000) |
| 2ec05c8 | 63 | |
| 2ec05c8 | 64 | # Day of the week: 0 = Sunday, ..., 6 = Saturday (1970-01-01 was a |
| 2ec05c8 | 65 | # Thursday, hence the `+ 4` offset from the epoch day). |
| 2ec05c8 | 66 | fun weekday(self) -> Int = |
| 2ec05c8 | 67 | return floorMod(floorDiv(self.value, MILLIS_PER_DAY) + 4, 7) |
| 2ec05c8 | 68 | |
| 2ec05c8 | 69 | # ISO 8601 / RFC 3339, always in UTC (`Z`): `YYYY-MM-DDTHH:MM:SS.mmmZ`. |
| 2ec05c8 | 70 | fun toStr(self) -> Str = |
| 2ec05c8 | 71 | d := self.date() |
| 2ec05c8 | 72 | return d.year.toStr().padStart("0", 4) + "-" + d.month.toStr().padStart("0", 2) |
| 2ec05c8 | 73 | + "-" + d.day.toStr().padStart("0", 2) + "T" + self.hour().toStr().padStart("0", 2) |
| 2ec05c8 | 74 | + ":" + self.minute().toStr().padStart("0", 2) + ":" + self.second().toStr().padStart("0", 2) |
| 2ec05c8 | 75 | + "." + self.millisecond().toStr().padStart("0", 3) + "Z" |
| 2ec05c8 | 76 | |
| a271f34 | 77 | fun now() -> Time = |
| a271f34 | 78 | return Time(value: rawNowMillis()) |
| a271f34 | 79 | |
| 2ec05c8 | 80 | # A Gregorian calendar date, decomposed from a day count by `civilFromDays`. |
| 5f2f962 | 81 | enum Civil = |
| 5f2f962 | 82 | | Civil(year: Int, month: Int, day: Int) |
| 2ec05c8 | 83 | |
| 2ec05c8 | 84 | MILLIS_PER_DAY = 86400000 |
| 2ec05c8 | 85 | |
| 2ec05c8 | 86 | # Days-since-epoch to proleptic Gregorian year/month/day, via Howard |
| 2ec05c8 | 87 | # Hinnant's `civil_from_days` (http://howardhinnant.github.io/date_algorithms.html) — |
| 2ec05c8 | 88 | # pure integer math, valid for the whole range representable by `Int`, and |
| 2ec05c8 | 89 | # exact (no floating-point rounding). |
| 2ec05c8 | 90 | fun civilFromDays(z: Int) -> Civil = |
| 2ec05c8 | 91 | zz := z + 719468 |
| 2ec05c8 | 92 | era := zz >= 0 ? zz / 146097 : { zz - 146096 } / 146097 |
| 2ec05c8 | 93 | doe := zz - era * 146097 |
| 2ec05c8 | 94 | yoe := { doe - doe / 1460 + doe / 36524 - doe / 146096 } / 365 |
| 2ec05c8 | 95 | y := yoe + era * 400 |
| 2ec05c8 | 96 | doy := doe - { 365 * yoe + yoe / 4 - yoe / 100 } |
| 2ec05c8 | 97 | mp := { 5 * doy + 2 } / 153 |
| 2ec05c8 | 98 | d := doy - { 153 * mp + 2 } / 5 + 1 |
| 2ec05c8 | 99 | m := mp < 10 ? mp + 3 : mp - 9 |
| 2ec05c8 | 100 | return Civil(year: m <= 2 ? y + 1 : y, month: m, day: d) |
| 2ec05c8 | 101 | |
| 2ec05c8 | 102 | # Integer division rounding toward negative infinity, unlike `/`'s |
| 2ec05c8 | 103 | # round-toward-zero — needed so times before the epoch (negative `value`) |
| 2ec05c8 | 104 | # still decompose into the correct calendar day/hour/etc rather than one |
| 2ec05c8 | 105 | # past it. |
| 2ec05c8 | 106 | fun floorDiv(a: Int, b: Int) -> Int = |
| 2ec05c8 | 107 | q := a / b |
| 2ec05c8 | 108 | r := a % b |
| 2ec05c8 | 109 | return { { r < 0 && b > 0 } || { r > 0 && b < 0 } } ? q - 1 : q |
| 2ec05c8 | 110 | |
| 2ec05c8 | 111 | fun floorMod(a: Int, b: Int) -> Int = |
| 2ec05c8 | 112 | r := a % b |
| 2ec05c8 | 113 | return { { r < 0 && b > 0 } || { r > 0 && b < 0 } } ? r + b : r |
| 2ec05c8 | 114 | |
| a271f34 | 115 | # A span of time, stored as milliseconds. Negative values are meaningful |
| a271f34 | 116 | # (e.g. the result of `Time.since` when `self` is earlier than `other`). |
| 5f2f962 | 117 | enum Duration = |
| 5f2f962 | 118 | | Duration(value: Int) |
| a271f34 | 119 | |
| 710607b | 120 | fun millis(n: Int) -> Duration = |
| 710607b | 121 | return Duration(value: n) |
| 710607b | 122 | |
| 710607b | 123 | fun seconds(n: Int) -> Duration = |
| 710607b | 124 | return Duration(value: n * 1000) |
| 710607b | 125 | |
| 710607b | 126 | fun minutes(n: Int) -> Duration = |
| 710607b | 127 | return Duration(value: n * 60000) |
| 710607b | 128 | |
| 710607b | 129 | fun hours(n: Int) -> Duration = |
| 710607b | 130 | return Duration(value: n * 3600000) |
| 710607b | 131 | |
| 710607b | 132 | fun days(n: Int) -> Duration = |
| 710607b | 133 | return Duration(value: n * MILLIS_PER_DAY) |
| 710607b | 134 | |
| a271f34 | 135 | fun toMillis(self) -> Int = |
| a271f34 | 136 | return self.value |
| a271f34 | 137 | |
| a271f34 | 138 | fun toSeconds(self) -> Float = |
| a271f34 | 139 | return Float(self.value) / 1000.0f |
| a271f34 | 140 | |
| a271f34 | 141 | fun add(self, other: Duration) -> Duration = |
| a271f34 | 142 | return Duration(value: self.value + other.value) |
| a271f34 | 143 | |
| a271f34 | 144 | fun sub(self, other: Duration) -> Duration = |
| a271f34 | 145 | return Duration(value: self.value - other.value) |
| a271f34 | 146 | |
| 2ec05c8 | 147 | fun negate(self) -> Duration = |
| 2ec05c8 | 148 | return Duration(value: -self.value) |
| 2ec05c8 | 149 | |
| 2ec05c8 | 150 | fun abs(self) -> Duration = |
| 2ec05c8 | 151 | return Duration(value: self.value < 0 ? -self.value : self.value) |
| 2ec05c8 | 152 | |
| 2ec05c8 | 153 | fun isZero(self) -> Bool = |
| 2ec05c8 | 154 | return self.value == 0 |
| 2ec05c8 | 155 | |
| 2ec05c8 | 156 | fun isBefore(self, other: Duration) -> Bool = |
| 2ec05c8 | 157 | return self.value < other.value |
| 2ec05c8 | 158 | |
| 2ec05c8 | 159 | fun isAfter(self, other: Duration) -> Bool = |
| 2ec05c8 | 160 | return self.value > other.value |
| 2ec05c8 | 161 | |
| 2ec05c8 | 162 | fun equals(self, other: Duration) -> Bool = |
| 2ec05c8 | 163 | return self.value == other.value |
| 2ec05c8 | 164 | |
| 2ec05c8 | 165 | test "Time.toStr formats the epoch as ISO 8601 in UTC" |
| a9a0147 | 166 | assert Time(value: 0).toStr() == "1970-01-01T00:00:00.000Z" |
| 2ec05c8 | 167 | |
| 2ec05c8 | 168 | test "Time calendar/clock accessors decompose a known instant correctly" |
| 2ec05c8 | 169 | t := Time(value: 1704067199000) # 2023-12-31T23:59:59.000Z |
| a9a0147 | 170 | assert t.year() == 2023 |
| a9a0147 | 171 | assert t.month() == 12 |
| a9a0147 | 172 | assert t.day() == 31 |
| a9a0147 | 173 | assert t.hour() == 23 |
| a9a0147 | 174 | assert t.minute() == 59 |
| a9a0147 | 175 | assert t.second() == 59 |
| a9a0147 | 176 | assert t.weekday() == 0 # Sunday |
| a9a0147 | 177 | assert t.toStr() == "2023-12-31T23:59:59.000Z" |
| 2ec05c8 | 178 | |
| 2ec05c8 | 179 | test "Time accessors handle instants before the epoch correctly" |
| 2ec05c8 | 180 | t := Time(value: -1000) # one second before the epoch |
| a9a0147 | 181 | assert t.toStr() == "1969-12-31T23:59:59.000Z" |
| 2ec05c8 | 182 | |
| 2ec05c8 | 183 | test "Duration arithmetic and unit constructors compute correctly" |
| a9a0147 | 184 | assert Duration.seconds(90).toMillis() == 90000 |
| a9a0147 | 185 | assert Duration.minutes(2).toMillis() == 120000 |
| a9a0147 | 186 | assert Duration.hours(1).toMillis() == 3600000 |
| a9a0147 | 187 | assert Duration.days(1).toMillis() == 86400000 |
| a9a0147 | 188 | assert Duration.seconds(90).negate().toMillis() == -90000 |
| a9a0147 | 189 | assert Duration.seconds(90).negate().abs().toMillis() == 90000 |
| a9a0147 | 190 | assert Duration(value: 0).isZero() == True |
| a9a0147 | 191 | assert Duration.seconds(1).isBefore(Duration.seconds(2)) == True |
| a9a0147 | 192 | assert Duration.seconds(2).isAfter(Duration.seconds(1)) == True |