use super::ticks::{Tick, Ticks};
const MINUTE: i64 = 60;
const HOUR: i64 = 3_600;
const DAY: i64 = 86_400;
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
#[derive(Debug, Clone, Copy, PartialEq)]
enum Interval {
Seconds(i64),
Weeks(i64),
Months(i64),
Years(i64),
}
impl Interval {
fn approximate_seconds(self) -> f64 {
match self {
Interval::Seconds(s) => s as f64,
Interval::Weeks(w) => (w * 7 * DAY) as f64,
Interval::Months(m) => m as f64 * 30.44 * DAY as f64,
Interval::Years(y) => y as f64 * 365.25 * DAY as f64,
}
}
}
fn ladder() -> impl Iterator<Item = Interval> {
let fixed = [
1,
5,
15,
30,
MINUTE,
5 * MINUTE,
15 * MINUTE,
30 * MINUTE,
HOUR,
3 * HOUR,
6 * HOUR,
12 * HOUR,
DAY,
2 * DAY,
]
.into_iter()
.map(Interval::Seconds);
let calendar = [
Interval::Weeks(1),
Interval::Months(1),
Interval::Months(3),
Interval::Months(6),
Interval::Years(1),
]
.into_iter();
let years = (0..).flat_map(|magnitude: u32| {
[2i64, 5, 10]
.into_iter()
.map(move |base| Interval::Years(base * 10i64.pow(magnitude)))
});
fixed.chain(calendar).chain(years)
}
impl Ticks {
pub fn time(min: f64, max: f64, target: usize) -> Ticks {
assert!(
min.is_finite() && max.is_finite(),
"Ticks::time requires finite bounds, got {min} and {max}"
);
let (lo, hi) = if min <= max { (min, max) } else { (max, min) };
let target = target.max(2);
let span = (hi - lo).max(1.0);
let interval = ladder()
.find(|interval| span / interval.approximate_seconds() <= target as f64)
.expect("the ladder is unbounded");
let stamps = generate(lo.floor() as i64, hi.ceil() as i64, interval);
let ticks = label(&stamps, interval);
Ticks::from_time(ticks)
}
}
fn generate(lo: i64, hi: i64, interval: Interval) -> Vec<i64> {
let mut stamps = Vec::new();
match interval {
Interval::Seconds(step) => {
let mut t = lo.div_euclid(step) * step;
if t < lo {
t += step;
}
while t <= hi {
stamps.push(t);
t += step;
}
}
Interval::Weeks(weeks) => {
let step = weeks * 7 * DAY;
let monday_offset = 4 * DAY;
let mut t = (lo - monday_offset).div_euclid(step) * step + monday_offset;
if t < lo {
t += step;
}
while t <= hi {
stamps.push(t);
t += step;
}
}
Interval::Months(step) => {
let (mut year, mut month, _) = civil_from_days(lo.div_euclid(DAY));
month = ((month - 1) / step as u32 * step as u32) + 1;
loop {
let t = days_from_civil(year, month, 1) * DAY;
if t > hi {
break;
}
if t >= lo {
stamps.push(t);
}
let advanced = (month as i64 - 1) + step;
year += (advanced / 12) as i32;
month = (advanced % 12) as u32 + 1;
}
}
Interval::Years(step) => {
let (year, ..) = civil_from_days(lo.div_euclid(DAY));
let mut year = (year as i64).div_euclid(step) * step;
loop {
let t = days_from_civil(year as i32, 1, 1) * DAY;
if t > hi {
break;
}
if t >= lo {
stamps.push(t);
}
year += step;
}
}
}
stamps
}
fn label(stamps: &[i64], interval: Interval) -> Vec<Tick> {
stamps
.iter()
.map(|&t| {
let days = t.div_euclid(DAY);
let second_of_day = t.rem_euclid(DAY);
let (year, month, day) = civil_from_days(days);
let (hour, minute, second) = (
second_of_day / HOUR,
(second_of_day % HOUR) / MINUTE,
second_of_day % MINUTE,
);
let month_name = MONTHS[(month - 1) as usize];
let label = match interval {
Interval::Seconds(step) if step < MINUTE => {
format!("{hour:02}:{minute:02}:{second:02}")
}
Interval::Seconds(step) if step < DAY => {
if second_of_day == 0 {
format!("{month_name} {day}")
} else {
format!("{hour:02}:{minute:02}")
}
}
Interval::Seconds(_) | Interval::Weeks(_) => {
if month == 1 && day == 1 {
format!("{year}")
} else {
format!("{month_name} {day}")
}
}
Interval::Months(_) => {
if month == 1 {
format!("{year}")
} else {
month_name.to_string()
}
}
Interval::Years(_) => format!("{year}"),
};
Tick {
value: t as f64,
label,
}
})
.collect()
}
fn civil_from_days(days: i64) -> (i32, u32, u32) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let day_of_era = (z - era * 146_097) as u64;
let year_of_era =
(day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
let year = year_of_era as i64 + era * 400;
let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
let mp = (5 * day_of_year + 2) / 153;
let day = (day_of_year - (153 * mp + 2) / 5 + 1) as u32;
let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
((year + i64::from(month <= 2)) as i32, month, day)
}
fn days_from_civil(year: i32, month: u32, day: u32) -> i64 {
let year = i64::from(year) - i64::from(month <= 2);
let era = if year >= 0 { year } else { year - 399 } / 400;
let year_of_era = (year - era * 400) as u64;
let month = u64::from(month);
let day_of_year =
(153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + u64::from(day) - 1;
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
era * 146_097 + day_of_era as i64 - 719_468
}
#[cfg(test)]
#[path = "tests/time_tests.rs"]
mod tests;