code wiki / (root) / nx_ts.nx

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}