Skip to content

chrono_now() in extract/build_helpers/timestamp.rs produces wrong dates #162

Description

@metavacua

Summary

chrono_now() in crates/larql-vindex/src/extract/build_helpers/timestamp.rs uses a hand-rolled day-to-calendar conversion that is systematically wrong, causing extracted_at in index.json to drift from the actual extraction date.

Root cause

let days = secs / 86400;
let years_approx = 1970 + days / 365;   // ignores leap years
let remainder_days = days % 365;
let months = remainder_days / 30 + 1;   // all months treated as 30 days
let day = remainder_days % 30 + 1;

Two compounding errors:

  1. Leap years ignoreddays / 365 accumulates ~1 day of error per 4 years (14 extra days by 2026).
  2. Month lengths wrongremainder_days / 30 treats every month as 30 days. Months are 28–31 days, so the month and day within month are both wrong.

Observed impact

A vindex extracted on 2026-06-16 (confirmed by filesystem mtime) has "extracted_at": "2026-07-01T14:24:16Z" — 15 days in the future. The error grows over time.

Existing tests do not catch this

The tests in timestamp.rs only verify format shape (length, separators, year ≥ 2020 and month/day in range). They do not verify that the emitted date matches the actual wall-clock date.

Suggested fix

Replace the approximation with Howard Hinnant's civil calendar algorithm (O(1), no extra dependencies, exact for all Gregorian dates):

pub(crate) fn chrono_now() -> String {
    let d = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    let secs = d.as_secs();
    let (year, month, day) = days_since_epoch_to_date(secs / 86400);
    let hour = (secs % 86400) / 3600;
    let min  = (secs % 3600)  / 60;
    let sec  =  secs % 60;
    format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", year, month, day, hour, min, sec)
}

fn days_since_epoch_to_date(days: u64) -> (u64, u64, u64) {
    // Hinnant civil calendar: https://howardhinnant.github.io/date_algorithms.html
    let z = days as i64 + 719468;
    let era = if z >= 0 { z } else { z - 146096 } / 146097;
    let doe = (z - era * 146097) as u64;
    let yoe = (doe - doe/1460 + doe/36524 - doe.min(146096)/146096) / 365;
    let y   = yoe as i64 + era * 400;
    let doy = doe - (365*yoe + yoe/4 - yoe/100);
    let mp  = (5*doy + 2) / 153;
    let d   = doy - (153*mp + 2)/5 + 1;
    let m   = if mp < 10 { mp + 3 } else { mp - 9 };
    let y   = if m <= 2 { y + 1 } else { y };
    (y as u64, m, d)
}

The existing tests should be extended to verify the emitted date matches SystemTime::now() (or at least that the year/month match within a small tolerance).

Files

  • crates/larql-vindex/src/extract/build_helpers/timestamp.rs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions