1use crate::aes::Aesthetic;
2use crate::data::Value;
3
4use super::Scale;
5
6#[derive(Clone, Copy, Debug)]
8enum DateBreak {
9 Secs(f64),
11 Months(u32),
13}
14
15#[derive(Clone, Debug)]
17pub struct ScaleDateTime {
18 aesthetic: Aesthetic,
19 name: String,
20 min: f64,
21 max: f64,
22 trained: bool,
23 expand: (f64, f64),
24 date_breaks: Option<DateBreak>,
25 date_labels: Option<String>,
26}
27
28struct DateParts {
30 year: i64,
31 month: u32,
32 day: u32,
33 hour: u32,
34 minute: u32,
35 second: u32,
36}
37
38fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
40 let y = if m <= 2 { y - 1 } else { y };
41 let era = (if y >= 0 { y } else { y - 399 }) / 400;
42 let yoe = y - era * 400;
43 let mp = if m > 2 { m - 3 } else { m + 9 } as i64;
44 let doy = (153 * mp + 2) / 5 + d as i64 - 1;
45 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
46 era * 146_097 + doe - 719_468
47}
48
49fn secs_from_civil(y: i64, m: u32, d: u32) -> i64 {
50 days_from_civil(y, m, d) * 86_400
51}
52
53fn civil_from_secs(secs: i64) -> DateParts {
55 let (mut days, rem) = if secs >= 0 {
56 (secs / 86_400, secs % 86_400)
57 } else {
58 let d = (secs - 86_400 + 1) / 86_400;
59 (d, secs - d * 86_400)
60 };
61 let hour = (rem / 3600) as u32;
62 let minute = ((rem % 3600) / 60) as u32;
63 let second = (rem % 60) as u32;
64
65 days += 719_468;
66 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
67 let doe = (days - era * 146_097) as u32;
68 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
69 let y = yoe as i64 + era * 400;
70 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
71 let mp = (5 * doy + 2) / 153;
72 let day = doy - (153 * mp + 2) / 5 + 1;
73 let month = if mp < 10 { mp + 3 } else { mp - 9 };
74 let year = if month <= 2 { y + 1 } else { y };
75 DateParts {
76 year,
77 month,
78 day,
79 hour,
80 minute,
81 second,
82 }
83}
84
85const MONTHS_SHORT: [&str; 12] = [
86 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
87];
88const MONTHS_LONG: [&str; 12] = [
89 "January",
90 "February",
91 "March",
92 "April",
93 "May",
94 "June",
95 "July",
96 "August",
97 "September",
98 "October",
99 "November",
100 "December",
101];
102
103fn strftime(secs: f64, fmt: &str) -> String {
106 let p = civil_from_secs(secs as i64);
107 let mi = (p.month.clamp(1, 12) - 1) as usize;
108 let mut out = String::new();
109 let mut chars = fmt.chars();
110 while let Some(c) = chars.next() {
111 if c != '%' {
112 out.push(c);
113 continue;
114 }
115 match chars.next() {
116 Some('Y') => out.push_str(&format!("{:04}", p.year)),
117 Some('y') => out.push_str(&format!("{:02}", p.year.rem_euclid(100))),
118 Some('m') => out.push_str(&format!("{:02}", p.month)),
119 Some('b') => out.push_str(MONTHS_SHORT[mi]),
120 Some('B') => out.push_str(MONTHS_LONG[mi]),
121 Some('d') => out.push_str(&format!("{:02}", p.day)),
122 Some('e') => out.push_str(&format!("{:2}", p.day)),
123 Some('H') => out.push_str(&format!("{:02}", p.hour)),
124 Some('M') => out.push_str(&format!("{:02}", p.minute)),
125 Some('S') => out.push_str(&format!("{:02}", p.second)),
126 Some('%') => out.push('%'),
127 Some(other) => {
128 out.push('%');
129 out.push(other);
130 }
131 None => out.push('%'),
132 }
133 }
134 out
135}
136
137fn parse_date_break(spec: &str) -> Option<DateBreak> {
139 let spec = spec.trim().to_lowercase();
140 let mut parts = spec.split_whitespace();
141 let first = parts.next()?;
142 let (n, unit) = match first.parse::<f64>() {
143 Ok(n) => (n, parts.next()?.to_string()),
144 Err(_) => (1.0, first.to_string()),
145 };
146 let unit = unit.trim_end_matches('s');
147 let secs = |s: f64| Some(DateBreak::Secs(n * s));
148 match unit {
149 "sec" | "second" => secs(1.0),
150 "min" | "minute" => secs(60.0),
151 "hour" => secs(3600.0),
152 "day" => secs(86_400.0),
153 "week" => secs(604_800.0),
154 "month" => Some(DateBreak::Months(n.max(1.0) as u32)),
155 "year" => Some(DateBreak::Months((n.max(1.0) as u32) * 12)),
156 _ => None,
157 }
158}
159
160impl ScaleDateTime {
161 pub fn new() -> Self {
162 ScaleDateTime {
163 aesthetic: Aesthetic::X,
164 name: String::new(),
165 min: f64::INFINITY,
166 max: f64::NEG_INFINITY,
167 trained: false,
168 expand: (0.05, 0.0),
169 date_breaks: None,
170 date_labels: None,
171 }
172 }
173
174 pub fn for_aesthetic(mut self, aes: Aesthetic) -> Self {
175 self.aesthetic = aes;
176 self
177 }
178
179 pub fn with_name(mut self, name: &str) -> Self {
180 self.name = name.to_string();
181 self
182 }
183
184 pub fn with_date_breaks(mut self, spec: &str) -> Self {
187 self.date_breaks = parse_date_break(spec);
188 self
189 }
190
191 pub fn with_date_labels(mut self, fmt: &str) -> Self {
194 self.date_labels = Some(fmt.to_string());
195 self
196 }
197
198 fn label(&self, secs: f64, step: f64) -> String {
199 match &self.date_labels {
200 Some(fmt) => strftime(secs, fmt),
201 None => Self::format_datetime(secs, step),
202 }
203 }
204
205 fn expanded_range(&self) -> (f64, f64) {
206 let range = self.max - self.min;
207 let mult = self.expand.0;
208 let add = self.expand.1;
209 (self.min - range * mult - add, self.max + range * mult + add)
210 }
211
212 fn nice_datetime_step(range_secs: f64) -> f64 {
214 const MINUTE: f64 = 60.0;
215 const HOUR: f64 = 3600.0;
216 const DAY: f64 = 86400.0;
217 const WEEK: f64 = 7.0 * DAY;
218 const MONTH: f64 = 30.0 * DAY;
219 const YEAR: f64 = 365.25 * DAY;
220
221 let candidates = [
222 1.0,
223 5.0,
224 10.0,
225 30.0,
226 MINUTE,
227 5.0 * MINUTE,
228 10.0 * MINUTE,
229 30.0 * MINUTE,
230 HOUR,
231 3.0 * HOUR,
232 6.0 * HOUR,
233 12.0 * HOUR,
234 DAY,
235 2.0 * DAY,
236 WEEK,
237 2.0 * WEEK,
238 MONTH,
239 3.0 * MONTH,
240 6.0 * MONTH,
241 YEAR,
242 2.0 * YEAR,
243 5.0 * YEAR,
244 10.0 * YEAR,
245 20.0 * YEAR,
246 50.0 * YEAR,
247 100.0 * YEAR,
248 ];
249
250 let target = range_secs / 5.0;
251 for &c in &candidates {
252 if c >= target {
253 return c;
254 }
255 }
256 let n = (target / (100.0 * YEAR)).ceil();
258 n * 100.0 * YEAR
259 }
260
261 fn format_datetime(secs: f64, _step: f64) -> String {
264 let epoch_secs = secs as i64;
265 crate::data::format_epoch_secs(epoch_secs)
266 }
267}
268
269impl Default for ScaleDateTime {
270 fn default() -> Self {
271 Self::new()
272 }
273}
274
275impl Scale for ScaleDateTime {
276 fn aesthetic(&self) -> Aesthetic {
277 self.aesthetic.clone()
278 }
279
280 fn train(&mut self, values: &[Value]) {
281 for v in values {
282 if let Some(f) = v.as_f64() {
283 if f.is_finite() {
284 if f < self.min {
285 self.min = f;
286 }
287 if f > self.max {
288 self.max = f;
289 }
290 }
291 }
292 }
293 self.trained = true;
294 }
295
296 fn map(&self, value: &Value) -> f64 {
297 let f = match value.as_f64() {
298 Some(f) => f,
299 None => return 0.0,
300 };
301 if f == f64::INFINITY {
303 return 1.0;
304 }
305 if f == f64::NEG_INFINITY {
306 return 0.0;
307 }
308 if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
311 return 0.5;
312 }
313 let (emin, emax) = self.expanded_range();
314 let range = emax - emin;
315 if range.abs() < f64::EPSILON {
316 0.5
317 } else {
318 (f - emin) / range
319 }
320 }
321
322 fn expanded_domain(&self) -> Option<(f64, f64)> {
323 if !self.trained || !self.min.is_finite() || !self.max.is_finite() {
324 return None;
325 }
326 let (a, b) = self.expanded_range();
327 if !(a.is_finite() && b.is_finite()) {
328 return None;
329 }
330 if (b - a).abs() < f64::EPSILON {
331 return Some((a - 0.5, b + 0.5));
333 }
334 Some((a, b))
335 }
336
337 fn breaks(&self) -> Vec<(f64, String)> {
338 if !self.trained || self.min > self.max {
339 return vec![];
340 }
341
342 let range = self.max - self.min;
343 if super::util::is_degenerate_range(self.min, self.max) {
344 return vec![(0.5, self.label(self.min, 1.0))];
345 }
346
347 let (emin, emax) = self.expanded_range();
348 const MAX_ABS_SECS: f64 = 1e12;
351 if !(emin.abs() <= MAX_ABS_SECS && emax.abs() <= MAX_ABS_SECS) {
352 return vec![];
353 }
354
355 if let Some(DateBreak::Months(n)) = self.date_breaks {
357 let n = n.max(1);
358 let start = civil_from_secs(emin.ceil() as i64);
359 let (mut y, mut m) = (start.year, start.month);
360 if (secs_from_civil(y, m, 1) as f64) < emin {
362 m += 1;
363 if m > 12 {
364 m = 1;
365 y += 1;
366 }
367 }
368 let mut breaks = Vec::new();
369 let mut guard = 0;
370 loop {
371 let secs = secs_from_civil(y, m, 1) as f64;
372 if secs > emax + 1.0 || guard > 10_000 {
373 break;
374 }
375 breaks.push((self.map(&Value::Float(secs)), self.label(secs, 0.0)));
376 m += n;
377 while m > 12 {
378 m -= 12;
379 y += 1;
380 }
381 guard += 1;
382 }
383 return breaks;
384 }
385
386 let explicit_secs = matches!(self.date_breaks, Some(DateBreak::Secs(s)) if s > 0.0);
387 let step = match self.date_breaks {
388 Some(DateBreak::Secs(s)) if s > 0.0 => s,
389 _ => Self::nice_datetime_step(range),
390 };
391
392 const DAY: f64 = 86_400.0;
396 const MONTH: f64 = 30.0 * DAY;
397 const YEAR: f64 = 365.25 * DAY;
398 if !explicit_secs && step >= 0.9 * YEAR {
399 let n = ((step / YEAR).round() as i64).max(1);
400 let mut y = civil_from_secs(emin.ceil() as i64).year.div_euclid(n) * n;
401 while (secs_from_civil(y, 1, 1) as f64) < emin {
402 y += n;
403 }
404 let mut breaks = Vec::new();
405 while (secs_from_civil(y, 1, 1) as f64) <= emax + 1.0 {
406 let secs = secs_from_civil(y, 1, 1) as f64;
407 breaks.push((self.map(&Value::Float(secs)), format!("{y}")));
408 y += n;
409 }
410 return breaks;
411 }
412 if !explicit_secs && step >= 0.9 * MONTH {
413 let n = ((step / MONTH).round() as i64).max(1);
414 let start = civil_from_secs(emin.ceil() as i64);
415 let mut tm = start.year * 12 + (start.month as i64 - 1); while (secs_from_civil(tm.div_euclid(12), tm.rem_euclid(12) as u32 + 1, 1) as f64)
417 < emin
418 {
419 tm += 1;
420 }
421 let mut breaks = Vec::new();
422 let mut guard = 0;
423 loop {
424 let (y, m) = (tm.div_euclid(12), tm.rem_euclid(12) as u32 + 1);
425 let secs = secs_from_civil(y, m, 1) as f64;
426 if secs > emax + 1.0 || guard > 10_000 {
427 break;
428 }
429 breaks.push((self.map(&Value::Float(secs)), format!("{y:04}-{m:02}")));
430 tm += n;
431 guard += 1;
432 }
433 return breaks;
434 }
435
436 let start = (emin / step).ceil() * step;
437 super::util::stepped_breaks(start, emax, step)
438 .into_iter()
439 .map(|v| (self.map(&Value::Float(v)), self.label(v, step)))
440 .collect()
441 }
442
443 fn name(&self) -> &str {
444 &self.name
445 }
446
447 fn set_name(&mut self, name: &str) {
448 self.name = name.to_string();
449 }
450
451 fn set_limits(&mut self, min: f64, max: f64) {
452 self.min = min;
453 self.max = max;
454 self.trained = true;
455 }
456
457 fn clone_box(&self) -> Box<dyn Scale> {
458 Box::new(self.clone())
459 }
460
461 fn reset_training(&mut self) {
462 self.min = f64::INFINITY;
463 self.max = f64::NEG_INFINITY;
464 self.trained = false;
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn civil_roundtrip() {
474 let p = civil_from_secs(1_615_811_445);
476 assert_eq!((p.year, p.month, p.day), (2021, 3, 15));
477 assert_eq!((p.hour, p.minute, p.second), (12, 30, 45));
478 assert_eq!(secs_from_civil(2021, 3, 15), 1_615_766_400); }
480
481 #[test]
482 fn multiyear_default_breaks_are_clean_years() {
483 let mut s = ScaleDateTime::new();
486 s.train(&[
487 Value::DateTime(secs_from_civil(2011, 2, 1)),
488 Value::DateTime(secs_from_civil(2016, 4, 1)),
489 ]);
490 let labels: Vec<String> = s.breaks().into_iter().map(|(_, l)| l).collect();
491 assert!(!labels.is_empty());
492 for l in &labels {
493 assert!(
494 l.len() == 4 && l.chars().all(|c| c.is_ascii_digit()),
495 "expected a bare year label, got {l:?}"
496 );
497 }
498 assert!(labels.contains(&"2014".to_string()));
499 }
500
501 #[test]
502 fn strftime_subset() {
503 let s = 1_615_766_400.0; assert_eq!(strftime(s, "%Y-%m-%d"), "2021-03-15");
505 assert_eq!(strftime(s, "%b %Y"), "Mar 2021");
506 assert_eq!(strftime(s, "%B"), "March");
507 assert_eq!(strftime(s, "100%%"), "100%");
508 }
509
510 #[test]
511 fn parse_specs() {
512 assert!(matches!(
513 parse_date_break("1 month"),
514 Some(DateBreak::Months(1))
515 ));
516 assert!(matches!(
517 parse_date_break("3 months"),
518 Some(DateBreak::Months(3))
519 ));
520 assert!(matches!(
521 parse_date_break("1 year"),
522 Some(DateBreak::Months(12))
523 ));
524 assert!(
525 matches!(parse_date_break("2 weeks"), Some(DateBreak::Secs(s)) if s == 1_209_600.0)
526 );
527 assert!(matches!(
528 parse_date_break("month"),
529 Some(DateBreak::Months(1))
530 ));
531 assert!(parse_date_break("fortnight").is_none());
532 }
533
534 #[test]
535 fn monthly_breaks_land_on_first_of_month() {
536 let mut s = ScaleDateTime::new()
537 .with_date_breaks("1 month")
538 .with_date_labels("%Y-%m-%d");
539 s.set_limits(
541 secs_from_civil(2021, 1, 10) as f64,
542 secs_from_civil(2021, 4, 20) as f64,
543 );
544 let labels: Vec<String> = s.breaks().into_iter().map(|(_, l)| l).collect();
545 assert!(labels.iter().all(|l| l.ends_with("-01")), "{labels:?}");
546 assert!(labels.contains(&"2021-02-01".to_string()));
547 }
548}