plum

#treesitter#compiler#wasm

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