use chrono::NaiveDateTime;
use super::tick_step;
pub struct ScaleTime {
domain: (NaiveDateTime, NaiveDateTime),
range: (f64, f64),
}
impl ScaleTime {
pub fn new(domain: (NaiveDateTime, NaiveDateTime), range: (f64, f64)) -> Self {
Self { domain, range }
}
pub fn map(&self, value: &NaiveDateTime) -> f64 {
let t = self.to_timestamp(value);
let (t0, t1) = self.domain_timestamps();
let (r0, r1) = self.range;
let domain_span = t1 - t0;
if domain_span == 0.0 {
return (r0 + r1) / 2.0;
}
r0 + (t - t0) / domain_span * (r1 - r0)
}
pub fn invert(&self, value: f64) -> NaiveDateTime {
let (t0, t1) = self.domain_timestamps();
let (r0, r1) = self.range;
let range_span = r1 - r0;
let t = if range_span == 0.0 {
(t0 + t1) / 2.0
} else {
t0 + (value - r0) / range_span * (t1 - t0)
};
self.secs_to_datetime(t)
}
pub fn ticks(&self, count: usize) -> Vec<NaiveDateTime> {
if count == 0 {
return vec![];
}
let (t0, t1) = self.domain_timestamps();
let min = t0.min(t1);
let max = t0.max(t1);
if min == max {
return vec![self.secs_to_datetime(min)];
}
let step = tick_step(min, max, count);
if step == 0.0 || !step.is_finite() {
return vec![];
}
let mut ticks = Vec::new();
let start = (min / step).ceil();
let stop = (max / step).floor();
let mut i = start;
while i <= stop {
let tick = i * step;
ticks.push(self.secs_to_datetime(tick));
i += 1.0;
}
if t0 > t1 {
ticks.reverse();
}
ticks
}
pub fn domain(&self) -> (NaiveDateTime, NaiveDateTime) {
self.domain
}
pub fn range(&self) -> (f64, f64) {
self.range
}
fn to_timestamp(&self, dt: &NaiveDateTime) -> f64 {
dt.and_utc().timestamp() as f64
}
fn domain_timestamps(&self) -> (f64, f64) {
(
self.to_timestamp(&self.domain.0),
self.to_timestamp(&self.domain.1),
)
}
fn secs_to_datetime(&self, secs: f64) -> NaiveDateTime {
let secs = (secs as i64).clamp(-8_334_632_851_200, 8_210_298_412_799);
chrono::DateTime::from_timestamp(secs, 0)
.unwrap_or_else(|| {
chrono::DateTime::from_timestamp(0, 0).unwrap()
})
.naive_utc()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDate;
fn make_dt(year: i32, month: u32, day: u32) -> NaiveDateTime {
NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
}
#[test]
fn time_scale_maps_midpoint() {
let start = make_dt(2024, 1, 1);
let end = make_dt(2024, 1, 31);
let mid = make_dt(2024, 1, 16);
let scale = ScaleTime::new((start, end), (0.0, 100.0));
let result = scale.map(&mid);
assert!(
(result - 50.0).abs() < 2.0,
"midpoint should map close to 50, got {}",
result
);
}
#[test]
fn time_scale_maps_endpoints() {
let start = make_dt(2024, 1, 1);
let end = make_dt(2024, 1, 31);
let scale = ScaleTime::new((start, end), (0.0, 100.0));
assert!(
(scale.map(&start) - 0.0).abs() < 1e-10,
"start should map to 0"
);
assert!(
(scale.map(&end) - 100.0).abs() < 1e-10,
"end should map to 100"
);
}
#[test]
fn time_scale_inverts() {
let start = make_dt(2024, 1, 1);
let end = make_dt(2024, 1, 31);
let scale = ScaleTime::new((start, end), (0.0, 100.0));
let mid_value = 50.0;
let inverted = scale.invert(mid_value);
let mid = make_dt(2024, 1, 16);
let diff_secs = (inverted.and_utc().timestamp() - mid.and_utc().timestamp()).abs();
assert!(
diff_secs < 86400, "inverted date should be close to midpoint, got {:?}",
inverted
);
}
#[test]
fn time_scale_ticks() {
let start = make_dt(2024, 1, 1);
let end = make_dt(2024, 12, 31);
let scale = ScaleTime::new((start, end), (0.0, 100.0));
let ticks = scale.ticks(5);
assert!(
!ticks.is_empty(),
"should generate at least one tick"
);
assert!(
ticks.len() <= 15,
"should not generate too many ticks, got {}",
ticks.len()
);
for tick in &ticks {
assert!(*tick >= start, "tick {:?} should be >= start", tick);
assert!(*tick <= end, "tick {:?} should be <= end", tick);
}
}
}