use crate::errors::CronError;
use crate::time::{CronDateTime, Resolution, SECONDS_PER_DAY};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Fold {
Earlier,
Later,
}
const MAX_OVERLAP_SECONDS: i64 = SECONDS_PER_DAY;
pub(crate) fn fold_of<T: CronDateTime>(instant: &T) -> Result<Option<Fold>, CronError> {
match instant.resolve_civil(instant.to_civil())? {
Resolution::Ambiguous(_, later) => Ok(Some(if instant.cmp_instant(&later).is_lt() {
Fold::Earlier
} else {
Fold::Later
})),
_ => Ok(None),
}
}
pub(crate) fn other_fold_edge<T: CronDateTime>(origin: &T, on: Fold) -> Result<T, CronError> {
let step = match on {
Fold::Earlier => 1,
Fold::Later => -1,
};
let at_offset = |offset: i64| -> Result<T, CronError> {
origin
.checked_add_seconds(offset * step)
.ok_or(CronError::InvalidTime)
};
let is_inside =
|offset: i64| -> Result<bool, CronError> { Ok(fold_of(&at_offset(offset)?)? == Some(on)) };
if is_inside(MAX_OVERLAP_SECONDS)? {
return Err(CronError::TimeSearchLimitExceeded);
}
let mut inside_offset = 0;
let mut outside_offset = MAX_OVERLAP_SECONDS;
while outside_offset - inside_offset > 1 {
let middle = inside_offset + (outside_offset - inside_offset) / 2;
if is_inside(middle)? {
inside_offset = middle;
} else {
outside_offset = middle;
}
}
at_offset(outside_offset)
}
#[cfg(all(test, feature = "chrono"))]
mod tests {
use chrono::{DateTime, TimeDelta, TimeZone};
use chrono_tz::Europe::Paris;
use chrono_tz::Tz;
use super::*;
fn paris(hour: u32, minute: u32, second: u32, fold: Fold) -> DateTime<Tz> {
let ambiguous = Paris.with_ymd_and_hms(2024, 10, 27, hour, minute, second);
match fold {
Fold::Earlier => ambiguous.earliest(),
Fold::Later => ambiguous.latest(),
}
.expect("the test time must exist")
}
#[test]
fn reads_the_half_that_an_instant_is_on() {
assert_eq!(
fold_of(&paris(2, 30, 0, Fold::Earlier)).unwrap(),
Some(Fold::Earlier)
);
assert_eq!(
fold_of(&paris(2, 30, 0, Fold::Later)).unwrap(),
Some(Fold::Later)
);
assert_eq!(fold_of(&paris(1, 30, 0, Fold::Earlier)).unwrap(), None);
assert_eq!(fold_of(&paris(3, 30, 0, Fold::Earlier)).unwrap(), None);
}
#[test]
fn reads_the_half_of_an_instant_with_sub_second_precision() {
for fold in [Fold::Earlier, Fold::Later] {
let instant = paris(2, 30, 0, fold) + TimeDelta::milliseconds(500);
assert_eq!(fold_of(&instant).unwrap(), Some(fold));
}
}
#[test]
fn finds_the_edge_from_anywhere_in_the_range() {
let change = paris(2, 0, 0, Fold::Later);
let before_change = change.checked_add_seconds(-1).unwrap();
for (hour, minute, second) in [(2, 0, 0), (2, 30, 0), (2, 59, 59)] {
let earlier = paris(hour, minute, second, Fold::Earlier);
assert_eq!(other_fold_edge(&earlier, Fold::Earlier).unwrap(), change);
let later = paris(hour, minute, second, Fold::Later);
assert_eq!(other_fold_edge(&later, Fold::Later).unwrap(), before_change);
}
}
}