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}