Skip to main content

chart/
time.rs

1//! Times as milliseconds since the Unix epoch, read on the UTC calendar.
2
3pub const SECOND: f64 = 1_000.0;
4pub const MINUTE: f64 = 60.0 * SECOND;
5pub const HOUR: f64 = 60.0 * MINUTE;
6pub const DAY: f64 = 24.0 * HOUR;
7
8const MONTHS: [&str; 12] = [
9    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
10];
11
12/// The finest calendar field a tick label shows.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Unit {
15    Second,
16    Minute,
17    Day,
18    Month,
19    Year,
20}
21
22/// Year, month 1–12 and day 1–31 of the day `days` after 1970-01-01.
23pub fn civil(days: i64) -> (i64, u32, u32) {
24    let z = days + 719_468;
25    let era = z.div_euclid(146_097);
26    let doe = z - era * 146_097;
27    let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
28    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
29    let mp = (5 * doy + 2) / 153;
30    let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
31    let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
32    (yoe + era * 400 + i64::from(month <= 2), month, day)
33}
34
35/// Days from 1970-01-01 to `year`-`month`-`day`.
36pub fn days(year: i64, month: u32, day: u32) -> i64 {
37    let year = year - i64::from(month <= 2);
38    let era = year.div_euclid(400);
39    let yoe = year - era * 400;
40    let mp = (i64::from(month) + 9) % 12;
41    let doy = (153 * mp + 2) / 5 + i64::from(day) - 1;
42    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
43    era * 146_097 + doe - 719_468
44}
45
46/// `YYYY`, `YYYY-MM`, `YYYY-MM-DD`, then optionally `T` or a space and
47/// `HH:MM`, `:SS` and `.fff`, and a trailing `Z`. Every time is UTC; an offset
48/// other than `Z` is `None`.
49pub fn parse(text: &str) -> Option<f64> {
50    let text = text.trim();
51    let text = text.strip_suffix('Z').unwrap_or(text);
52    let (date, clock) = match text.find(['T', ' ']) {
53        Some(at) => (&text[..at], Some(&text[at + 1..])),
54        None => (text, None),
55    };
56    let mut parts = date.split('-');
57    let year: i64 = parts.next()?.parse().ok()?;
58    let month: u32 = parts.next().map_or(Some(1), |p| p.parse().ok())?;
59    let day: u32 = parts.next().map_or(Some(1), |p| p.parse().ok())?;
60    if parts.next().is_some() || !(1..=12).contains(&month) || !(1..=31).contains(&day) {
61        return None;
62    }
63    let mut ms = days(year, month, day) as f64 * DAY;
64    if let Some(clock) = clock {
65        let mut parts = clock.split(':');
66        let hour: f64 = parts.next()?.parse().ok()?;
67        let minute: f64 = parts.next()?.parse().ok()?;
68        let second: f64 = parts.next().map_or(Some(0.0), |p| p.parse().ok())?;
69        if parts.next().is_some() {
70            return None;
71        }
72        ms += hour * HOUR + minute * MINUTE + second * SECOND;
73    }
74    Some(ms)
75}
76
77/// `ms` as a tick label showing fields down to `unit`.
78pub fn label(ms: f64, unit: Unit) -> String {
79    let (year, month, day, hour, minute, second) = fields(ms);
80    let name = MONTHS[month as usize - 1];
81    match unit {
82        Unit::Year => format!("{year}"),
83        Unit::Month if month == 1 => format!("{year}"),
84        Unit::Month => name.to_string(),
85        Unit::Day => format!("{name} {day}"),
86        Unit::Minute => format!("{hour:02}:{minute:02}"),
87        Unit::Second => format!("{hour:02}:{minute:02}:{second:02}"),
88    }
89}
90
91/// `ms` in full: the date, then the time of day unless it is midnight.
92pub fn describe(ms: f64) -> String {
93    let (year, month, day, hour, minute, second) = fields(ms);
94    match (hour, minute, second) {
95        (0, 0, 0) => format!("{year}-{month:02}-{day:02}"),
96        (_, _, 0) => format!("{year}-{month:02}-{day:02} {hour:02}:{minute:02}"),
97        _ => format!("{year}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"),
98    }
99}
100
101fn fields(ms: f64) -> (i64, u32, u32, u32, u32, u32) {
102    let day = (ms / DAY).floor();
103    let (year, month, date) = civil(day as i64);
104    let rest = ((ms - day * DAY) / SECOND).floor() as u32;
105    (year, month, date, rest / 3_600, rest / 60 % 60, rest % 60)
106}
107
108/// Ticks on calendar boundaries inside `lo..=hi`, about `count` of them, and
109/// the unit to label them with. Weeks start on Monday.
110pub fn ticks(lo: f64, hi: f64, count: usize) -> (Vec<f64>, Unit) {
111    const FIXED: [(f64, Unit); 15] = [
112        (SECOND, Unit::Second),
113        (5.0 * SECOND, Unit::Second),
114        (15.0 * SECOND, Unit::Second),
115        (30.0 * SECOND, Unit::Second),
116        (MINUTE, Unit::Minute),
117        (5.0 * MINUTE, Unit::Minute),
118        (15.0 * MINUTE, Unit::Minute),
119        (30.0 * MINUTE, Unit::Minute),
120        (HOUR, Unit::Minute),
121        (3.0 * HOUR, Unit::Minute),
122        (6.0 * HOUR, Unit::Minute),
123        (12.0 * HOUR, Unit::Minute),
124        (DAY, Unit::Day),
125        (2.0 * DAY, Unit::Day),
126        (7.0 * DAY, Unit::Day),
127    ];
128    const MONTH: f64 = 30.44 * DAY;
129    let target = (hi - lo) / count.max(1) as f64;
130    let mut ticks = Vec::new();
131
132    if let Some(&(step, unit)) = FIXED.iter().find(|(step, _)| *step >= target) {
133        // 1970-01-05 was a Monday.
134        let offset = if step == 7.0 * DAY { 4.0 * DAY } else { 0.0 };
135        let mut at = ((lo - offset) / step).ceil() * step + offset;
136        while at <= hi {
137            ticks.push(at);
138            at += step;
139        }
140        return (ticks, unit);
141    }
142
143    let (year, month, _) = civil((lo / DAY).floor() as i64);
144    if let Some(step) = [1, 3, 6]
145        .into_iter()
146        .find(|&k: &i64| k as f64 * MONTH >= target)
147    {
148        let mut index = year * 12 + i64::from(month) - 1;
149        if month_start(index) < lo {
150            index += 1;
151        }
152        index = (index + step - 1).div_euclid(step) * step;
153        while month_start(index) <= hi {
154            ticks.push(month_start(index));
155            index += step;
156        }
157        return (ticks, Unit::Month);
158    }
159
160    let years = target / (365.25 * DAY);
161    let mut step = 10f64.powf(years.log10().floor()).max(1.0);
162    for factor in [1.0, 2.0, 5.0, 10.0] {
163        if step * factor >= years {
164            step *= factor;
165            break;
166        }
167    }
168    let step = step as i64;
169    let mut year = if (days(year, 1, 1) as f64 * DAY) < lo {
170        year + 1
171    } else {
172        year
173    };
174    year = (year + step - 1).div_euclid(step) * step;
175    while (days(year, 1, 1) as f64 * DAY) <= hi {
176        ticks.push(days(year, 1, 1) as f64 * DAY);
177        year += step;
178    }
179    (ticks, Unit::Year)
180}
181
182fn month_start(index: i64) -> f64 {
183    days(index.div_euclid(12), index.rem_euclid(12) as u32 + 1, 1) as f64 * DAY
184}