code wiki / (root) / civil_date.nx

civil_date.nx source

↩ module page · 145 lines · 4974 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 32import "syscalls.nx" 33 34struct CivilDate { 35 year: i64, 36 month: i64, // 1..12 37 day: i64, // 1..31 38 hour: i64, // 0..23 39 minute: i64, // 0..59 40 second: i64, // 0..59 (no leap-second support) 41 weekday: i64, // 0=Sun .. 6=Sat 42} 43 44const SECONDS_PER_DAY: i64 = 86400 45 46// Integer floor-division that matches the mathematical floor for 47// negative numbers. C and NishiLang truncate toward zero; we 48// need floor behaviour for correct civil-date math below. 49func cd_fdiv(a: i64, b: i64) -> i64 { 50 if a >= 0 { return a / b } 51 // a < 0; b > 0 in all our use sites. 52 let q: i64 = a / b 53 if q * b == a { return q } 54 return q - 1 55} 56 57// Floor modulo: result in [0, b). 58func cd_fmod(a: i64, b: i64) -> i64 { 59 let r: i64 = a - cd_fdiv(a, b) * b 60 return r 61} 62 63// Convert unix_seconds to civil date fields. 64func civil_from_unix(unix_sec: i64, out: *CivilDate) -> i64 { 65 let days: i64 = cd_fdiv(unix_sec, SECONDS_PER_DAY) 66 let tod: i64 = cd_fmod(unix_sec, SECONDS_PER_DAY) 67 68 out.hour = tod / 3600 69 out.minute = (tod / 60) % 60 70 out.second = tod % 60 71 72 // Hinnant civil_from_days. 73 let z: i64 = days + 719468 74 let era: i64 = cd_fdiv(z, 146097) 75 let doe: i64 = z - era * 146097 76 let yoe: i64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365 77 var y: i64 = yoe + era * 400 78 let doy: i64 = doe - (365 * yoe + yoe / 4 - yoe / 100) 79 let mp: i64 = (5 * doy + 2) / 153 80 let d: i64 = doy - (153 * mp + 2) / 5 + 1 81 var m: i64 = mp + 3 82 if mp >= 10 { m = mp - 9 } 83 if m <= 2 { y = y + 1 } 84 85 out.year = y 86 out.month = m 87 out.day = d 88 89 // Weekday. Unix epoch 1970-01-01 was a Thursday (4). 90 // weekday = (4 + days) mod 7, but days may be negative. 91 out.weekday = cd_fmod(days + 4, 7) 92 return 0 93} 94 95// Inverse: civil date to unix seconds. Uses Hinnant 96// days_from_civil. 97func unix_from_civil(y: i64, m: i64, d: i64, 98 h: i64, mi: i64, s: i64) -> i64 { 99 var yy: i64 = y 100 if m <= 2 { yy = y - 1 } 101 let era: i64 = cd_fdiv(yy, 400) 102 let yoe: i64 = yy - era * 400 103 var mp: i64 = m - 3 104 if m <= 2 { mp = m + 9 } 105 let doy: i64 = (153 * mp + 2) / 5 + d - 1 106 let doe: i64 = yoe * 365 + yoe / 4 - yoe / 100 + doy 107 let days: i64 = era * 146097 + doe - 719468 108 return days * SECONDS_PER_DAY + h * 3600 + mi * 60 + s 109} 110 111// Compile-only smoke. 112func main() -> i64 { 113 let cd_raw: *u8 = sys_mmap(128) 114 let cd: *CivilDate = cd_raw as *CivilDate 115 116 // Unix epoch = 1970-01-01 00:00:00 UTC, Thursday (4). 117 civil_from_unix(0, cd) 118 if cd.year != 1970 { return 1 } 119 if cd.month != 1 { return 2 } 120 if cd.day != 1 { return 3 } 121 if cd.hour != 0 { return 4 } 122 if cd.minute != 0 { return 5 } 123 if cd.second != 0 { return 6 } 124 if cd.weekday != 4 { return 7 } // Thursday 125 126 // 2000-01-01 00:00:00 = 946684800 seconds, Saturday (6). 127 civil_from_unix(946684800, cd) 128 if cd.year != 2000 { return 8 } 129 if cd.month != 1 { return 9 } 130 if cd.day != 1 { return 10 } 131 if cd.weekday != 6 { return 11 } 132 133 // 2026-04-22 = today. 2026-04-22 00:00:00 UTC 134 // = 1777161600 approximately. Round-trip via unix_from_civil. 135 let u: i64 = unix_from_civil(2026, 4, 22, 12, 34, 56) 136 civil_from_unix(u, cd) 137 if cd.year != 2026 { return 12 } 138 if cd.month != 4 { return 13 } 139 if cd.day != 22 { return 14 } 140 if cd.hour != 12 { return 15 } 141 if cd.minute != 34 { return 16 } 142 if cd.second != 56 { return 17 } 143 144 return 0 145}