diff --git a/src/duration.ts b/src/duration.ts index 58492b2..82b531d 100644 --- a/src/duration.ts +++ b/src/duration.ts @@ -104,27 +104,80 @@ export function applyDuration(date: Date | number, duration: Duration): Date { return r } -export function elapsedTime(date: Date, precision: Unit = 'second', now = Date.now()): Duration { - const delta = date.getTime() - now - if (delta === 0) return new Duration() - const sign = Math.sign(delta) - const ms = 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 i = unitNames.indexOf(precision) || unitNames.length +// Calculates the elapsed time from `now` to `date`. +export function elapsedTime( + date: Date, + precision: Unit = 'second', + nowTimestamp: Date | number = Date.now(), +): Duration { + const now = new Date(nowTimestamp) + // eslint-disable-next-line prefer-const + let [sign, subtrahend, minuend] = date < now ? [-1, now, date] : [1, date, now] + let ms: number + let sec: number + let min: number + let hr: number + let day: number + let month: number + let year: number + // Using UTC to avoid timezone-specific variations. + if (subtrahend.getUTCMilliseconds() >= minuend.getUTCMilliseconds()) { + ms = subtrahend.getUTCMilliseconds() - minuend.getUTCMilliseconds() + } else { + ms = 1000 + subtrahend.getUTCMilliseconds() - minuend.getUTCMilliseconds() + subtrahend = new Date(subtrahend.getTime() - 1000) + } + + if (subtrahend.getUTCSeconds() >= minuend.getUTCSeconds()) { + sec = subtrahend.getUTCSeconds() - minuend.getUTCSeconds() + } else { + sec = 60 + subtrahend.getUTCSeconds() - minuend.getUTCSeconds() + subtrahend = new Date(subtrahend.getTime() - 1000 * 60) + } + + if (subtrahend.getUTCMinutes() >= minuend.getUTCMinutes()) { + min = subtrahend.getUTCMinutes() - minuend.getUTCMinutes() + } else { + min = 60 + subtrahend.getUTCMinutes() - minuend.getUTCMinutes() + subtrahend = new Date(subtrahend.getTime() - 1000 * 60 * 60) + } + + if (subtrahend.getUTCHours() >= minuend.getUTCHours()) { + hr = subtrahend.getUTCHours() - minuend.getUTCHours() + } else { + hr = 24 + subtrahend.getUTCHours() - minuend.getUTCHours() + subtrahend = new Date(subtrahend.getTime() - 1000 * 60 * 60 * 24) + } + + if (subtrahend.getUTCDate() >= minuend.getUTCDate()) { + day = subtrahend.getUTCDate() - minuend.getUTCDate() + } else { + day = subtrahend.getUTCDate() + subtrahend = new Date(subtrahend.getTime() - 1000 * 60 * 60 * 24 * day) + day += Math.max(0, subtrahend.getUTCDate() - minuend.getUTCDate()) + } + + year = subtrahend.getUTCFullYear() - minuend.getUTCFullYear() + if (subtrahend.getUTCMonth() >= minuend.getUTCMonth()) { + month = subtrahend.getUTCMonth() - minuend.getUTCMonth() + } else { + month = 12 + subtrahend.getUTCMonth() - minuend.getUTCMonth() + year -= 1 + } + + let precisionIndex = unitNames.indexOf(precision) + if (precisionIndex === -1) { + precisionIndex = unitNames.length + } return new Duration( - i >= 0 ? year * sign : 0, - i >= 1 ? (month - year * 12) * sign : 0, + precisionIndex >= 0 ? year * sign : 0, + precisionIndex >= 1 ? month * sign : 0, 0, - i >= 3 ? (day - month * 30) * sign : 0, - i >= 4 ? (hr - day * 24) * sign : 0, - i >= 5 ? (min - hr * 60) * sign : 0, - i >= 6 ? (sec - min * 60) * sign : 0, - i >= 7 ? (ms - sec * 1000) * sign : 0, + precisionIndex >= 3 ? day * sign : 0, + precisionIndex >= 4 ? hr * sign : 0, + precisionIndex >= 5 ? min * sign : 0, + precisionIndex >= 6 ? sec * sign : 0, + precisionIndex >= 7 ? ms * sign : 0, ) } diff --git a/test/duration.ts b/test/duration.ts index ecb9e68..f5e2e0e 100644 --- a/test/duration.ts +++ b/test/duration.ts @@ -101,130 +101,244 @@ suite('duration', function () { }) suite('elapsedTime', function () { - const elapsed = new Set([ + const elapsedTests = [ { now: '2022-01-21T16:48:44.104Z', - input: '2022-10-21T16:48:44.104Z', - expected: 'P9M3D', + input: '2022-01-21T16:48:45.104Z', + expected: 'PT1S', }, { now: '2022-01-21T16:48:44.104Z', - input: '2022-10-21T16:48:45.104Z', - expected: 'P9M3DT1S', + input: '2022-01-21T16:49:43.104Z', + expected: 'PT59S', }, { now: '2022-01-21T16:48:44.104Z', - input: '2022-10-21T16:48:45.104Z', - precision: 'day', - expected: 'P9M3D', + input: '2022-01-21T16:49:44.104Z', + expected: 'PT1M', }, { - now: '2022-10-21T16:44:44.104Z', - input: '2022-10-21T16:48:44.104Z', - expected: 'PT4M', + now: '2022-01-21T16:48:44.104Z', + input: '2022-01-21T16:49:46.104Z', + expected: 'PT1M2S', }, { - now: '2022-09-22T16:48:44.104Z', - input: '2022-10-21T16:48:44.104Z', - expected: 'P29D', + now: '2022-01-21T16:48:44.104Z', + input: '2022-01-21T17:47:43.104Z', + expected: 'PT58M59S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T14:46:10.000Z', - expected: 'PT10S', + now: '2022-01-21T16:48:44.104Z', + input: '2022-01-21T17:48:44.104Z', + expected: 'PT1H', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T14:45:50.000Z', - expected: '-PT10S', + now: '2022-01-21T16:48:44.104Z', + input: '2022-01-21T17:50:47.104Z', + expected: 'PT1H2M3S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T14:45:50.000Z', - precision: 'minute', - expected: 'PT0M', + now: '2022-01-21T16:48:44.104Z', + input: '2022-01-22T15:47:43.104Z', + expected: 'PT22H58M59S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T14:47:40.000Z', - expected: 'PT1M40S', + now: '2022-01-21T16:48:44.104Z', + input: '2022-01-22T16:48:44.104Z', + expected: 'P1D', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T14:44:20.000Z', - expected: '-PT1M40S', + now: '2022-01-21T16:48:44.104Z', + input: '2022-01-22T18:51:48.104Z', + expected: 'P1DT2H3M4S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T14:44:20.000Z', - precision: 'minute', - expected: '-PT1M', + now: '2022-01-21T16:48:44.104Z', + input: '2022-02-20T15:47:43.104Z', + expected: 'P29DT22H58M59S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T15:51:40.000Z', - expected: 'PT1H5M40S', + now: '2022-01-21T16:48:44.104Z', + input: '2022-02-21T16:48:44.104Z', + expected: 'P1M', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T15:51:40.000Z', - precision: 'minute', - expected: 'PT1H5M', + now: '2022-01-21T16:48:44.104Z', + input: '2022-02-23T19:52:49.104Z', + expected: 'P1M2DT3H4M5S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T15:52:00.000Z', - expected: 'PT1H6M', + now: '2022-02-21T16:48:44.104Z', + input: '2023-01-20T15:47:43.104Z', + expected: 'P10M29DT22H58M59S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T17:46:00.000Z', - expected: 'PT3H', + now: '2022-01-21T16:48:44.104Z', + input: '2023-01-21T16:48:44.104Z', + expected: 'P1Y', + }, + { + now: '2022-01-21T16:48:44.104Z', + input: '2023-03-24T20:53:50.104Z', + expected: 'P1Y2M3DT4H5M6S', }, + { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-24T10:46:00.000Z', - expected: '-PT4H', + now: '2022-01-21T16:48:44.104Z', + input: '2023-03-24T20:53:50.104Z', + precision: 'second', + expected: 'P1Y2M3DT4H5M6S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-25T18:46:00.000Z', - expected: 'P1DT4H', + now: '2022-01-21T16:48:44.104Z', + input: '2023-03-24T20:53:50.104Z', + precision: 'minute', + expected: 'P1Y2M3DT4H5M', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-23T10:46:00.000Z', - expected: '-P1DT4H', + now: '2022-01-21T16:48:44.104Z', + input: '2023-03-24T20:53:50.104Z', + precision: 'hour', + expected: 'P1Y2M3DT4H', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2022-10-23T10:46:00.000Z', + now: '2022-01-21T16:48:44.104Z', + input: '2023-03-24T20:53:50.104Z', precision: 'day', + expected: 'P1Y2M3D', + }, + { + now: '2022-01-21T16:48:44.104Z', + input: '2023-03-24T20:53:50.104Z', + precision: 'month', + expected: 'P1Y2M', + }, + { + now: '2022-01-21T16:48:44.104Z', + input: '2023-03-24T20:53:50.104Z', + precision: 'year', + expected: 'P1Y', + }, + { + now: '2022-01-21T16:48:44.104Z', + input: '2023-03-24T20:53:50.104Z', + precision: 'garbage', + expected: 'P1Y2M3DT4H5M6S', + }, + + { + now: '2022-01-21T16:48:45.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-PT1S', + }, + { + now: '2022-01-21T16:49:43.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-PT59S', + }, + { + now: '2022-01-21T16:49:44.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-PT1M', + }, + { + now: '2022-01-21T16:49:46.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-PT1M2S', + }, + { + now: '2022-01-21T17:47:43.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-PT58M59S', + }, + { + now: '2022-01-21T17:48:44.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-PT1H', + }, + { + now: '2022-01-21T17:50:47.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-PT1H2M3S', + }, + { + now: '2022-01-22T15:47:43.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-PT22H58M59S', + }, + { + now: '2022-01-22T16:48:44.104Z', + input: '2022-01-21T16:48:44.104Z', expected: '-P1D', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2021-10-30T14:46:00.000Z', - expected: '-P11M29D', + now: '2022-01-22T18:51:48.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-P1DT2H3M4S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2021-10-30T14:46:00.000Z', - precision: 'month', - expected: '-P11M', + now: '2022-02-20T15:47:43.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-P29DT22H58M59S', + }, + { + now: '2022-02-21T16:48:44.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-P1M', + }, + { + now: '2022-02-23T19:52:49.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-P1M2DT3H4M5S', + }, + { + now: '2023-01-20T15:47:43.104Z', + input: '2022-02-21T16:48:44.104Z', + expected: '-P10M29DT22H58M59S', }, { - now: '2022-10-24T14:46:00.000Z', - input: '2021-10-29T14:46:00.000Z', + now: '2023-01-21T16:48:44.104Z', + input: '2022-01-21T16:48:44.104Z', expected: '-P1Y', }, { - now: '2023-03-23T12:03:00.000Z', - input: '2023-03-21T16:03:00.000Z', - expected: '-P1DT20H', + now: '2023-03-24T20:53:50.104Z', + input: '2022-01-21T16:48:44.104Z', + expected: '-P1Y2M3DT4H5M6S', }, - ]) - for (const {input, now, precision = 'millisecond', expected} of elapsed) { + + { + now: '2022-01-31T00:00:00.000Z', + input: '2022-02-28T00:00:00.000Z', + expected: 'P28D', + }, + { + now: '2022-02-28T00:00:00.000Z', + input: '2022-03-31T00:00:00.000Z', + expected: 'P1M3D', + }, + { + now: '2022-03-30T00:00:00.000Z', + input: '2022-05-01T00:00:00.000Z', + expected: 'P1M1D', + }, + { + now: '2022-03-31T00:00:00.000Z', + input: '2022-05-01T00:00:00.000Z', + expected: 'P1M1D', + }, + { + now: '2022-04-30T00:00:00.000Z', + input: '2022-06-01T00:00:00.000Z', + expected: 'P1M2D', + }, + { + now: '2024-02-29T00:00:00.000Z', + input: '2025-02-28T00:00:00.000Z', + expected: 'P11M30D', + }, + ] + for (const {input, now, precision = 'millisecond', expected} of elapsedTests) { test(`${input} is ${expected} elapsed from ${now} (precision ${precision})`, () => { assert.deepEqual(elapsedTime(new Date(input), precision, new Date(now).getTime()), Duration.from(expected)) }) diff --git a/test/relative-time.js b/test/relative-time.js index c98a1ae..02e01f6 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -452,7 +452,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 () => { @@ -1276,25 +1276,25 @@ suite('relative-time', function () { { datetime: '2022-12-03T15:46:00.000Z', format: 'duration', - expected: '1 month, 10 days, 1 hour', + expected: '1 month, 9 days, 1 hour', }, { datetime: '2022-12-03T15:46:00.000Z', format: 'duration', precision: 'minute', - expected: '1 month, 10 days, 1 hour', + expected: '1 month, 9 days, 1 hour', }, { datetime: '2022-12-03T15:46:00.000Z', format: 'duration', precision: 'day', - expected: '1 month, 10 days', + expected: '1 month, 9 days', }, { datetime: '2022-12-03T15:46:00.000Z', format: 'duration', tense: 'future', - expected: '1 month, 10 days, 1 hour', + expected: '1 month, 9 days, 1 hour', }, { datetime: '2022-12-03T15:46:00.000Z', @@ -1349,25 +1349,25 @@ 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', 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', @@ -1799,19 +1799,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', @@ -1823,7 +1823,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', @@ -2143,7 +2143,7 @@ suite('relative-time', function () { { datetime: '2021-10-30T14:46:00.000Z', format: 'elapsed', - expected: '11m 29d', + expected: '11m 24d', }, { datetime: '2021-10-30T14:46:00.000Z', @@ -2152,7 +2152,7 @@ suite('relative-time', function () { expected: '11m', }, { - datetime: '2021-10-29T14:46:00.000Z', + datetime: '2021-10-24T14:46:00.000Z', format: 'elapsed', expected: '1y', }, @@ -2481,7 +2481,7 @@ suite('relative-time', function () { datetime: '2022-01-01T12:00:00.000Z', tense: 'past', format: 'micro', - expected: '1y', + expected: '11m', }, { reference: '2022-12-31T12:00:00.000Z',