use crate::aes::Aesthetic;
use crate::data::{DataFrame, Value};
use crate::scale::ScaleSet;
use super::Stat;
pub const MAX_CALENDAR_YEARS: i64 = 50;
const MAX_SPAN_DAYS: i64 = MAX_CALENDAR_YEARS * 366;
pub const DATE_KEY_COL: &str = ".key";
pub fn day_number(v: &Value) -> Option<i64> {
const LIMIT: f64 = 1.0e14; match v {
Value::DateTime(s) => Some(s.div_euclid(86_400)),
Value::Integer(i) => Some(i.div_euclid(86_400)),
Value::Float(f) if f.is_finite() && f.abs() < LIMIT => Some((f / 86_400.0).floor() as i64),
Value::Str(s) => parse_iso_date(s),
_ => None,
}
}
fn parse_iso_date(s: &str) -> Option<i64> {
let s = s.trim();
let date = s.get(..10)?;
let mut it = date.split('-');
let y: i64 = it.next()?.parse().ok()?;
let m: u32 = it.next()?.parse().ok()?;
let d: u32 = it.next()?.parse().ok()?;
if !(1..=12).contains(&m) || !(1..=31).contains(&d) || !(-9999..=9999).contains(&y) {
return None;
}
Some(days_from_civil(y, m, d))
}
pub fn days_from_civil(y: i64, m: u32, d: u32) -> i64 {
let y = if m <= 2 { y - 1 } else { y };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let m = m as i64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
pub fn civil_from_days(z: i64) -> (i64, u32, u32) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
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;
(if m <= 2 { y + 1 } else { y }, m, d)
}
pub fn weekday(day: i64, monday_first: bool) -> i64 {
(day + if monday_first { 3 } else { 4 }).rem_euclid(7)
}
#[derive(Clone, Copy, Debug)]
pub struct CalendarGrid {
pub origin: i64,
pub first_day: i64,
pub last_day: i64,
pub monday_first: bool,
}
impl CalendarGrid {
pub fn from_days(days: impl Iterator<Item = i64>, monday_first: bool) -> Option<(Self, bool)> {
let (mut lo, mut hi) = (i64::MAX, i64::MIN);
for d in days {
lo = lo.min(d);
hi = hi.max(d);
}
if lo > hi {
return None;
}
let clipped = hi - lo > MAX_SPAN_DAYS;
let lo = lo.max(hi - MAX_SPAN_DAYS);
Some((
CalendarGrid {
origin: lo - weekday(lo, monday_first),
first_day: lo,
last_day: hi,
monday_first,
},
clipped,
))
}
pub fn column(&self, day: i64) -> i64 {
(day - self.origin).div_euclid(7)
}
pub fn row(&self, day: i64) -> i64 {
6 - weekday(day, self.monday_first)
}
pub fn n_weeks(&self) -> i64 {
self.column(self.last_day) + 1
}
pub fn contains(&self, day: i64) -> bool {
(self.first_day..=self.last_day).contains(&day)
}
}
#[derive(Clone, Debug, Default)]
pub struct StatCalendar {
pub grid: Option<CalendarGrid>,
pub monday_first: bool,
}
impl Stat for StatCalendar {
fn compute_group(&self, data: &DataFrame, _scales: &ScaleSet) -> DataFrame {
let Some(x) = data.column("x") else {
return DataFrame::new();
};
let days: Vec<Option<i64>> = x.iter().map(day_number).collect();
let grid = match self.grid {
Some(g) => g,
None => {
match CalendarGrid::from_days(days.iter().flatten().copied(), self.monday_first) {
Some((g, _)) => g,
None => return DataFrame::new(),
}
}
};
let rows: Vec<(usize, i64)> = days
.iter()
.enumerate()
.filter_map(|(i, d)| d.filter(|d| grid.contains(*d)).map(|d| (i, d)))
.collect();
let mut out = DataFrame::new();
let col = |off: f64, row: bool| -> Vec<Value> {
rows.iter()
.map(|&(_, d)| {
let base = if row { grid.row(d) } else { grid.column(d) };
Value::Float(base as f64 + off)
})
.collect()
};
out.add_column("x".to_string(), col(0.0, false));
out.add_column("y".to_string(), col(0.0, true));
out.add_column("xmin".to_string(), col(-0.5, false));
out.add_column("xmax".to_string(), col(0.5, false));
out.add_column("ymin".to_string(), col(-0.5, true));
out.add_column("ymax".to_string(), col(0.5, true));
out.add_column(
DATE_KEY_COL.to_string(),
rows.iter()
.map(|&(_, d)| {
let (y, m, dd) = civil_from_days(d);
Value::Str(format!("{y:04}-{m:02}-{dd:02}"))
})
.collect(),
);
for name in data.column_names() {
if out.has_column(name) {
continue;
}
if let Some(src) = data.column(name) {
out.add_column(
name.to_string(),
rows.iter().map(|&(i, _)| src[i].clone()).collect(),
);
}
}
out
}
fn required_aes(&self) -> Vec<Aesthetic> {
vec![Aesthetic::X]
}
fn name(&self) -> &str {
"calendar"
}
}
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
pub fn month_breaks(grid: &CalendarGrid) -> (Vec<f64>, Vec<String>) {
let starts = month_starts(grid);
let (y0, _, _) = civil_from_days(grid.first_day);
let (y1, _, _) = civil_from_days(grid.last_day);
let many = starts.len() > 24;
let mut breaks = Vec::new();
let mut labels = Vec::new();
for (day, y, m) in starts {
if many && m != 1 {
continue;
}
breaks.push(grid.column(day) as f64);
labels.push(if many {
y.to_string()
} else if y0 != y1 && m == 1 {
format!("{} {y}", MONTHS[0])
} else {
MONTHS[(m - 1) as usize].to_string()
});
}
(breaks, labels)
}
fn month_starts(grid: &CalendarGrid) -> Vec<(i64, i64, u32)> {
let (mut y, mut m, _) = civil_from_days(grid.first_day);
let mut out = vec![(grid.first_day, y, m)];
loop {
(y, m) = if m == 12 { (y + 1, 1) } else { (y, m + 1) };
let d = days_from_civil(y, m, 1);
if d > grid.last_day {
break;
}
out.push((d, y, m));
}
out
}
pub fn month_boundaries(grid: &CalendarGrid) -> Vec<(f64, f64, f64, f64)> {
let mut segs = Vec::new();
for (day, _, _) in month_starts(grid).into_iter().skip(1) {
let c = grid.column(day) as f64;
let w = weekday(day, grid.monday_first) as f64;
let row_top = 6.5 - w;
if w > 0.0 {
segs.push((c + 0.5, 6.5, c + 0.5, row_top));
segs.push((c + 0.5, row_top, c - 0.5, row_top));
}
segs.push((c - 0.5, row_top, c - 0.5, -0.5));
}
segs
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn civil_roundtrip() {
for d in [-1_000_000, -1, 0, 1, 19_000, 2_000_000] {
let (y, m, dd) = civil_from_days(d);
assert_eq!(days_from_civil(y, m, dd), d);
}
assert_eq!(days_from_civil(1970, 1, 1), 0);
assert_eq!(weekday(0, false), 4); assert_eq!(weekday(0, true), 3);
}
#[test]
fn huge_span_is_clipped() {
let (g, clipped) =
CalendarGrid::from_days([-5_000_000, 20_000].into_iter(), false).unwrap();
assert!(clipped);
assert!(g.n_weeks() <= MAX_SPAN_DAYS / 7 + 2);
assert!(month_starts(&g).len() <= (MAX_CALENDAR_YEARS as usize + 1) * 12 + 1);
}
}