diff --git a/src/duration.ts b/src/duration.ts index 0195fc2..8e982e9 100644 --- a/src/duration.ts +++ b/src/duration.ts @@ -149,17 +149,27 @@ export function elapsedTime(date: Date, precision: Unit = 'second', now = Date.n const delta = date.getTime() - now if (delta === 0) return new Duration() const sign = Math.sign(delta) - const ms = Math.abs(delta) + // Months below are approximated as 30 days, so a year is 360 days long. Over many years that drift adds up to + // whole extra years, so count the whole calendar years first and only approximate what is left over. + const earlier = new Date(sign < 0 ? date : now) + const later = new Date(sign < 0 ? now : date) + let calendarYears = later.getUTCFullYear() - earlier.getUTCFullYear() + while (calendarYears > 0 && applyDuration(earlier, new Duration(calendarYears)) > later) calendarYears -= 1 + const ms = + calendarYears > 0 + ? later.getTime() - applyDuration(earlier, new Duration(calendarYears)).getTime() + : Math.abs(delta) const sec = Math.floor(ms / 1000) const min = Math.floor(sec / 60) const hr = Math.floor(min / 60) const day = Math.floor(hr / 24) const month = Math.floor(day / 30) - const year = Math.floor(month / 12) + const extraYears = Math.floor(month / 12) + const year = Math.max(calendarYears, 0) + extraYears const i = unitNames.indexOf(precision) return new Duration( i >= 0 ? year * sign : 0, - i >= 1 ? (month - year * 12) * sign : 0, + i >= 1 ? (month - extraYears * 12) * sign : 0, 0, i >= 3 ? (day - month * 30) * sign : 0, i >= 4 ? (hr - day * 24) * sign : 0, diff --git a/test/duration.ts b/test/duration.ts index 87707de..dd73def 100644 --- a/test/duration.ts +++ b/test/duration.ts @@ -225,6 +225,21 @@ suite('duration', function () { input: '2023-03-21T16:03:00.000Z', expected: '-P1DT20H', }, + { + now: '2025-02-06T12:00:00.000Z', + input: '2015-02-14T05:13:48.000Z', + expected: '-P9Y11M28DT6H46M12S', + }, + { + now: '2025-01-01T00:00:00.000Z', + input: '2020-01-01T00:00:00.000Z', + expected: '-P5Y', + }, + { + now: '2025-02-06T00:00:00.000Z', + input: '2035-02-14T00:00:00.000Z', + expected: 'P10Y8D', + }, { now: '2022-10-24T14:46:00.000Z', input: '2024-10-24T14:46:00.000Z', diff --git a/test/relative-time.js b/test/relative-time.js index 6edb88e..4e422e5 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -774,7 +774,7 @@ suite('relative-time', function () { time.setAttribute('tense', 'past') time.setAttribute('datetime', '2023-01-01T00:00:00Z') await Promise.resolve() - assert.equal(time.shadowRoot.textContent, '11 years ago') + assert.equal(time.shadowRoot.textContent, '10 years ago') }) test('rewrites from now past datetime to minutes ago', async () => { @@ -842,9 +842,25 @@ suite('relative-time', function () { assert.equal(time.shadowRoot.textContent, '3 months ago') }) + test('does not count an extra year for dates many years in the past', async () => { + freezeTime(new Date('2025-02-06T12:00:00Z')) + const time = document.createElement('relative-time') + time.setAttribute('tense', 'past') + time.setAttribute('datetime', '2015-02-14T15:13:48+05:00') + await Promise.resolve() + assert.equal(time.shadowRoot.textContent, '10 years ago') + }) + + test('does not count an extra year for exact multiples of five years', async () => { + freezeTime(new Date('2025-01-01T00:00:00Z')) + const time = document.createElement('relative-time') + time.setAttribute('tense', 'past') + time.setAttribute('datetime', '2020-01-01T00:00:00Z') + await Promise.resolve() + assert.equal(time.shadowRoot.textContent, '5 years ago') + }) + test('micro formats years', async () => { - // FIXME: there is still a bug, if the duration is long enough (say, 10 or 100 years) - // then the `month = Math.floor(day / 30)` in elapsedTime causes errors, then "10 years" would output "11y" const now = new Date(Date.now() - 2 * 365 * 24 * 60 * 60 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('tense', 'past') @@ -946,8 +962,6 @@ suite('relative-time', function () { }) test('micro formats years', async () => { - // FIXME: there is still a bug, if the duration is long enough (say, 10 or 100 years) - // then the `month = Math.floor(day / 30)` in elapsedTime causes errors, then "10 years" would output "11y" const now = new Date(Date.now() + 2 * 365 * 24 * 60 * 60 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('tense', 'future') @@ -1684,7 +1698,7 @@ suite('relative-time', function () { { datetime: '2024-10-24T14:46:00.000Z', format: 'duration', - expected: '2 years, 11 days', + expected: '2 years', }, { datetime: '2024-10-24T14:46:00.000Z', @@ -1696,19 +1710,19 @@ suite('relative-time', function () { datetime: '2024-10-24T14:46:00.000Z', format: 'duration', precision: 'minute', - expected: '2 years, 11 days', + expected: '2 years', }, { datetime: '2024-10-24T14:46:00.000Z', format: 'duration', precision: 'day', - expected: '2 years, 11 days', + expected: '2 years', }, { datetime: '2024-10-24T14:46:00.000Z', format: 'duration', tense: 'future', - expected: '2 years, 11 days', + expected: '2 years', }, { datetime: '2024-10-24T14:46:00.000Z', @@ -2140,19 +2154,19 @@ suite('relative-time', function () { { datetime: '2020-10-24T14:46:00.000Z', format: 'duration', - expected: '2 years, 10 days', + expected: '2 years', }, { datetime: '2020-10-24T14:46:00.000Z', format: 'duration', precision: 'minute', - expected: '2 years, 10 days', + expected: '2 years', }, { datetime: '2020-10-24T14:46:00.000Z', format: 'duration', precision: 'day', - expected: '2 years, 10 days', + expected: '2 years', }, { datetime: '2020-10-24T14:46:00.000Z', @@ -2164,7 +2178,7 @@ suite('relative-time', function () { datetime: '2020-10-24T14:46:00.000Z', format: 'duration', tense: 'past', - expected: '2 years, 10 days', + expected: '2 years', }, { reference: '2023-03-23T12:03:00.000Z',