code wiki / (root) / iso8601.nx

iso8601.nx source

↩ module page · 190 lines · 6599 B

1// iso8601.nx -- format + parse ISO 8601 date-time strings. 2// 3// The canonical machine-readable timestamp format: 4// 2026-04-22T12:34:56Z 5// 2026-04-22T12:34:56.123Z (milliseconds -- optional) 6// 7// Used by: JSON APIs (the convention), HTTP Last-Modified headers 8// (RFC 7231 also accepts IMF-fixdate but ISO is simpler), 9// file export headers, nishi-pages post dates, obligation 10// deadlines, log timestamps. 11// 12// We support the "basic profile" of ISO 8601: 13// YYYY-MM-DDTHH:MM:SS[.fff]Z (UTC only, 'Z' suffix) 14// 15// Longer time-offset variants (+01:00, etc.) are NOT produced; 16// we always emit UTC. Parser accepts both 'Z' and an optional 17// '+HH:MM' / '-HH:MM' offset that we convert to seconds. 18// 19// Composes civil_date.nx. 20// 21// Invariants: 22// I1 Format output is exactly 20 bytes for seconds precision, 23// 24 bytes for millisecond precision. 24// I2 Parser rejects strings with unknown fields (e.g. week 25// numbers) with ISO_ERR_FORMAT; we stay narrowly useful. 26// I3 Round-trip exact for format(parse(s)) on strings we emit. 27 28import "syscalls.nx" 29import "civil_date.nx" 30 31const ISO_ERR_FORMAT: i64 = -1 32const ISO_ERR_SHORT: i64 = -2 33 34// Write a 2-digit zero-padded integer at out[off]. 35func iso_put_2d(out: *u8, off: i64, v: i64) -> i64 { 36 out[off] = 0x30 + ((v / 10) % 10) 37 out[off + 1] = 0x30 + (v % 10) 38 return off + 2 39} 40 41// Write a 4-digit zero-padded year at out[off]. 42func iso_put_4d(out: *u8, off: i64, v: i64) -> i64 { 43 out[off] = 0x30 + ((v / 1000) % 10) 44 out[off + 1] = 0x30 + ((v / 100) % 10) 45 out[off + 2] = 0x30 + ((v / 10) % 10) 46 out[off + 3] = 0x30 + (v % 10) 47 return off + 4 48} 49 50// Format a CivilDate (assumed UTC) as "YYYY-MM-DDTHH:MM:SSZ". 51// Returns 20 on success or ISO_ERR_SHORT. 52func iso_format(out: *u8, cap: i64, cd: *CivilDate) -> i64 { 53 if cap < 20 { return ISO_ERR_SHORT } 54 var off: i64 = 0 55 off = iso_put_4d(out, off, cd.year) 56 out[off] = 0x2D; off = off + 1 57 off = iso_put_2d(out, off, cd.month) 58 out[off] = 0x2D; off = off + 1 59 off = iso_put_2d(out, off, cd.day) 60 out[off] = 0x54; off = off + 1 // 'T' 61 off = iso_put_2d(out, off, cd.hour) 62 out[off] = 0x3A; off = off + 1 63 off = iso_put_2d(out, off, cd.minute) 64 out[off] = 0x3A; off = off + 1 65 off = iso_put_2d(out, off, cd.second) 66 out[off] = 0x5A; off = off + 1 // 'Z' 67 return off 68} 69 70// Convenience: format unix seconds directly. 71func iso_format_unix(out: *u8, cap: i64, unix_sec: i64) -> i64 { 72 let cd_raw: *u8 = sys_mmap(128) 73 let cd: *CivilDate = cd_raw as *CivilDate 74 civil_from_unix(unix_sec, cd) 75 return iso_format(out, cap, cd) 76} 77 78// Parse 2 decimal digits at buf[off]. Returns value or -1. 79func iso_parse_2d(buf: *u8, off: i64) -> i64 { 80 let a: i64 = buf[off] 81 let b: i64 = buf[off + 1] 82 if a < 0x30 { return -1 } 83 if a > 0x39 { return -1 } 84 if b < 0x30 { return -1 } 85 if b > 0x39 { return -1 } 86 return (a - 0x30) * 10 + (b - 0x30) 87} 88 89// Parse 4 decimal digits. 90func iso_parse_4d(buf: *u8, off: i64) -> i64 { 91 let hi: i64 = iso_parse_2d(buf, off) 92 if hi < 0 { return -1 } 93 let lo: i64 = iso_parse_2d(buf, off + 2) 94 if lo < 0 { return -1 } 95 return hi * 100 + lo 96} 97 98// Parse an ISO 8601 string into *unix_sec_out. Returns 0 on 99// success. Accepts 20-char minimum + optional ".fff" fractional 100// seconds + optional timezone offset. 101func iso_parse(buf: *u8, n: i64, unix_sec_out: *i64) -> i64 { 102 if n < 20 { return ISO_ERR_SHORT } 103 let y: i64 = iso_parse_4d(buf, 0) 104 if y < 0 { return ISO_ERR_FORMAT } 105 if buf[4] != 0x2D { return ISO_ERR_FORMAT } 106 let mo: i64 = iso_parse_2d(buf, 5) 107 if mo < 0 { return ISO_ERR_FORMAT } 108 if buf[7] != 0x2D { return ISO_ERR_FORMAT } 109 let d: i64 = iso_parse_2d(buf, 8) 110 if d < 0 { return ISO_ERR_FORMAT } 111 // Accept 'T' (RFC 3339) or ' ' (loose). 112 if buf[10] != 0x54 { 113 if buf[10] != 0x20 { return ISO_ERR_FORMAT } 114 } 115 let h: i64 = iso_parse_2d(buf, 11) 116 if h < 0 { return ISO_ERR_FORMAT } 117 if buf[13] != 0x3A { return ISO_ERR_FORMAT } 118 let mi: i64 = iso_parse_2d(buf, 14) 119 if mi < 0 { return ISO_ERR_FORMAT } 120 if buf[16] != 0x3A { return ISO_ERR_FORMAT } 121 let s: i64 = iso_parse_2d(buf, 17) 122 if s < 0 { return ISO_ERR_FORMAT } 123 124 var tail: i64 = 19 125 // Skip optional .fff fractional. 126 if tail < n { 127 if buf[tail] == 0x2E { 128 tail = tail + 1 129 while tail < n { 130 if buf[tail] < 0x30 { break } 131 if buf[tail] > 0x39 { break } 132 tail = tail + 1 133 } 134 } 135 } 136 137 // Timezone offset. 'Z' = UTC; +HH:MM / -HH:MM accepted. 138 var tz_off: i64 = 0 139 if tail < n { 140 if buf[tail] == 0x5A { 141 tz_off = 0 142 tail = tail + 1 143 } else { 144 var sign: i64 = 0 145 if buf[tail] == 0x2B { sign = 1 } 146 if buf[tail] == 0x2D { sign = -1 } 147 if sign == 0 { return ISO_ERR_FORMAT } 148 tail = tail + 1 149 if n < tail + 5 { return ISO_ERR_FORMAT } 150 let oh: i64 = iso_parse_2d(buf, tail) 151 if oh < 0 { return ISO_ERR_FORMAT } 152 if buf[tail + 2] != 0x3A { return ISO_ERR_FORMAT } 153 let om: i64 = iso_parse_2d(buf, tail + 3) 154 if om < 0 { return ISO_ERR_FORMAT } 155 tz_off = sign * (oh * 3600 + om * 60) 156 } 157 } 158 159 let local_unix: i64 = unix_from_civil(y, mo, d, h, mi, s) 160 *unix_sec_out = local_unix - tz_off 161 return 0 162} 163 164// Compile-only smoke: round-trip. 165func main() -> i64 { 166 let out: *u8 = sys_mmap(64) 167 let n: i64 = iso_format_unix(out, 64, 0) 168 if n != 20 { return 1 } 169 // "1970-01-01T00:00:00Z" 170 if out[0] != 0x31 { return 2 } // '1' 171 if out[10] != 0x54 { return 3 } // 'T' 172 if out[19] != 0x5A { return 4 } // 'Z' 173 174 // Parse back. 175 let parsed: *i64 = (sys_mmap(16)) as *i64 176 if iso_parse(out, 20, parsed) != 0 { return 5 } 177 if *parsed != 0 { return 6 } 178 179 // 2026-04-22T12:34:56Z round-trip. 180 let u: i64 = unix_from_civil(2026, 4, 22, 12, 34, 56) 181 iso_format_unix(out, 64, u) 182 iso_parse(out, 20, parsed) 183 if *parsed != u { return 7 } 184 185 // Parse with +02:00 offset: "1970-01-01T02:00:00+02:00" = unix 0. 186 if iso_parse("1970-01-01T02:00:00+02:00", 25, parsed) != 0 { return 8 } 187 if *parsed != 0 { return 9 } 188 189 return 0 190}