use chrono::{DateTime, TimeDelta, Utc};
use crate::{Error, Resolution, Result};
const NANOS_PER_SECOND: i128 = 1_000_000_000;
pub fn posix_nanos(t: DateTime<Utc>) -> i128 {
i128::from(t.timestamp()) * NANOS_PER_SECOND + i128::from(t.timestamp_subsec_nanos())
}
fn from_posix_nanos(nanos: i128) -> Option<DateTime<Utc>> {
let secs = i64::try_from(nanos.div_euclid(NANOS_PER_SECOND)).ok()?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let subsec = nanos.rem_euclid(NANOS_PER_SECOND) as u32;
DateTime::from_timestamp(secs, subsec)
}
#[inline]
pub fn nanos_to_f64(nanos: i128) -> f64 {
#[allow(clippy::cast_precision_loss)]
i64::try_from(nanos).map_or(nanos as f64, |n| n as f64)
}
#[derive(Debug, Clone, Copy)]
pub struct Grid {
pub start: DateTime<Utc>,
pub start_nanos: i128,
pub step: TimeDelta,
pub step_nanos: i64,
pub len: usize,
}
impl Grid {
pub fn new(start: DateTime<Utc>, end: DateTime<Utc>, resolution: Resolution) -> Result<Self> {
let last = posix_nanos(DateTime::<Utc>::MAX_UTC);
let (start_nanos, end_nanos) = (posix_nanos(start), posix_nanos(end).min(last));
if start_nanos > end_nanos {
return Err(Error::InvalidTimeRange { start, end });
}
let step_nanos = resolution.step_nanos();
let points = u128::try_from((end_nanos - start_nanos) / i128::from(step_nanos)).unwrap_or(0) + 1;
let len = usize::try_from(points).ok().filter(|&n| isize::try_from(n).is_ok()).ok_or(Error::OutputTooLarge { points })?;
let start = from_posix_nanos(start_nanos).unwrap_or(start);
Ok(Self { start, start_nanos, step: resolution.step(), step_nanos, len })
}
pub fn at(&self, k: usize) -> DateTime<Utc> {
from_posix_nanos(self.start_nanos + i128::from(self.step_nanos) * k as i128).unwrap_or(self.start)
}
pub fn timestamps(&self, first: usize, len: usize) -> impl Iterator<Item = DateTime<Utc>> + '_ {
std::iter::successors(Some(self.at(first)), move |t| t.checked_add_signed(self.step)).take(len)
}
pub fn offset_nanos(&self, origin: i128, k: usize) -> i128 {
self.start_nanos - origin + i128::from(self.step_nanos) * k as i128
}
pub fn offsets(&self, origin: i128, first: usize) -> impl Iterator<Item = i128> {
let step = i128::from(self.step_nanos);
std::iter::successors(Some(self.offset_nanos(origin, first)), move |o| Some(o + step))
}
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
fn at(secs: i64) -> DateTime<Utc> {
Utc.timestamp_opt(secs, 0).single().expect("valid")
}
#[test]
fn grid_is_inclusive_and_anchored_at_start() {
let grid = Grid::new(at(1), at(11), Resolution::Seconds).expect("grid");
assert_eq!(grid.len, 11);
let ts: Vec<_> = grid.timestamps(0, grid.len).collect();
assert_eq!(ts.first(), Some(&at(1)));
assert_eq!(ts.last(), Some(&at(11)));
let grid = Grid::new(at(0), at(10), Resolution::Minutes).expect("grid");
assert_eq!(grid.len, 1);
}
#[test]
fn single_instant_grid() {
assert_eq!(Grid::new(at(5), at(5), Resolution::Days).expect("grid").len, 1);
}
#[test]
fn reversed_range_is_an_error() {
assert!(matches!(Grid::new(at(5), at(4), Resolution::Seconds), Err(Error::InvalidTimeRange { .. })));
}
#[test]
fn huge_grids_are_refused_not_allocated() {
let err = Grid::new(DateTime::<Utc>::MIN_UTC, DateTime::<Utc>::MAX_UTC, Resolution::Nanoseconds).expect_err("too large");
assert!(matches!(err, Error::OutputTooLarge { .. }));
}
#[test]
fn posix_nanos_spans_the_whole_chrono_range() {
let span = posix_nanos(DateTime::<Utc>::MAX_UTC) - posix_nanos(DateTime::<Utc>::MIN_UTC);
assert!(span > i128::from(i64::MAX), "beyond what i64 nanoseconds can hold");
assert_eq!(posix_nanos(at(1)) - posix_nanos(at(0)), 1_000_000_000);
for t in [DateTime::<Utc>::MIN_UTC, DateTime::<Utc>::MAX_UTC, at(-1), at(1_700_000_000)] {
assert_eq!(from_posix_nanos(posix_nanos(t)), Some(t));
}
}
#[test]
fn a_leap_second_on_the_last_day_is_capped() {
let leap = chrono::NaiveDate::MAX.and_hms_nano_opt(23, 59, 59, 1_500_000_000).expect("chrono represents it").and_utc();
let max = DateTime::<Utc>::MAX_UTC;
let grid = Grid::new(max - TimeDelta::seconds(2), leap, Resolution::Seconds).expect("grid");
let ts: Vec<_> = grid.timestamps(0, grid.len).collect();
assert!(ts.windows(2).all(|w| w[0] < w[1]), "{ts:?}");
assert!(ts.iter().all(|&t| t <= max));
assert!(matches!(Grid::new(leap, leap, Resolution::Seconds), Err(Error::InvalidTimeRange { .. })));
}
#[test]
fn leap_seconds_fold_onto_the_next_second() {
let leap = DateTime::parse_from_rfc3339("2016-12-31T23:59:60.25Z").expect("chrono accepts leap seconds").to_utc();
let next = DateTime::parse_from_rfc3339("2017-01-01T00:00:00.25Z").expect("valid").to_utc();
assert_eq!(posix_nanos(leap), posix_nanos(next));
let grid = Grid::new(leap, next + TimeDelta::seconds(2), Resolution::Seconds).expect("grid");
assert_eq!(grid.start, next);
assert_eq!(grid.len, 3);
let ts: Vec<_> = grid.timestamps(0, grid.len).collect();
for (k, (t, o)) in ts.iter().zip(grid.offsets(posix_nanos(next), 0)).enumerate() {
assert_eq!(posix_nanos(*t) - posix_nanos(next), o, "point {k}");
assert_eq!(*t, grid.at(k));
}
let before = DateTime::parse_from_rfc3339("2016-12-31T23:59:59Z").expect("valid").to_utc();
let grid = Grid::new(before, leap, Resolution::Seconds).expect("grid");
assert!(grid.timestamps(0, grid.len).all(|t| posix_nanos(t) <= posix_nanos(leap)));
}
}