Skip to content

feat: implement Add and Sub operator overloading for ergonomic date arithmetic #55

Description

@ZialeHub

Summary

The current arithmetic API requires verbose method calls:

let tomorrow = date.update(DateUnit::Day, 1)?;
let last_week = datetime.update(DateTimeUnit::Day, -7)?;

Rust's std::ops::Add and std::ops::Sub traits enable a far more natural syntax that matches how developers think about time arithmetic, and is consistent with how chrono itself works:

let tomorrow   = (date + Days(1))?;
let last_week  = (datetime - Days(7))?;
let next_month = (date + Months(1))?;

Proposed Design

Unit value wrapper types

pub struct Years(pub i32);
pub struct Months(pub i32);
pub struct Weeks(pub i32);
pub struct Days(pub i32);
pub struct Hours(pub i32);
pub struct Minutes(pub i32);
pub struct Seconds(pub i32);

These are lightweight newtypes in a timeflow::units module, re-exported from prelude.

Trait implementations

// Add
impl Add<Days> for Date {
    type Output = Result<Date, SpanError>;
    fn add(self, rhs: Days) -> Self::Output { self.update(DateUnit::Day, rhs.0) }
}

impl Add<Days> for DateTime {
    type Output = Result<DateTime, SpanError>;
    fn add(self, rhs: Days) -> Self::Output { self.update(DateTimeUnit::Day, rhs.0) }
}

// Sub delegates to Add with negated value
impl Sub<Days> for Date {
    type Output = Result<Date, SpanError>;
    fn sub(self, rhs: Days) -> Self::Output { self.update(DateUnit::Day, -rhs.0) }
}

Full matrix: {Date, DateTime} × {Years, Months, Weeks, Days} and {DateTime, Time} × {Hours, Minutes, Seconds}.

Usage

use timeflow::prelude::*;
use timeflow::units::{Days, Months, Hours};

let date = Date::new(2024, 1, 31)?;
let next_month = (date + Months(1))?;   // 2024-02-29 (clamped, leap year)
let two_weeks  = (date + Weeks(2))?;    // 2024-02-14

let dt = DateTime::new(2024, 3, 1)?.with_time(23, 0, 0)?;
let prev = (dt - Hours(2))?;             // 2024-03-01 21:00:00

Alternative: chrono::Duration overload

Additionally implement Add<chrono::Duration> / Sub<chrono::Duration> for interop with existing chrono-using code:

let dt = DateTime::new(2024, 1, 1)?.with_time(0, 0, 0)?;
let later = (dt + chrono::Duration::hours(3))?;

Implementation Notes

  • Output = Result<T, SpanError> is necessary because some additions can fail (e.g. arithmetic overflow, invalid resulting date). This is an unusual but correct use of Add.
  • Alternatively, Output = T with a panic on error could be offered as an unchecked variant, but the Result-returning form should be the default.
  • Weeks and Years should delegate to their Day and Month equivalents respectively (× 7 and × 12), consistent with how update works.
  • All unit wrappers should be Copy + Clone + Debug.
  • Feature-gate: unit wrappers only available when the corresponding feature (date, time, datetime) is active.

Acceptance Criteria

  • timeflow::units module with Years, Months, Weeks, Days, Hours, Minutes, Seconds newtypes
  • All newtypes re-exported from timeflow::prelude
  • Add and Sub implemented for all valid (Type, Unit) combinations
  • Add<chrono::Duration> / Sub<chrono::Duration> implemented for all three types
  • Output = Result<T, SpanError> for all implementations
  • Unit tests for: normal addition, subtraction, overflow/error path, month-end clamping
  • Doc examples in timeflow::units module

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-domainbusiness logic / coreB-ideaDiscussion; or implementation attempt, to be reviewed before further work

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions