1use crate::aes::Aesthetic;
5use crate::data::{DataFrame, Value};
6use crate::scale::ScaleSet;
7
8use super::Stat;
9
10pub const MAX_CALENDAR_YEARS: i64 = 50;
14const MAX_SPAN_DAYS: i64 = MAX_CALENDAR_YEARS * 366;
15
16pub const DATE_KEY_COL: &str = ".key";
19
20pub fn day_number(v: &Value) -> Option<i64> {
24 const LIMIT: f64 = 1.0e14; match v {
26 Value::DateTime(s) => Some(s.div_euclid(86_400)),
27 Value::Integer(i) => Some(i.div_euclid(86_400)),
28 Value::Float(f) if f.is_finite() && f.abs() < LIMIT => Some((f / 86_400.0).floor() as i64),
29 Value::Str(s) => parse_iso_date(s),
30 _ => None,
31 }
32}
33
34fn parse_iso_date(s: &str) -> Option<i64> {
35 let s = s.trim();
36 let date = s.get(..10)?;
37 let mut it = date.split('-');
38 let y: i64 = it.next()?.parse().ok()?;
39 let m: u32 = it.next()?.parse().ok()?;
40 let d: u32 = it.next()?.parse().ok()?;
41 if !(1..=12).contains(&m) || !(1..=31).contains(&d) || !(-9999..=9999).contains(&y) {
42 return None;
43 }
44 Some(days_from_civil(y, m, d))
45}
46
47pub fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
49 let y = if m <= 2 { y - 1 } else { y };
50 let era = if y >= 0 { y } else { y - 399 } / 400;
51 let yoe = y - era * 400;
52 let m = m as i64;
53 let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d as i64 - 1;
54 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
55 era * 146_097 + doe - 719_468
56}
57
58pub 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;
63 let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
64 let y = yoe + era * 400;
65 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
66 let mp = (5 * doy + 2) / 153;
67 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
68 let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
69 (if m <= 2 { y + 1 } else { y }, m, d)
70}
71
72pub fn weekday(day: i64, monday_first: bool) -> i64 {
74 (day + if monday_first { 3 } else { 4 }).rem_euclid(7)
76}
77
78#[derive(Clone, Copy, Debug)]
81pub struct CalendarGrid {
82 pub origin: i64,
84 pub first_day: i64,
85 pub last_day: i64,
86 pub monday_first: bool,
87}
88
89impl CalendarGrid {
90 pub fn from_days(days: impl Iterator<Item = i64>, monday_first: bool) -> Option<(Self, bool)> {
93 let (mut lo, mut hi) = (i64::MAX, i64::MIN);
94 for d in days {
95 lo = lo.min(d);
96 hi = hi.max(d);
97 }
98 if lo > hi {
99 return None;
100 }
101 let clipped = hi - lo > MAX_SPAN_DAYS;
102 let lo = lo.max(hi - MAX_SPAN_DAYS);
103 Some((
104 CalendarGrid {
105 origin: lo - weekday(lo, monday_first),
106 first_day: lo,
107 last_day: hi,
108 monday_first,
109 },
110 clipped,
111 ))
112 }
113
114 pub fn column(&self, day: i64) -> i64 {
116 (day - self.origin).div_euclid(7)
117 }
118
119 pub fn row(&self, day: i64) -> i64 {
121 6 - weekday(day, self.monday_first)
122 }
123
124 pub fn n_weeks(&self) -> i64 {
125 self.column(self.last_day) + 1
126 }
127
128 pub fn contains(&self, day: i64) -> bool {
129 (self.first_day..=self.last_day).contains(&day)
130 }
131}
132
133#[derive(Clone, Debug, Default)]
139pub struct StatCalendar {
140 pub grid: Option<CalendarGrid>,
143 pub monday_first: bool,
145}
146
147impl Stat for StatCalendar {
148 fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
149 let Some(x) = data.column("x") else {
150 return DataFrame::new();
151 };
152 let days: Vec<Option<i64>> = x.iter().map(day_number).collect();
153 let grid = match self.grid {
154 Some(g) => g,
155 None => {
156 match CalendarGrid::from_days(days.iter().flatten().copied(), self.monday_first) {
157 Some((g, _)) => g,
158 None => return DataFrame::new(),
159 }
160 }
161 };
162 let rows: Vec<(usize, i64)> = days
163 .iter()
164 .enumerate()
165 .filter_map(|(i, d)| d.filter(|d| grid.contains(*d)).map(|d| (i, d)))
166 .collect();
167 let mut out = DataFrame::new();
168 let col = |off: f64, row: bool| -> Vec<Value> {
169 rows.iter()
170 .map(|&(_, d)| {
171 let base = if row { grid.row(d) } else { grid.column(d) };
172 Value::Float(base as f64 + off)
173 })
174 .collect()
175 };
176 out.add_column("x".to_string(), col(0.0, false));
177 out.add_column("y".to_string(), col(0.0, true));
178 out.add_column("xmin".to_string(), col(-0.5, false));
179 out.add_column("xmax".to_string(), col(0.5, false));
180 out.add_column("ymin".to_string(), col(-0.5, true));
181 out.add_column("ymax".to_string(), col(0.5, true));
182 out.add_column(
183 DATE_KEY_COL.to_string(),
184 rows.iter()
185 .map(|&(_, d)| {
186 let (y, m, dd) = civil_from_days(d);
187 Value::Str(format!("{y:04}-{m:02}-{dd:02}"))
188 })
189 .collect(),
190 );
191 for name in data.column_names() {
192 if out.has_column(name) {
193 continue;
194 }
195 if let Some(src) = data.column(name) {
196 out.add_column(
197 name.to_string(),
198 rows.iter().map(|&(i, _)| src[i].clone()).collect(),
199 );
200 }
201 }
202 out
203 }
204
205 fn required_aes(&self) -> Vec<Aesthetic> {
206 vec![Aesthetic::X]
207 }
208
209 fn name(&self) -> &str {
210 "calendar"
211 }
212}
213
214const MONTHS: [&str; 12] = [
215 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
216];
217
218pub fn month_breaks(grid: &CalendarGrid) -> (Vec<f64>, Vec<String>) {
222 let starts = month_starts(grid);
223 let (y0, _, _) = civil_from_days(grid.first_day);
224 let (y1, _, _) = civil_from_days(grid.last_day);
225 let many = starts.len() > 24;
226 let mut breaks = Vec::new();
227 let mut labels = Vec::new();
228 for (day, y, m) in starts {
229 if many && m != 1 {
230 continue;
231 }
232 breaks.push(grid.column(day) as f64);
233 labels.push(if many {
234 y.to_string()
235 } else if y0 != y1 && m == 1 {
236 format!("{} {y}", MONTHS[0])
237 } else {
238 MONTHS[(m - 1) as usize].to_string()
239 });
240 }
241 (breaks, labels)
242}
243
244fn month_starts(grid: &CalendarGrid) -> Vec<(i64, i64, u32)> {
247 let (mut y, mut m, _) = civil_from_days(grid.first_day);
248 let mut out = vec![(grid.first_day, y, m)];
249 loop {
250 (y, m) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
251 let d = days_from_civil(y, m, 1);
252 if d > grid.last_day {
253 break;
254 }
255 out.push((d, y, m));
256 }
257 out
258}
259
260pub fn month_boundaries(grid: &CalendarGrid) -> Vec<(f64, f64, f64, f64)> {
263 let mut segs = Vec::new();
264 for (day, _, _) in month_starts(grid).into_iter().skip(1) {
265 let c = grid.column(day) as f64;
266 let w = weekday(day, grid.monday_first) as f64;
267 let row_top = 6.5 - w;
268 if w > 0.0 {
269 segs.push((c + 0.5, 6.5, c + 0.5, row_top));
270 segs.push((c + 0.5, row_top, c - 0.5, row_top));
271 }
272 segs.push((c - 0.5, row_top, c - 0.5, -0.5));
273 }
274 segs
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn civil_roundtrip() {
283 for d in [-1_000_000, -1, 0, 1, 19_000, 2_000_000] {
284 let (y, m, dd) = civil_from_days(d);
285 assert_eq!(days_from_civil(y, m, dd), d);
286 }
287 assert_eq!(days_from_civil(1970, 1, 1), 0);
288 assert_eq!(weekday(0, false), 4); assert_eq!(weekday(0, true), 3);
290 }
291
292 #[test]
293 fn huge_span_is_clipped() {
294 let (g, clipped) =
295 CalendarGrid::from_days([-5_000_000, 20_000].into_iter(), false).unwrap();
296 assert!(clipped);
297 assert!(g.n_weeks() <= MAX_SPAN_DAYS / 7 + 2);
298 assert!(month_starts(&g).len() <= (MAX_CALENDAR_YEARS as usize + 1) * 12 + 1);
299 }
300}