nandi/frqpublic Fork 0
ce824252aa511f0e1d68bf8fbf6be9419ab4dfd1
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Build the APK, out of ClojureDart and Flutter e2b0e6b · on ce824252aa511f0e1d68bf8fbf6be9419ab4dfd1 · nandi · 7d ago
clock.cljc · 106 lines · 4.1 KBMySQL Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
(ns frq.clock
  "Wall-clock time, in the reader's own zone.

  Messages carry an IRCv3 `time` tag in UTC; a channel is read in local time.
  Everything between those two is arithmetic, which is why this namespace is
  shared: the only thing it asks the platform for is the offset at an instant
  and the current time, both through `frq.io`.

  Finding the zone used to live here, and it was four guesses deep — TZ, then
  the target of /etc/localtime, then the file itself by path, then Android's
  system property. That is a libc question with a different answer on each
  side, so it moved to the backends: `frq.io.jolt` still carries the whole
  ladder, and `frq.io.dart` answers it with one call, because Dart carries the
  zone in its runtime."
  (:require [frq.io :as io]))

(defn now-ms [] (quot (io/wall-nanos) 1000000))

(defn parse-time-tag
  "The `time=` value of an IRCv3 tag string as epoch milliseconds, or nil.

  Fixed format, always UTC: `2026-08-30T07:05:09.000Z`. Read by hand rather
  than through a parser, since this runs once per message of a hundred-message
  backlog."
  [tags]
  (when-let [[_ y mo d h mi s] (re-find #"time=(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})"
                                        (or tags ""))]
    (let [[y mo d h mi s] (map parse-long [y mo d h mi s])
          ;; days from the civil date, by Howard Hinnant's algorithm
          y (if (<= mo 2) (dec y) y)
          era (quot (if (>= y 0) y (- y 399)) 400)
          yoe (- y (* era 400))
          doy (+ (quot (+ (* 153 (+ mo (if (> mo 2) -3 9))) 2) 5) (dec d))
          doe (+ (* yoe 365) (quot yoe 4) (- (quot yoe 100)) doy)
          days (+ (* era 146097) doe -719468)]
      (* 1000 (+ (* days 86400) (* h 3600) (* mi 60) s)))))

(defn- floor-div
  "`quot` rounds toward zero and this rounds down, which for a day number
  before 1970 is a different day.

  Written out rather than taken from the host: `Math/floorDiv` is Java, and
  this namespace is compiled by ClojureDart too, where there is no Math class
  to call into. The same reason `frq.io` exists, one scale down."
  [a b]
  (let [q (quot a b)
        r (rem a b)]
    (if (or (zero? r) (= (neg? r) (neg? b))) q (dec q))))

(defn- floor-mod [a b] (- a (* b (floor-div a b))))

(defn- civil-from-days
  "`[y m d]` for a day number since the epoch — Hinnant's algorithm the other
  way round, which is the direction `parse-time-tag` does not go.

  This was `jolt.time.local/local-dt` and a `subs` off the string it printed.
  Two problems with that: it is jolt's, so the namespace could not be shared,
  and it built a formatted date only to take it apart again. The arithmetic is
  eleven lines and the same on both platforms."
  [days]
  (let [z (+ days 719468)
        era (floor-div z 146097)
        doe (- z (* era 146097))
        yoe (quot (+ (- doe (quot doe 1460)) (quot doe 36524) (- (quot doe 146096))) 365)
        y (+ yoe (* era 400))
        doy (- doe (+ (* 365 yoe) (quot yoe 4) (- (quot yoe 100))))
        mp (quot (+ (* 5 doy) 2) 153)
        d (+ (- doy (quot (+ (* 153 mp) 2) 5)) 1)
        m (+ mp (if (< mp 10) 3 -9))]
    [(if (<= m 2) (inc y) y) m d]))

(defn- pad2 [n] (if (< n 10) (str "0" n) (str n)))

(defn- local-parts
  "`[date hour minute]` in the reader's zone, the hour on a 24-clock."
  [ms]
  (let [secs (quot ms 1000)
        secs (+ secs (io/local-offset-seconds secs))
        days (floor-div secs 86400)
        sod (floor-mod secs 86400)
        [y m d] (civil-from-days days)]
    [(str y "-" (pad2 m) "-" (pad2 d))
     (quot sod 3600)
     (pad2 (quot (mod sod 3600) 60))]))

(defn clock-time
  "A twelve-hour time: `9:05 AM`, `12:30 PM`."
  [ms]
  (let [[_ hour minute] (local-parts ms)
        display (cond (zero? hour) 12
                      (> hour 12) (- hour 12)
                      :else hour)]
    (str display ":" minute " " (if (< hour 12) "AM" "PM"))))

(defn day [ms] (first (local-parts ms)))

(defn day-label
  "The heading for a day's messages: today and yesterday by name, anything
  older by date."
  [ms]
  (let [d (day ms)
        today (day (now-ms))
        yesterday (day (- (now-ms) 86400000))]
    (cond (= d today) "Today"
          (= d yesterday) "Yesterday"
          :else d)))