Bug Report
Description
DateTime::unit_elapsed applies .abs() to its return value, always producing a
non-negative result. The other two Span implementations — Date::unit_elapsed and
Time::unit_elapsed — return a signed value, where negative means self is earlier
than rhs.
This inconsistency means code that relies on sign to determine temporal ordering will work
correctly with Date and Time but silently fail with DateTime, which is the type most
commonly used for time-bounded checks (token expiry, scheduling, access control).
Affected Code
timeflow/src/datetime.rs (~line 346):
fn unit_elapsed(&self, rhs: &Self, unit: DateTimeUnit) -> Result<i64, SpanError> {
Ok(match unit {
...
}.abs()) // ← strips sign; Date and Time do not do this
}
Reproduction
use timeflow::prelude::*;
let past = DateTime::new(2020, 1, 1)?.with_time(0, 0, 0)?;
let future = DateTime::new(2030, 1, 1)?.with_time(0, 0, 0)?;
// With Date (correct signed behaviour):
let d_past = Date::new(2020, 1, 1)?;
let d_future = Date::new(2030, 1, 1)?;
assert!(d_past.unit_elapsed(&d_future, DateUnit::Day)? < 0); // past is before future ✓
// With DateTime (sign stripped):
assert!(past.unit_elapsed(&future, DateTimeUnit::Second)? < 0); // FAILS — returns positive
A scheduler or expiry check using:
if now.unit_elapsed(&expiry, DateTimeUnit::Second)? > 0 { /* expired */ }
will evaluate as expired even when the expiry is in the future.
Fix
Remove the .abs() call:
fn unit_elapsed(&self, rhs: &Self, unit: DateTimeUnit) -> Result<i64, SpanError> {
Ok(match unit {
DateTimeUnit::Year => (self.datetime.year() - rhs.datetime.year()) as i64,
DateTimeUnit::Month => { ... }
DateTimeUnit::Second => self.datetime.and_utc().timestamp() - rhs.datetime.and_utc().timestamp(),
...
}) // no .abs()
}
Add a doc comment on the Span::unit_elapsed trait method clarifying sign convention
(positive = self is after rhs, negative = self is before rhs) so all three
implementations are consistent and the behaviour is explicit.
Severity
Medium — silent sign loss is invisible at the call site and most dangerous in any
time-ordering or expiry logic built on top of DateTime, which is the primary type
intended for such use cases.
Bug Report
Description
DateTime::unit_elapsedapplies.abs()to its return value, always producing anon-negative result. The other two
Spanimplementations —Date::unit_elapsedandTime::unit_elapsed— return a signed value, where negative meansselfis earlierthan
rhs.This inconsistency means code that relies on sign to determine temporal ordering will work
correctly with
DateandTimebut silently fail withDateTime, which is the type mostcommonly used for time-bounded checks (token expiry, scheduling, access control).
Affected Code
timeflow/src/datetime.rs(~line 346):Reproduction
A scheduler or expiry check using:
if now.unit_elapsed(&expiry, DateTimeUnit::Second)? > 0 { /* expired */ }will evaluate as expired even when the expiry is in the future.
Fix
Remove the .abs() call:
Add a doc comment on the Span::unit_elapsed trait method clarifying sign convention
(positive = self is after rhs, negative = self is before rhs) so all three
implementations are consistent and the behaviour is explicit.
Severity
Medium — silent sign loss is invisible at the call site and most dangerous in any
time-ordering or expiry logic built on top of DateTime, which is the primary type
intended for such use cases.