GitoxideLabs/gitoxide · error

Day lies out of range

Error message

Day {} lies out of range

What it means

`Fields::normalize()` converts the day offset into a 24h-second `SignedDuration` and adds it to the first-of-month date via `checked_add`. When the addition overflows jiff's `civil::Date` range (the day field is so large/small that the resulting date leaves the supported range), `or_raise` attaches Error 'Day {day} lies out of range'.

Solutions

  1. Bound the day/period magnitude before parsing so the resulting date stays within jiff's supported range
  2. Catch the Error and surface a clearer 'date out of supported range' message to the user
  3. Use absolute dates for very large offsets

Example fix

// before
parse("now", "1000000 days ago")
// after
let days = count.min(365_000); // keep within representable range
parse("now", &format!("{days} days ago"))
Defensive patterns

Strategy: validation

Validate before calling

fn day_offset_safe(days: i64) -> bool {
    days.abs() < 365 * 9000 // stays within jiff's civil-date range
}

Try / catch

match Fields::normalize(&fields) {
    Ok(z) => z,
    Err(e) => fallback_to_now_or_error(e),
}

Prevention

When it happens

Trigger: Relative-date parsing where a computed `day` value (from adding/subtracting large counts of months/weeks/days in `subtract_pairs`) pushes the date past the representable civil-date range, e.g. '100000 months ago'.

Common situations: Extreme relative date expressions in CLI tools or config like `--since='1000000 days ago'` implemented with gix-date.

Understand the failure class

Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/1fb28cc91e0ad7ac. Report an issue: GitHub.

Appendix: source

Thrown at gix-date/src/parse/relative.rs:174

                year: zdt.year(),
                month: zdt.month(),
                day: zdt.day(),
                time: zdt.time(),
                timezone: zdt.time_zone().clone(),
            }
        }
    }

    impl Fields {
        /// Turn the fields back into a point in time: a day beyond the end of the month rolls over into the
        /// following month. One month before May 31st is thus May 1st, a day after April 30th.
        fn normalize(&self) -> Result<Zoned, Exn<Error>> {
            let first_of_month = civil::Date::new(self.year, self.month, 1)
                .or_raise(|| Error::new(format!("Date lies out of range: {}-{:02}", self.year, self.month)))?;
            let days_beyond_first = SignedDuration::from_secs((i64::from(self.day) - 1) * 24 * 60 * 60);
            first_of_month
                .checked_add(days_beyond_first)
                .or_raise(|| Error::new(format!("Day {} lies out of range", self.day)))?
                .to_datetime(self.time)
                .to_zoned(self.timezone.clone())
                .or_raise(|| Error::new("Could not convert date to a point in time"))
        }
    }

    let now = now.ok_or(ValidationError::new("Missing current time"))?;
    let mut fields = Fields::from(now);
    for Pair { period, count, unit } in pairs {
        let err = || Error::new(format!("Couldn't parse span from '{period} {count}'"));
        match unit {
            Unit::Seconds(factor) => {
                let seconds = count
                    .checked_mul(*factor)
                    .map(SignedDuration::from_secs)
                    .ok_or_else(err)?;
                let ts = fields.normalize()?.timestamp().checked_sub(seconds).or_raise(err)?;
                fields = ts.to_zoned(fields.timezone.clone()).into();

View on GitHub (pinned to e73179060b)