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