Skip to content

Fix calendar-aligned elapsed durations - #376

Open
francinelucca wants to merge 9 commits into
mainfrom
francinelucca-fix-relative-time-tests
Open

Fix calendar-aligned elapsed durations#376
francinelucca wants to merge 9 commits into
mainfrom
francinelucca-fix-relative-time-tests

Conversation

@francinelucca

@francinelucca francinelucca commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix elapsed durations that incorrectly gain extra days or prematurely cross into a year because months are currently approximated as fixed 30-day periods.

Fixes #262 and incorporates the regression cases proposed in #263.

Problem

elapsedTime derives larger units from elapsed milliseconds:

days -> 30-day months -> 12-month years

That approximation is useful for short durations, but it does not represent calendar boundaries. It causes results such as:

  • May 1 to July 1 → 2 months, 1 day
  • Jan 1 to Jan 1 one year later → 1 year, 5 days
  • Jan 1 to Dec 31 → 1 year, even though the anniversary has not occurred
  • Jan 31 to Feb 28 and Feb 29 to Feb 28 → incorrect rollover-based durations

Approach

The change keeps the existing fixed-duration path for shorter, non-aligned intervals, while applying a calendar correction when:

  • the fixed estimate reaches year scale, or
  • the target aligns with a calendar month/year boundary at the requested precision

The correction:

  1. Calculates a candidate number of calendar months from the dates' year and month fields.
  2. Applies those months to the reference date to create an anchor.
  3. Clamps the reference day to the target month's last valid day, so Jan 31 becomes Feb 28 and Feb 29 becomes Feb 28 in a non-leap year instead of rolling into March.
  4. Backs the candidate off by one month when it passes the target.
  5. Calculates days and smaller units from the remaining exact elapsed time between the anchor and target.

This produces same-sign Duration fields while respecting real calendar boundaries.

Precision behavior

Sub-day differences are discarded only when excluded by precision; duration formatting no longer silently rounds them away.

For example, from 2022-01-01T10:00Z to 2023-01-01T09:00Z:

  • default second precision → 11 months, 30 days, 23 hours
  • day precision → 1 year

format remains responsible for presentation, while precision controls the smallest represented unit.

Notable edge cases covered

  • exact whole months and years in both recent and long ranges
  • dates immediately before and after a calendar anniversary
  • prevention of a false year for Jan 1 to Dec 31
  • leap-day anniversary: Feb 29 to Feb 28
  • end-of-month clamping: Jan 31 to Feb 28
  • differing times on the same calendar anniversary
  • past and future durations

Testing

  • npm run build
  • full browser test suite: 648 passing tests

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6d8328fd-448c-41bc-8426-102150ede25f
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6d8328fd-448c-41bc-8426-102150ede25f

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates elapsed durations to use calendar-aligned month and year anniversaries.

Changes:

  • Adds calendar-based duration handling.
  • Corrects affected expectations and adds regression cases.
Show a summary per file
File Description
src/duration.ts Adds calendar-aligned elapsed-time logic.
test/duration.ts Updates month-duration expectations.
test/relative-time.js Adds month/year regression coverage.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Suppressed comments (1)

src/duration.ts:167

  • Skipping the time comparison for every span of at least 12 months silently drops lower units even at second or millisecond precision. The added 2022-01-01 10:00 to 2023-01-01 09:00 case is only 364 days and 23 hours, yet this returns exactly -P1Y. Year spans should also require the time to match at the requested precision, or retain the remainder relative to the calendar anchor.
  // Treat matching calendar days at least a year apart as anniversaries even
  // when their times differ, rather than leaking fixed-month remainder days.
  const isAnniversary = Math.abs(calendarMonths) >= 12
  if (!isAnniversary && !hasSameTimeAtPrecision(date, reference, precisionIndex)) return
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/duration.ts Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6d8328fd-448c-41bc-8426-102150ede25f
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6d8328fd-448c-41bc-8426-102150ede25f
@francinelucca
francinelucca marked this pull request as ready for review August 27, 2026 05:47
@francinelucca
francinelucca requested a review from a team as a code owner August 27, 2026 05:47
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6d8328fd-448c-41bc-8426-102150ede25f
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6d8328fd-448c-41bc-8426-102150ede25f

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/duration.ts
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6d8328fd-448c-41bc-8426-102150ede25f

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Comment thread src/duration.ts
Comment thread src/duration.ts Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 6d8328fd-448c-41bc-8426-102150ede25f

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Suppressed comments (2)

src/duration.ts:211

  • Negative end-of-month spans still miss the calendar correction. With now = 2023-02-28 and date = 2023-01-31, the -1-month anchor clamps to Jan 28, this branch backs it off to zero months, and elapsedTime falls through to -P28D; the reverse direction returns P1M. This leaves the advertised Jan 31 → Feb 28 behavior unfixed for past durations. Decompose from the chronologically earlier endpoint and apply the elapsed sign afterward (including the equivalent leap-day case).
  const candidateOvershot =
    calendarMonths !== 0 && !candidateAligned && (calendarMonths > 0 ? anchor > date : anchor < date)
  if (candidateOvershot) {
    wholeMonths += calendarMonths > 0 ? -1 : 1
    anchor = applyCalendarMonths(reference, wholeMonths)

src/duration.ts:224

  • The remainder is measured after applying months, but applyDuration applies days before months for negative durations. Consequently the newly expected -P11M25D from 2022-10-24 to 2021-10-30 reapplies to 2021-10-29, one day before its target. Derive negative fields in the same operation order as applyDuration, or adopt one consistent month-then-remainder order in both functions so generated durations represent the actual endpoint.
  const sign = Math.sign(date.getTime() - reference.getTime())
  const remainder = isCalendarAligned ? 0 : Math.abs(date.getTime() - anchor.getTime())
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/duration.ts
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread src/duration.ts
Comment on lines +224 to +225
const sign = Math.sign(date.getTime() - reference.getTime())
const remainder = isCalendarAligned ? 0 : Math.abs(date.getTime() - anchor.getTime())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stray days added to duration

2 participants