use chrono::Datelike;
use super::{TransformKind, TransformTrait};
use crate::plot::scale::breaks::{integer_breaks, minor_breaks_linear};
use crate::plot::ArrayElement;
#[derive(Debug, Clone, Copy)]
pub struct Date;
#[derive(Debug, Clone, Copy, PartialEq)]
enum DateInterval {
Year,
Quarter,
Month,
Week,
Day,
}
impl DateInterval {
fn days(&self) -> f64 {
match self {
DateInterval::Year => 365.25,
DateInterval::Quarter => 91.3125, DateInterval::Month => 30.4375, DateInterval::Week => 7.0,
DateInterval::Day => 1.0,
}
}
fn expected_breaks(&self, span_days: f64) -> f64 {
span_days / self.days()
}
fn select(span_days: f64, n: usize) -> (Self, usize) {
let n_f64 = n as f64;
let tolerance = 0.2; let min_breaks = n_f64 * (1.0 - tolerance);
let max_breaks = n_f64 * (1.0 + tolerance);
let intervals = [
DateInterval::Year,
DateInterval::Quarter,
DateInterval::Month,
DateInterval::Week,
DateInterval::Day,
];
for &interval in &intervals {
let expected = interval.expected_breaks(span_days);
if expected < 1.0 {
continue;
}
if expected >= min_breaks && expected <= max_breaks {
return (interval, 1);
}
if expected > max_breaks {
let raw_step = expected / n_f64;
let nice = match interval {
DateInterval::Year => nice_step(raw_step) as usize,
DateInterval::Quarter => nice_quarter_step(raw_step),
DateInterval::Month => nice_month_step(raw_step),
DateInterval::Week => nice_week_step(raw_step),
DateInterval::Day => nice_step(raw_step) as usize,
};
let step = nice.max(1);
let stepped_breaks = expected / step as f64;
if stepped_breaks >= 1.0 {
return (interval, step);
}
}
}
let expected = DateInterval::Day.expected_breaks(span_days);
let step = (nice_step(expected / n_f64) as usize).max(1);
(DateInterval::Day, step)
}
}
impl TransformTrait for Date {
fn transform_kind(&self) -> TransformKind {
TransformKind::Date
}
fn name(&self) -> &'static str {
"date"
}
fn allowed_domain(&self) -> (f64, f64) {
(f64::NEG_INFINITY, f64::INFINITY)
}
fn transform(&self, value: f64) -> f64 {
value
}
fn inverse(&self, value: f64) -> f64 {
value
}
fn calculate_breaks(&self, min: f64, max: f64, n: usize, pretty: bool) -> Vec<f64> {
if n == 0 || min >= max {
return vec![];
}
let span = max - min;
let (interval, step) = DateInterval::select(span, n);
if pretty {
calculate_pretty_date_breaks(min, max, interval, step)
} else {
integer_breaks(min, max, n, false)
}
}
fn calculate_minor_breaks(
&self,
major_breaks: &[f64],
n: usize,
range: Option<(f64, f64)>,
) -> Vec<f64> {
minor_breaks_linear(major_breaks, n, range)
}
fn default_minor_break_count(&self) -> usize {
3
}
fn wrap_numeric(&self, value: f64) -> ArrayElement {
ArrayElement::Date(value as i32)
}
fn parse_value(&self, elem: &ArrayElement) -> ArrayElement {
match elem {
ArrayElement::String(s) => {
ArrayElement::from_date_string(s).unwrap_or_else(|| elem.clone())
}
ArrayElement::Number(n) => self.wrap_numeric(*n),
ArrayElement::Date(_) => elem.clone(),
other => other.clone(),
}
}
}
fn calculate_pretty_date_breaks(
min: f64,
max: f64,
interval: DateInterval,
step: usize,
) -> Vec<f64> {
let unix_epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
let min_date = unix_epoch + chrono::Duration::days(min.floor() as i64);
let max_date = unix_epoch + chrono::Duration::days(max.ceil() as i64);
let mut breaks = Vec::new();
match interval {
DateInterval::Year => {
let start_year = min_date.year();
let end_year = max_date.year();
let step = step as i32;
let aligned_start = (start_year / step) * step;
let mut year = aligned_start;
while year <= end_year + step {
if let Some(date) = chrono::NaiveDate::from_ymd_opt(year, 1, 1) {
let days = (date - unix_epoch).num_days() as f64;
if days >= min && days <= max {
breaks.push(days);
}
}
year += step;
}
}
DateInterval::Quarter => {
let start_year = min_date.year();
let start_quarter = (min_date.month() - 1) / 3;
let end_year = max_date.year();
let end_quarter = (max_date.month() - 1) / 3;
let aligned_start_quarter = (start_quarter / step as u32) * step as u32;
let mut year = start_year;
let mut quarter = aligned_start_quarter;
while year < end_year || (year == end_year && quarter <= end_quarter) {
let month = quarter * 3 + 1;
if let Some(date) = chrono::NaiveDate::from_ymd_opt(year, month, 1) {
let days = (date - unix_epoch).num_days() as f64;
if days >= min && days <= max {
breaks.push(days);
}
}
quarter += step as u32;
if quarter > 3 {
let years_advance = quarter / 4;
quarter %= 4;
year += years_advance as i32;
}
}
}
DateInterval::Month => {
let start_year = min_date.year();
let start_month = min_date.month();
let end_year = max_date.year();
let end_month = max_date.month();
let mut year = start_year;
let mut month = ((start_month - 1) / step as u32) * step as u32 + 1;
while year < end_year || (year == end_year && month <= end_month) {
if let Some(date) = chrono::NaiveDate::from_ymd_opt(year, month, 1) {
let days = (date - unix_epoch).num_days() as f64;
if days >= min && days <= max {
breaks.push(days);
}
}
month += step as u32;
if month > 12 {
month -= 12;
year += 1;
}
}
}
DateInterval::Week => {
let start_days = min.floor() as i64;
let weekday = (start_days.rem_euclid(7) + 3) % 7; let first_monday = start_days - weekday;
let end_days = max.ceil() as i64;
let step_days = (step * 7) as i64;
let mut day = first_monday;
while day <= end_days {
let days = day as f64;
if days >= min && days <= max {
breaks.push(days);
}
day += step_days;
}
}
DateInterval::Day => {
let step = step as i64;
let start_day = (min / step as f64).floor() as i64 * step;
let end_day = max.ceil() as i64;
let mut day = start_day;
while day <= end_day {
let days = day as f64;
if days >= min && days <= max {
breaks.push(days);
}
day += step;
}
}
}
if breaks.is_empty() {
breaks.push(min);
if max > min {
breaks.push(max);
}
}
breaks
}
fn nice_step(step: f64) -> f64 {
if step <= 0.0 {
return 1.0;
}
let magnitude = 10_f64.powf(step.log10().floor());
let residual = step / magnitude;
let nice = if residual <= 1.5 {
1.0
} else if residual <= 3.0 {
2.0
} else if residual <= 7.0 {
5.0
} else {
10.0
};
nice * magnitude
}
fn nice_week_step(step: f64) -> usize {
if step <= 1.5 {
1
} else if step <= 3.0 {
2
} else {
4
}
}
fn nice_quarter_step(step: f64) -> usize {
if step <= 1.5 {
1
} else if step <= 3.0 {
2
} else {
4
}
}
fn nice_month_step(step: f64) -> usize {
if step <= 1.0 {
1
} else if step <= 2.0 {
2
} else if step <= 4.0 {
3
} else if step <= 8.0 {
6
} else {
12
}
}
impl std::fmt::Display for Date {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_date_transform_kind() {
let t = Date;
assert_eq!(t.transform_kind(), TransformKind::Date);
}
#[test]
fn test_date_name() {
let t = Date;
assert_eq!(t.name(), "date");
}
#[test]
fn test_date_domain() {
let t = Date;
let (min, max) = t.allowed_domain();
assert!(min < 0.0);
assert!(max > 0.0);
}
#[test]
fn test_date_transform_is_identity() {
let t = Date;
assert_eq!(t.transform(100.0), 100.0);
assert_eq!(t.transform(-50.0), -50.0);
assert_eq!(t.inverse(100.0), 100.0);
assert_eq!(t.inverse(-50.0), -50.0);
}
#[test]
fn test_date_breaks_year_span() {
let t = Date;
let min = 0.0; let max = 365.0 * 5.0; let breaks = t.calculate_breaks(min, max, 5, true);
assert!(!breaks.is_empty());
for &b in &breaks {
assert!(b >= min && b <= max);
}
}
#[test]
fn test_date_breaks_month_span() {
let t = Date;
let min = 0.0;
let max = 180.0;
let breaks = t.calculate_breaks(min, max, 6, true);
assert!(!breaks.is_empty());
}
#[test]
fn test_date_breaks_week_span() {
let t = Date;
let min = 0.0;
let max = 28.0;
let breaks = t.calculate_breaks(min, max, 5, true);
assert!(!breaks.is_empty());
}
#[test]
fn test_date_breaks_day_span() {
let t = Date;
let min = 0.0;
let max = 7.0;
let breaks = t.calculate_breaks(min, max, 7, true);
assert!(!breaks.is_empty());
}
#[test]
fn test_date_breaks_linear() {
let t = Date;
let breaks = t.calculate_breaks(0.0, 100.0, 5, false);
assert!(!breaks.is_empty());
assert!(breaks[0] <= 0.0);
assert!(*breaks.last().unwrap() >= 100.0);
for b in &breaks {
assert_eq!(*b, b.round(), "Break {} should be a whole day", b);
}
if breaks.len() >= 2 {
let step = breaks[1] - breaks[0];
for i in 1..breaks.len() {
let gap = breaks[i] - breaks[i - 1];
assert!(
(gap - step).abs() < 0.01,
"Date breaks should be evenly spaced"
);
}
}
}
#[test]
fn test_date_interval_selection() {
let (interval, step) = DateInterval::select(3650.0, 5);
assert_eq!(interval, DateInterval::Year);
assert!(step >= 1);
let (interval, step) = DateInterval::select(180.0, 6);
assert_eq!(interval, DateInterval::Month);
assert!(step >= 1);
let (interval, step) = DateInterval::select(28.0, 4);
assert_eq!(interval, DateInterval::Week);
assert!(step >= 1);
let (interval, step) = DateInterval::select(7.0, 7);
assert_eq!(interval, DateInterval::Day);
assert!(step >= 1);
}
#[test]
fn test_date_interval_selection_airquality() {
let (interval, step) = DateInterval::select(150.0, 7);
let expected_breaks = interval.expected_breaks(150.0) / step as f64;
assert!(
(3.0..=10.0).contains(&expected_breaks),
"Expected 3-10 breaks for 150 days, n=7, got {} ({:?} with step {})",
expected_breaks,
interval,
step
);
}
#[test]
fn test_date_breaks_airquality_count() {
let t = Date;
let min = 0.0;
let max = 150.0;
let breaks = t.calculate_breaks(min, max, 7, true);
assert!(
breaks.len() >= 3 && breaks.len() <= 12,
"Expected 3-12 breaks for 150 days, n=7, got {}",
breaks.len()
);
}
#[test]
fn test_nice_step() {
assert_eq!(nice_step(1.0), 1.0);
assert_eq!(nice_step(1.5), 1.0); assert_eq!(nice_step(1.6), 2.0); assert_eq!(nice_step(3.0), 2.0); assert_eq!(nice_step(3.5), 5.0); assert_eq!(nice_step(7.0), 5.0);
assert_eq!(nice_step(8.0), 10.0);
assert_eq!(nice_step(15.0), 10.0); assert_eq!(nice_step(16.0), 20.0); }
}