use chrono::{DateTime, Duration, FixedOffset, Timelike, Utc};
const BACKDATE_DAYS: i64 = 1;
const SPACING_MINUTES: i64 = 1;
const MAX_OFFSET_MINUTES: i64 = BACKDATE_DAYS * 24 * 60 - SPACING_MINUTES;
pub fn next_version_time(
existing_entry_count: usize,
previous: Option<DateTime<FixedOffset>>,
) -> DateTime<FixedOffset> {
let now = truncate_to_second(Utc::now().fixed_offset());
let offset_minutes = i64::try_from(existing_entry_count)
.unwrap_or(i64::MAX)
.saturating_mul(SPACING_MINUTES)
.min(MAX_OFFSET_MINUTES);
let backdated = now - Duration::days(BACKDATE_DAYS) + Duration::minutes(offset_minutes);
match previous.map(truncate_to_second) {
Some(prev) if backdated <= prev => prev + Duration::seconds(1),
_ => backdated,
}
}
fn truncate_to_second(t: DateTime<FixedOffset>) -> DateTime<FixedOffset> {
t.with_nanosecond(0)
.expect("zero is always a valid nanosecond")
}
#[cfg(test)]
mod tests {
use super::{MAX_OFFSET_MINUTES, next_version_time};
use chrono::{Duration, Utc};
#[test]
fn is_past_and_strictly_increasing() {
let now = Utc::now();
let t0 = next_version_time(0, None);
let t1 = next_version_time(1, None);
let t2 = next_version_time(2, None);
assert!(
t0 < now.fixed_offset(),
"genesis timestamp must be in the past"
);
assert!(
t2 < now.fixed_offset(),
"later timestamps must still be in the past"
);
assert!(t0 < t1, "entry 1 must be strictly after entry 0");
assert!(t1 < t2, "entry 2 must be strictly after entry 1");
assert_ne!(
t0.timestamp(),
t1.timestamp(),
"entries must differ at second precision"
);
assert_ne!(t1.timestamp(), t2.timestamp());
}
#[test]
fn clamps_against_a_non_backdated_previous_entry() {
let genesis = Utc::now().fixed_offset() - Duration::seconds(30);
let next = next_version_time(1, Some(genesis));
assert!(
next > genesis,
"entry after a wall-clock genesis must be strictly later: \
next={next}, genesis={genesis}"
);
assert_ne!(
next.timestamp(),
genesis.timestamp(),
"must also differ after second-granularity truncation"
);
}
#[test]
fn stays_increasing_across_a_run_after_a_clamp() {
let mut prev = Utc::now().fixed_offset() - Duration::seconds(5);
for index in 1..=5 {
let next = next_version_time(index, Some(prev));
assert!(
next > prev,
"entry {index} must be strictly after its predecessor"
);
assert_ne!(next.timestamp(), prev.timestamp());
prev = next;
}
}
#[test]
fn saturates_instead_of_drifting_into_the_future() {
let long_chain = usize::try_from(MAX_OFFSET_MINUTES).unwrap() * 10;
for count in [
usize::try_from(MAX_OFFSET_MINUTES).unwrap(),
usize::try_from(MAX_OFFSET_MINUTES).unwrap() + 1,
long_chain,
usize::MAX,
] {
let t = next_version_time(count, None);
assert!(
t < Utc::now().fixed_offset(),
"entry {count} must still be in the past, got {t}"
);
}
}
#[test]
fn saturated_entries_are_still_strictly_increasing() {
let count = usize::try_from(MAX_OFFSET_MINUTES).unwrap() + 1;
let prev = next_version_time(count, None);
let next = next_version_time(count + 1, Some(prev));
assert!(next > prev, "saturated entries must still increase");
assert_ne!(next.timestamp(), prev.timestamp());
}
}