1pub trait Clock: Send + Sync {
12 fn strftime(&self, format: &str) -> String;
14}
15
16#[derive(Clone, Copy, Debug)]
18pub(crate) struct Civil {
19 pub year: i64,
20 pub month: u32, pub day: u32, pub hour: u32,
23 pub min: u32,
24 pub sec: u32,
25 pub weekday: u32, pub yday: u32, }
28
29const MONTHS_FULL: [&str; 12] = [
30 "January",
31 "February",
32 "March",
33 "April",
34 "May",
35 "June",
36 "July",
37 "August",
38 "September",
39 "October",
40 "November",
41 "December",
42];
43const MONTHS_ABBR: [&str; 12] = [
44 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
45];
46const DAYS_FULL: [&str; 7] = [
47 "Sunday",
48 "Monday",
49 "Tuesday",
50 "Wednesday",
51 "Thursday",
52 "Friday",
53 "Saturday",
54];
55const DAYS_ABBR: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
56
57pub(crate) fn civil_from_days(z: i64) -> (i64, u32, u32) {
60 let z = z + 719_468;
61 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
62 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; let y = yoe + era * 400;
65 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = (doy - (153 * mp + 2) / 5 + 1) as u32; let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32; let year = if m <= 2 { y + 1 } else { y };
70 (year, m, d)
71}
72
73fn is_leap(year: i64) -> bool {
75 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
76}
77
78fn day_of_year(year: i64, month: u32, day: u32) -> u32 {
80 const CUM: [u32; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
81 let mut d = CUM[(month - 1) as usize] + day;
82 if month > 2 && is_leap(year) {
83 d += 1;
84 }
85 d
86}
87
88impl Civil {
89 pub(crate) fn from_unix_secs(secs: i64) -> Civil {
91 let days = secs.div_euclid(86_400);
92 let tod = secs.rem_euclid(86_400);
93 let (year, month, day) = civil_from_days(days);
94 let weekday = ((days.rem_euclid(7) + 4) % 7) as u32;
96 Civil {
97 year,
98 month,
99 day,
100 hour: (tod / 3600) as u32,
101 min: ((tod % 3600) / 60) as u32,
102 sec: (tod % 60) as u32,
103 weekday,
104 yday: day_of_year(year, month, day),
105 }
106 }
107
108 pub(crate) fn strftime(&self, format: &str) -> String {
110 let mut out = String::with_capacity(format.len() + 8);
111 let mut chars = format.chars().peekable();
112 while let Some(c) = chars.next() {
113 if c != '%' {
114 out.push(c);
115 continue;
116 }
117 match chars.next() {
118 Some('Y') => out.push_str(&self.year.to_string()),
119 Some('y') => out.push_str(&format!("{:02}", self.year.rem_euclid(100))),
120 Some('m') => out.push_str(&format!("{:02}", self.month)),
121 Some('d') => out.push_str(&format!("{:02}", self.day)),
122 Some('e') => out.push_str(&format!("{:2}", self.day)),
123 Some('B') => out.push_str(MONTHS_FULL[(self.month - 1) as usize]),
124 Some('b') | Some('h') => out.push_str(MONTHS_ABBR[(self.month - 1) as usize]),
125 Some('A') => out.push_str(DAYS_FULL[self.weekday as usize]),
126 Some('a') => out.push_str(DAYS_ABBR[self.weekday as usize]),
127 Some('j') => out.push_str(&format!("{:03}", self.yday)),
128 Some('H') => out.push_str(&format!("{:02}", self.hour)),
129 Some('I') => {
130 let h12 = match self.hour % 12 {
131 0 => 12,
132 h => h,
133 };
134 out.push_str(&format!("{:02}", h12));
135 }
136 Some('M') => out.push_str(&format!("{:02}", self.min)),
137 Some('S') => out.push_str(&format!("{:02}", self.sec)),
138 Some('p') => out.push_str(if self.hour < 12 { "AM" } else { "PM" }),
139 Some('%') => out.push('%'),
140 Some(other) => {
142 out.push('%');
143 out.push(other);
144 }
145 None => out.push('%'),
146 }
147 }
148 out
149 }
150}
151
152#[derive(Clone, Copy, Debug, Default)]
159pub struct SystemClock;
160
161impl Clock for SystemClock {
162 fn strftime(&self, format: &str) -> String {
163 let secs = std::time::SystemTime::now()
164 .duration_since(std::time::UNIX_EPOCH)
165 .map(|d| d.as_secs() as i64)
166 .unwrap_or(0);
167 Civil::from_unix_secs(secs).strftime(format)
168 }
169}
170
171#[derive(Clone, Copy, Debug)]
173pub struct FixedClock {
174 unix_secs: i64,
175}
176
177impl FixedClock {
178 pub fn from_unix_secs(unix_secs: i64) -> Self {
180 FixedClock { unix_secs }
181 }
182
183 pub fn from_ymd(year: i64, month: u32, day: u32) -> Option<Self> {
186 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
187 return None;
188 }
189 let y = if month <= 2 { year - 1 } else { year };
191 let era = if y >= 0 { y } else { y - 399 } / 400;
192 let yoe = y - era * 400;
193 let mp = if month > 2 { month - 3 } else { month + 9 } as i64;
194 let doy = (153 * mp + 2) / 5 + day as i64 - 1;
195 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
196 let days = era * 146_097 + doe - 719_468;
197 Some(FixedClock {
198 unix_secs: days * 86_400,
199 })
200 }
201}
202
203impl Clock for FixedClock {
204 fn strftime(&self, format: &str) -> String {
205 Civil::from_unix_secs(self.unix_secs).strftime(format)
206 }
207}
208
209#[cfg(feature = "strftime")]
228#[derive(Clone, Copy, Debug, Default)]
229pub struct LocalClock;
230
231#[cfg(feature = "strftime")]
232impl Clock for LocalClock {
233 fn strftime(&self, format: &str) -> String {
234 use chrono::{Datelike, Local, Timelike};
235 let now = Local::now();
236 let civil = Civil {
237 year: now.year() as i64,
238 month: now.month(),
239 day: now.day(),
240 hour: now.hour(),
241 min: now.minute(),
242 sec: now.second(),
243 weekday: now.weekday().num_days_from_sunday(),
245 yday: now.ordinal(), };
247 civil.strftime(format)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn fixed_clock_formats_a_known_date() {
257 let clk = FixedClock::from_ymd(2024, 7, 4).unwrap();
259 assert_eq!(clk.strftime("%B %d, %Y"), "July 04, 2024");
260 assert_eq!(clk.strftime("%Y-%m-%d"), "2024-07-04");
261 assert_eq!(clk.strftime("%A"), "Thursday");
262 }
263
264 #[cfg(feature = "strftime")]
269 #[test]
270 fn local_clock_reads_local_time() {
271 use chrono::{Datelike, Local};
272 let got = LocalClock.strftime("%Y-%m-%d");
273 let now = Local::now();
274 let same = format!("{:04}-{:02}-{:02}", now.year(), now.month(), now.day());
275 let prev = (now - chrono::Duration::days(1)).date_naive();
276 let prev = format!("{:04}-{:02}-{:02}", prev.year(), prev.month(), prev.day());
277 assert!(
278 got == same || got == prev,
279 "LocalClock date {got} matched neither {same} nor {prev}"
280 );
281 }
282}