Skip to content

Bug: DateTime::unit_elapsed unconditionally strips the sign via .abs(), inconsistent with Date and Time and breaks past/future detection #49

Description

@ZialeHub

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions