nx_ts.nx source
↩ module page · 210 lines · 8047 B
1// nx_ts.nx -- Unix epoch <-> calendar / ISO-8601 conversion.
2//
3// nx_log writes timestamps as raw seconds-since-epoch. That's
4// machine-readable but not human-readable: you can't tell at a
5// glance whether a log line was 2 minutes ago or 2 weeks ago.
6// This module provides:
7//
8// - epoch -> (year, month, day, hour, min, sec) calendar tuple
9// using the Howard Hinnant civil-from-days algorithm (no
10// lookup tables, no floating point, exact).
11// - calendar -> ISO-8601 string formatter ("2026-04-25T03:14:07Z")
12//
13// Reference: Hinnant, "chrono-Compatible Low-Level Date Algorithms",
14// http://howardhinnant.github.io/date_algorithms.html
15//
16// Pairs with nx_log + nx_strfmt. No timezone handling -- we always
17// emit UTC ('Z' suffix). TZ awareness can come later if anyone
18// actually needs it.
19
20// nx_safety_envelope:
21// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
22// sil_target: SIL1
23// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
24// verdict: NOT_YET_EVALUATED
25
26import "syscalls.nx"
27const NX_MAGIC_719468: i64 = 719468
28const NX_MAGIC_146097: i64 = 146097
29const NX_MAGIC_146096: i64 = 146096
30const NX_MAGIC_1460: i64 = 1460
31const NX_MAGIC_36524: i64 = 36524
32const NX_MAGIC_3600: i64 = 3600
33const NX_MAGIC_9999: i64 = 9999
34const NX_MAGIC_1970: i64 = 1970
35const NX_MAGIC_1234567890: i64 = 1234567890
36const NX_MAGIC_2009: i64 = 2009
37const NX_MAGIC_951782400: i64 = 951782400
38const NX_MAGIC_2000: i64 = 2000
39
40struct NxTsCal {
41 year: i64,
42 month: i64, // 1..12
43 day: i64, // 1..31
44 hour: i64, // 0..23
45 min: i64, // 0..59
46 sec: i64, // 0..59 (we don't model leap seconds)
47}
48
49const NX_TS_CAL_BYTES: i64 = 48
50
51// Days per non-leap year.
52const NX_TS_DAYS_PER_YEAR: i64 = 365
53// Seconds per day.
54const NX_TS_SECS_PER_DAY: i64 = 86400
55
56// Hinnant's civil_from_days: given a day count z relative to
57// 1970-01-01, return (year, month, day).
58// Internally shifts to era 0000-03-01 to make the math cycle on
59// Mar 1. Re-shifts at the end.
60func nx_ts_civil_from_days(z: i64, out: *NxTsCal) -> i64 {
61 let z2: i64 = z + NX_MAGIC_719468
62 var era: i64 = 0
63 if z2 >= 0 { era = z2 / NX_MAGIC_146097 } else { era = (z2 - NX_MAGIC_146096) / NX_MAGIC_146097 }
64 let doe: i64 = z2 - era * NX_MAGIC_146097 // day of era [0, NX_MAGIC_146096]
65 let yoe: i64 = (doe - doe / NX_MAGIC_1460 + doe / NX_MAGIC_36524 - doe / NX_MAGIC_146096) / 365 // year of era
66 let y: i64 = yoe + era * 400
67 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100)
68 let mp: i64 = (5 * doy + 2) / 153
69 let d: i64 = doy - (153 * mp + 2) / 5 + 1
70 var m: i64 = 0
71 if mp < 10 { m = mp + 3 } else { m = mp - 9 }
72 var year: i64 = y
73 if m <= 2 { year = y + 1 }
74 out.year = year
75 out.month = m
76 out.day = d
77 return 0
78}
79
80// Decompose `epoch_secs` (Unix seconds since 1970-01-01 UTC) into
81// a calendar tuple. Negative epoch (pre-1970) is well-defined.
82func nx_ts_from_epoch(epoch_secs: i64, out: *NxTsCal) -> i64 {
83 var d: i64 = 0
84 var s: i64 = 0
85 if epoch_secs >= 0 {
86 d = epoch_secs / NX_TS_SECS_PER_DAY
87 s = epoch_secs - d * NX_TS_SECS_PER_DAY
88 } else {
89 // Floor division for negative values.
90 let abs_e: i64 = 0 - epoch_secs
91 let abs_d: i64 = abs_e / NX_TS_SECS_PER_DAY
92 let abs_s: i64 = abs_e - abs_d * NX_TS_SECS_PER_DAY
93 if abs_s == 0 {
94 d = 0 - abs_d
95 s = 0
96 } else {
97 d = (0 - abs_d) - 1
98 s = NX_TS_SECS_PER_DAY - abs_s
99 }
100 }
101 nx_ts_civil_from_days(d, out)
102 out.hour = s / NX_MAGIC_3600
103 out.min = (s - out.hour * NX_MAGIC_3600) / 60
104 out.sec = s - out.hour * NX_MAGIC_3600 - out.min * 60
105 return 0
106}
107
108// Helper: write a 2-digit zero-padded value into buf[off..off+2].
109func nx_ts_write2(buf: *u8, off: i64, v: i64) -> i64 {
110 let tens: i64 = v / 10
111 let ones: i64 = v - tens * 10
112 buf[off] = 0x30 + tens
113 buf[off + 1] = 0x30 + ones
114 return off + 2
115}
116
117// Helper: write a 4-digit zero-padded year into buf[off..off+4].
118// Years before 0 or after 9999 fall back to "????" (we don't have
119// the century-extending logic and the use case is logs, not
120// archeology).
121func nx_ts_write4(buf: *u8, off: i64, v: i64) -> i64 {
122 if v < 0 {
123 buf[off] = 0x3F; buf[off + 1] = 0x3F; buf[off + 2] = 0x3F; buf[off + 3] = 0x3F
124 return off + 4
125 }
126 if v > NX_MAGIC_9999 {
127 buf[off] = 0x3F; buf[off + 1] = 0x3F; buf[off + 2] = 0x3F; buf[off + 3] = 0x3F
128 return off + 4
129 }
130 buf[off] = 0x30 + v / 1000
131 buf[off + 1] = 0x30 + (v / 100) - (v / 1000) * 10
132 buf[off + 2] = 0x30 + (v / 10) - (v / 100) * 10
133 buf[off + 3] = 0x30 + v - (v / 10) * 10
134 return off + 4
135}
136
137// Format `epoch_secs` as ISO-8601 UTC into `buf` (must be >= 21
138// bytes). Writes 20 chars + trailing NUL.
139//
140// Output: YYYY-MM-DDTHH:MM:SSZ\0
141func nx_ts_format_iso(epoch_secs: i64, buf: *u8) -> i64 {
142 let cal_raw: *u8 = sys_mmap(NX_TS_CAL_BYTES)
143 let cal: *NxTsCal = cal_raw as *NxTsCal
144 nx_ts_from_epoch(epoch_secs, cal)
145
146 var off: i64 = 0
147 off = nx_ts_write4(buf, off, cal.year)
148 buf[off] = 0x2D // '-'
149 off = off + 1
150 off = nx_ts_write2(buf, off, cal.month)
151 buf[off] = 0x2D // '-'
152 off = off + 1
153 off = nx_ts_write2(buf, off, cal.day)
154 buf[off] = 0x54 // 'T'
155 off = off + 1
156 off = nx_ts_write2(buf, off, cal.hour)
157 buf[off] = 0x3A // ':'
158 off = off + 1
159 off = nx_ts_write2(buf, off, cal.min)
160 buf[off] = 0x3A // ':'
161 off = off + 1
162 off = nx_ts_write2(buf, off, cal.sec)
163 buf[off] = 0x5A // 'Z'
164 off = off + 1
165 buf[off] = 0
166 return off
167}
168
169// ---- self-test ---------------------------------------------------
170
171func main() -> i64 {
172 let cal_raw: *u8 = sys_mmap(NX_TS_CAL_BYTES)
173 let cal: *NxTsCal = cal_raw as *NxTsCal
174
175 // Epoch = 0 -> 1970-01-01T00:00:00Z
176 nx_ts_from_epoch(0, cal)
177 if cal.year != NX_MAGIC_1970 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
178 if cal.month != 1 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
179 if cal.day != 1 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
180 if cal.hour != 0 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
181
182 // Epoch = 1234567890 -> 2009-02-13T23:31:30Z (canonical "DEADBEEF" timestamp)
183 nx_ts_from_epoch(NX_MAGIC_1234567890, cal)
184 if cal.year != NX_MAGIC_2009 { return __syscall(93, 5, 0, 0, 0, 0, 0) }
185 if cal.month != 2 { return __syscall(93, 6, 0, 0, 0, 0, 0) }
186 if cal.day != 13 { return __syscall(93, 7, 0, 0, 0, 0, 0) }
187 if cal.hour != 23 { return __syscall(93, 8, 0, 0, 0, 0, 0) }
188 if cal.min != 31 { return __syscall(93, 9, 0, 0, 0, 0, 0) }
189 if cal.sec != 30 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
190
191 // Year 2000 leap day: epoch = 951782400 -> 2000-02-29T00:00:00Z
192 nx_ts_from_epoch(NX_MAGIC_951782400, cal)
193 if cal.year != NX_MAGIC_2000 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
194 if cal.month != 2 { return __syscall(93, 12, 0, 0, 0, 0, 0) }
195 if cal.day != 29 { return __syscall(93, 13, 0, 0, 0, 0, 0) }
196
197 // ISO format epoch=0
198 let buf: *u8 = sys_mmap(32)
199 let n: i64 = nx_ts_format_iso(0, buf)
200 if n != 20 { return __syscall(93, 14, 0, 0, 0, 0, 0) }
201 if buf[0] != 0x31 { return __syscall(93, 15, 0, 0, 0, 0, 0) } // '1'
202 if buf[1] != 0x39 { return __syscall(93, 16, 0, 0, 0, 0, 0) } // '9'
203 if buf[2] != 0x37 { return __syscall(93, 17, 0, 0, 0, 0, 0) } // '7'
204 if buf[3] != 0x30 { return __syscall(93, 18, 0, 0, 0, 0, 0) } // '0'
205 if buf[4] != 0x2D { return __syscall(93, 19, 0, 0, 0, 0, 0) } // '-'
206 if buf[10] != 0x54 { return __syscall(93, 20, 0, 0, 0, 0, 0) } // 'T'
207 if buf[19] != 0x5A { return __syscall(93, 21, 0, 0, 0, 0, 0) } // 'Z'
208
209 return 0
210}