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