nandi/frqpublic Fork 0
a32699ed51bfbcc605737c045b0f1b5673a05a7c
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.

Read the clock in the reader's zone on Android too 0daacbb · on a32699ed51bfbcc605737c045b0f1b5673a05a7c · nandi · 18d ago
clock.jolt · 96 lines · 4.3 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
(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.
  jolt's time library works in epoch days and nanoseconds-of-day, and the
  offset needs a zone name, which no single call hands over — so the zone is
  found once here and everything else is arithmetic."
  (:require [clojure.string :as str]
            [jolt.host :as host]
            [jolt.time.local :as local]))

(def ^:private zone
  ;; TZ if it is set, otherwise whatever /etc/localtime points at, otherwise
  ;; UTC  which is wrong by hours but never wrong by a day's worth of parsing.
  ;;
  ;; /etc/localtime is not always a symlink. In a container it is often the
  ;; bytes themselves — distrobox's Arch has it as an overlay-mounted regular
  ;; file  and then `readlink -f` answers with the path it was given and there
  ;; is no zone name to read out of it at all. That fell through to UTC, so a
  ;; channel read seven hours ahead of the clock on the same screen.
  ;;
  ;; The colon form is what libc has for exactly this: `TZ=:/etc/localtime`
  ;; names the tzfile by path rather than by zone, and tzset reads it with the
  ;; transitions intact  the offset it gives is DST-correct per instant, not a
  ;; fixed one. Nothing downstream cares that this is a path: the zone is only
  ;; ever handed back to tz-offset-seconds, never shown.
  (delay
    (or (let [tz (host/getenv "TZ")] (when (seq tz) tz))
        (try
          (second (re-find #"/zoneinfo/(.+)$"
                           (str/trim (str (host/sh-out "readlink -f /etc/localtime")))))
          (catch Exception _ nil))
        (when (host/file-exists? "/etc/localtime") ":/etc/localtime")
        ;; Android has neither: no TZ in the environment and no /etc/localtime
        ;; to point at or read, so both branches above come up empty and the
        ;; APK read every channel in UTC. The zone is a system property there,
        ;; and bionic resolves the name against its own tzdata just as glibc
        ;; resolves it against /usr/share/zoneinfo  so the name is all we need.
        (try
          (let [tz (str/trim (str (host/sh-out "getprop persist.sys.timezone")))]
            (when (seq tz) tz))
          (catch Exception _ nil))
        "UTC")))

(defn now-ms [] (quot (host/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算 — the same one
          ;; jolt's own local-date uses in reverse.
          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- local-parts
  "`[date hour minute]` in the reader's zone, the hour on a 24-clock."
  [ms]
  (let [secs (quot ms 1000)
        secs (+ secs (host/tz-offset-seconds @zone secs))
        text (str (local/local-dt (Math/floorDiv secs 86400)
                                  (* (Math/floorMod secs 86400) 1000000000)))]
    [(subs text 0 10) (parse-long (subs text 11 13)) (subs text 14 16)]))

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