1pub const SECOND: f64 = 1_000.0;
4pub const MINUTE: f64 = 60.0 * SECOND;
5pub const HOUR: f64 = 60.0 * MINUTE;
6pub const DAY: f64 = 24.0 * HOUR;
7
8const MONTHS: [&str; 12] = [
9 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
10];
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Unit {
15 Second,
16 Minute,
17 Day,
18 Month,
19 Year,
20}
21
22pub fn civil(days: i64) -> (i64, u32, u32) {
24 let z = days + 719_468;
25 let era = z.div_euclid(146_097);
26 let doe = z - era * 146_097;
27 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
28 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
29 let mp = (5 * doy + 2) / 153;
30 let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
31 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
32 (yoe + era * 400 + i64::from(month <= 2), month, day)
33}
34
35pub fn days(year: i64, month: u32, day: u32) -> i64 {
37 let year = year - i64::from(month <= 2);
38 let era = year.div_euclid(400);
39 let yoe = year - era * 400;
40 let mp = (i64::from(month) + 9) % 12;
41 let doy = (153 * mp + 2) / 5 + i64::from(day) - 1;
42 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
43 era * 146_097 + doe - 719_468
44}
45
46pub fn parse(text: &str) -> Option<f64> {
50 let text = text.trim();
51 let text = text.strip_suffix('Z').unwrap_or(text);
52 let (date, clock) = match text.find(['T', ' ']) {
53 Some(at) => (&text[..at], Some(&text[at + 1..])),
54 None => (text, None),
55 };
56 let mut parts = date.split('-');
57 let year: i64 = parts.next()?.parse().ok()?;
58 let month: u32 = parts.next().map_or(Some(1), |p| p.parse().ok())?;
59 let day: u32 = parts.next().map_or(Some(1), |p| p.parse().ok())?;
60 if parts.next().is_some() || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
61 return None;
62 }
63 let mut ms = days(year, month, day) as f64 * DAY;
64 if let Some(clock) = clock {
65 let mut parts = clock.split(':');
66 let hour: f64 = parts.next()?.parse().ok()?;
67 let minute: f64 = parts.next()?.parse().ok()?;
68 let second: f64 = parts.next().map_or(Some(0.0), |p| p.parse().ok())?;
69 if parts.next().is_some() {
70 return None;
71 }
72 ms += hour * HOUR + minute * MINUTE + second * SECOND;
73 }
74 Some(ms)
75}
76
77pub fn label(ms: f64, unit: Unit) -> String {
79 let (year, month, day, hour, minute, second) = fields(ms);
80 let name = MONTHS[month as usize - 1];
81 match unit {
82 Unit::Year => format!("{year}"),
83 Unit::Month if month == 1 => format!("{year}"),
84 Unit::Month => name.to_string(),
85 Unit::Day => format!("{name} {day}"),
86 Unit::Minute => format!("{hour:02}:{minute:02}"),
87 Unit::Second => format!("{hour:02}:{minute:02}:{second:02}"),
88 }
89}
90
91pub fn describe(ms: f64) -> String {
93 let (year, month, day, hour, minute, second) = fields(ms);
94 match (hour, minute, second) {
95 (0, 0, 0) => format!("{year}-{month:02}-{day:02}"),
96 (_, _, 0) => format!("{year}-{month:02}-{day:02} {hour:02}:{minute:02}"),
97 _ => format!("{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"),
98 }
99}
100
101fn fields(ms: f64) -> (i64, u32, u32, u32, u32, u32) {
102 let day = (ms / DAY).floor();
103 let (year, month, date) = civil(day as i64);
104 let rest = ((ms - day * DAY) / SECOND).floor() as u32;
105 (year, month, date, rest / 3_600, rest / 60 % 60, rest % 60)
106}
107
108pub fn ticks(lo: f64, hi: f64, count: usize) -> (Vec<f64>, Unit) {
111 const FIXED: [(f64, Unit); 15] = [
112 (SECOND, Unit::Second),
113 (5.0 * SECOND, Unit::Second),
114 (15.0 * SECOND, Unit::Second),
115 (30.0 * SECOND, Unit::Second),
116 (MINUTE, Unit::Minute),
117 (5.0 * MINUTE, Unit::Minute),
118 (15.0 * MINUTE, Unit::Minute),
119 (30.0 * MINUTE, Unit::Minute),
120 (HOUR, Unit::Minute),
121 (3.0 * HOUR, Unit::Minute),
122 (6.0 * HOUR, Unit::Minute),
123 (12.0 * HOUR, Unit::Minute),
124 (DAY, Unit::Day),
125 (2.0 * DAY, Unit::Day),
126 (7.0 * DAY, Unit::Day),
127 ];
128 const MONTH: f64 = 30.44 * DAY;
129 let target = (hi - lo) / count.max(1) as f64;
130 let mut ticks = Vec::new();
131
132 if let Some(&(step, unit)) = FIXED.iter().find(|(step, _)| *step >= target) {
133 let offset = if step == 7.0 * DAY { 4.0 * DAY } else { 0.0 };
135 let mut at = ((lo - offset) / step).ceil() * step + offset;
136 while at <= hi {
137 ticks.push(at);
138 at += step;
139 }
140 return (ticks, unit);
141 }
142
143 let (year, month, _) = civil((lo / DAY).floor() as i64);
144 if let Some(step) = [1, 3, 6]
145 .into_iter()
146 .find(|&k: &i64| k as f64 * MONTH >= target)
147 {
148 let mut index = year * 12 + i64::from(month) - 1;
149 if month_start(index) < lo {
150 index += 1;
151 }
152 index = (index + step - 1).div_euclid(step) * step;
153 while month_start(index) <= hi {
154 ticks.push(month_start(index));
155 index += step;
156 }
157 return (ticks, Unit::Month);
158 }
159
160 let years = target / (365.25 * DAY);
161 let mut step = 10f64.powf(years.log10().floor()).max(1.0);
162 for factor in [1.0, 2.0, 5.0, 10.0] {
163 if step * factor >= years {
164 step *= factor;
165 break;
166 }
167 }
168 let step = step as i64;
169 let mut year = if (days(year, 1, 1) as f64 * DAY) < lo {
170 year + 1
171 } else {
172 year
173 };
174 year = (year + step - 1).div_euclid(step) * step;
175 while (days(year, 1, 1) as f64 * DAY) <= hi {
176 ticks.push(days(year, 1, 1) as f64 * DAY);
177 year += step;
178 }
179 (ticks, Unit::Year)
180}
181
182fn month_start(index: i64) -> f64 {
183 days(index.div_euclid(12), index.rem_euclid(12) as u32 + 1, 1) as f64 * DAY
184}