code wiki / (root) / nx_civil_date.nx

nx_civil_date.nx source

↩ module page · 161 lines · 5687 B

1// civil_date.nx -- Gregorian civil date from unix seconds. 2// 3// Pairs with ntp.nx (returns unix seconds) to convert seconds 4// into (year, month, day, hour, minute, second, weekday) without 5// depending on glibc strftime or the OS tzdata. UTC only; 6// timezone handling is a separate concern. 7// 8// Uses Howard Hinnant's "civil_from_days" algorithm (the 9// <chrono> library implementation used by libc++). Correct for 10// all years in i64 range (billions of years past and future). 11// 12// Algorithm: 13// z = days since 1970-01-01 14// Shift epoch to 0000-03-01 so Feb is last: z += 719468 15// era = z / 146097 (146097 days = 400 years) 16// doe = z % 146097 17// yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365 18// y = yoe + era*400 19// doy = doe - (365*yoe + yoe/4 - yoe/100) 20// mp = (5*doy + 2) / 153 21// d = doy - (153*mp + 2)/5 + 1 22// m = mp < 10 ? mp + 3 : mp - 9 23// y += m <= 2 ? 1 : 0 24// 25// Invariants: 26// CD1 1970-01-01 00:00:00 UTC <-> unix_sec = 0, all fields 27// returned accordingly (year=1970, month=1, day=1, ...). 28// CD2 Negative unix_sec works (dates before 1970). 29// CD3 Weekday: 0 = Sunday, 1 = Monday, ..., 6 = Saturday 30// (Unix convention; struct tm compatible). 31 32// nx_safety_envelope: 33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 34// sil_target: SIL1 35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 36// verdict: NOT_YET_EVALUATED 37 38import "nx_syscalls.nx" 39const SECONDS_MAGIC_3600: i64 = 3600 40const SECONDS_MAGIC_719468: i64 = 719468 41const SECONDS_MAGIC_146097: i64 = 146097 42const SECONDS_MAGIC_1460: i64 = 1460 43const SECONDS_MAGIC_36524: i64 = 36524 44const SECONDS_MAGIC_146096: i64 = 146096 45const SECONDS_MAGIC_1970: i64 = 1970 46const SECONDS_MAGIC_946684800: i64 = 946684800 47const SECONDS_MAGIC_2000: i64 = 2000 48const SECONDS_MAGIC_2026: i64 = 2026 49 50struct CivilDate { 51 year: i64, 52 month: i64, // 1..12 53 day: i64, // 1..31 54 hour: i64, // 0..23 55 minute: i64, // 0..59 56 second: i64, // 0..59 (no leap-second support) 57 weekday: i64, // 0=Sun .. 6=Sat 58} 59 60const SECONDS_PER_DAY: i64 = 86400 61 62// Integer floor-division that matches the mathematical floor for 63// negative numbers. C and NishiLang truncate toward zero; we 64// need floor behaviour for correct civil-date math below. 65func cd_fdiv(a: i64, b: i64) -> i64 { 66 if a >= 0 { return a / b } 67 // a < 0; b > 0 in all our use sites. 68 let q: i64 = a / b 69 if q * b == a { return q } 70 return q - 1 71} 72 73// Floor modulo: result in [0, b). 74func cd_fmod(a: i64, b: i64) -> i64 { 75 let r: i64 = a - cd_fdiv(a, b) * b 76 return r 77} 78 79// Convert unix_seconds to civil date fields. 80func civil_from_unix(unix_sec: i64, out: *CivilDate) -> i64 { 81 let days: i64 = cd_fdiv(unix_sec, SECONDS_PER_DAY) 82 let tod: i64 = cd_fmod(unix_sec, SECONDS_PER_DAY) 83 84 out.hour = tod / SECONDS_MAGIC_3600 85 out.minute = (tod / 60) % 60 86 out.second = tod % 60 87 88 // Hinnant civil_from_days. 89 let z: i64 = days + SECONDS_MAGIC_719468 90 let era: i64 = cd_fdiv(z, SECONDS_MAGIC_146097) 91 let doe: i64 = z - era * SECONDS_MAGIC_146097 92 let yoe: i64 = (doe - doe / SECONDS_MAGIC_1460 + doe / SECONDS_MAGIC_36524 - doe / SECONDS_MAGIC_146096) / 365 93 var y: i64 = yoe + era * 400 94 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100) 95 let mp: i64 = (5 * doy + 2) / 153 96 let d: i64 = doy - (153 * mp + 2) / 5 + 1 97 var m: i64 = mp + 3 98 if mp >= 10 { m = mp - 9 } 99 if m <= 2 { y = y + 1 } 100 101 out.year = y 102 out.month = m 103 out.day = d 104 105 // Weekday. Unix epoch 1970-01-01 was a Thursday (4). 106 // weekday = (4 + days) mod 7, but days may be negative. 107 out.weekday = cd_fmod(days + 4, 7) 108 return 0 109} 110 111// Inverse: civil date to unix seconds. Uses Hinnant 112// days_from_civil. 113func unix_from_civil(y: i64, m: i64, d: i64, 114 h: i64, mi: i64, s: i64) -> i64 { 115 var yy: i64 = y 116 if m <= 2 { yy = y - 1 } 117 let era: i64 = cd_fdiv(yy, 400) 118 let yoe: i64 = yy - era * 400 119 var mp: i64 = m - 3 120 if m <= 2 { mp = m + 9 } 121 let doy: i64 = (153 * mp + 2) / 5 + d - 1 122 let doe: i64 = yoe * 365 + yoe / 4 - yoe / 100 + doy 123 let days: i64 = era * SECONDS_MAGIC_146097 + doe - SECONDS_MAGIC_719468 124 return days * SECONDS_PER_DAY + h * SECONDS_MAGIC_3600 + mi * 60 + s 125} 126 127// Compile-only smoke. 128func main() -> i64 { 129 let cd_raw: *u8 = sys_mmap(128) 130 let cd: *CivilDate = cd_raw as *CivilDate 131 132 // Unix epoch = 1970-01-01 00:00:00 UTC, Thursday (4). 133 civil_from_unix(0, cd) 134 if cd.year != SECONDS_MAGIC_1970 { return 1 } 135 if cd.month != 1 { return 2 } 136 if cd.day != 1 { return 3 } 137 if cd.hour != 0 { return 4 } 138 if cd.minute != 0 { return 5 } 139 if cd.second != 0 { return 6 } 140 if cd.weekday != 4 { return 7 } // Thursday 141 142 // 2000-01-01 00:00:00 = 946684800 seconds, Saturday (6). 143 civil_from_unix(SECONDS_MAGIC_946684800, cd) 144 if cd.year != SECONDS_MAGIC_2000 { return 8 } 145 if cd.month != 1 { return 9 } 146 if cd.day != 1 { return 10 } 147 if cd.weekday != 6 { return 11 } 148 149 // 2026-04-22 = today. 2026-04-22 00:00:00 UTC 150 // = 1777161600 approximately. Round-trip via unix_from_civil. 151 let u: i64 = unix_from_civil(SECONDS_MAGIC_2026, 4, 22, 12, 34, 56) 152 civil_from_unix(u, cd) 153 if cd.year != SECONDS_MAGIC_2026 { return 12 } 154 if cd.month != 4 { return 13 } 155 if cd.day != 22 { return 14 } 156 if cd.hour != 12 { return 15 } 157 if cd.minute != 34 { return 16 } 158 if cd.second != 56 { return 17 } 159 160 return 0 161}