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}