diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index adaa975..2ce84cd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install @@ -12,8 +12,10 @@ jobs: strategy: matrix: node-version: - - "12" - - "14" + - "20" + - "22" + - "24" + - "26" name: tests "on": - push diff --git a/index.d.ts b/index.d.ts index c7a33be..cf2cca4 100644 --- a/index.d.ts +++ b/index.d.ts @@ -61,6 +61,24 @@ declare namespace PostgresInterval { * ``` */ toISOStringShort(): string; + /** + * Returns a [`Temporal.Duration`](https://tc39.es/proposal-temporal/docs/duration.html) representing the interval. + * + * Requires `globalThis.Temporal` (Node 26+, or a polyfill assigned to the global). Throws otherwise. + * + * Postgres mixed-sign intervals (e.g. `1 mon -1 days`) throw a `RangeError`, because `Temporal.Duration` requires a single sign across all fields. + * + * The `Temporal` types are not yet in the default TypeScript lib. For this signature to resolve, your project needs Temporal lib types (the TypeScript lib once available, or `@js-temporal/polyfill` types). + * + * ```js + * var parse = require('postgres-interval') + * var interval = parse('01:02:03') + * // => { hours: 1, minutes: 2, seconds: 3 } + * interval.toTemporalDuration().toString() + * // PT1H2M3S + * ``` + */ + toTemporalDuration(): Temporal.Duration; } } diff --git a/index.js b/index.js index 1332b33..daa3a34 100644 --- a/index.js +++ b/index.js @@ -93,6 +93,25 @@ PostgresInterval.prototype.toISOStringShort = function () { return toISOString.call(this, { short: true }) } +PostgresInterval.prototype.toTemporalDuration = function () { + if (typeof globalThis.Temporal === 'undefined') { + throw new Error('Temporal is not available. It ships unflagged in Node 26+. On older runtimes, install a polyfill (e.g. @js-temporal/polyfill) and assign it to globalThis.Temporal.') + } + + const totalMicroseconds = Math.round(this.milliseconds * 1000) + + return globalThis.Temporal.Duration.from({ + years: this.years, + months: this.months, + days: this.days, + hours: this.hours, + minutes: this.minutes, + seconds: this.seconds, + milliseconds: Math.trunc(totalMicroseconds / 1000), + microseconds: totalMicroseconds % 1000 + }) +} + function toISOString ({ short }) { let datePart = '' diff --git a/readme.md b/readme.md index bb95bb9..306e3a3 100644 --- a/readme.md +++ b/readme.md @@ -57,6 +57,16 @@ Also available as `interval.toISO()` for backwards compatibility. Returns an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) compliant string shortened to minimum length, for example `PT9M`. +#### `interval.toTemporalDuration()` -> `Temporal.Duration` + +Returns a [`Temporal.Duration`](https://tc39.es/proposal-temporal/docs/duration.html) representing the interval. + +Requires `globalThis.Temporal`. It ships unflagged in Node 26+. On older runtimes, install a polyfill such as [`@js-temporal/polyfill`](https://www.npmjs.com/package/@js-temporal/polyfill) and assign it to `globalThis.Temporal`. The method throws if `Temporal` is unavailable. + +Postgres mixed-sign intervals (e.g. `1 mon -1 days`) throw a `RangeError`. `Temporal.Duration` requires all fields to share a single sign, which these intervals violate. + +The `Temporal` types are not yet in the default TypeScript lib. To resolve the return type, your project needs Temporal lib types: the TypeScript lib once available, or the `@js-temporal/polyfill` types. This package adds no type dependency. + ## License MIT © [Ben Drucker](http://bendrucker.me) diff --git a/test.js b/test.js index 3336941..1d04a09 100644 --- a/test.js +++ b/test.js @@ -114,5 +114,40 @@ test(function (t) { t.end() }) + if (typeof globalThis.Temporal !== 'undefined') { + t.test('toTemporalDuration', function (t) { + const simple = interval('01:02:03').toTemporalDuration() + t.equal(simple.hours, 1) + t.equal(simple.minutes, 2) + t.equal(simple.seconds, 3) + + const full = interval('1 year 2 mons 3 days 04:05:06').toTemporalDuration() + t.equal(full.years, 1) + t.equal(full.months, 2) + t.equal(full.days, 3) + t.equal(full.hours, 4) + t.equal(full.minutes, 5) + t.equal(full.seconds, 6) + + const subSecond = interval('00:00:00.123456').toTemporalDuration() + t.equal(subSecond.milliseconds, 123) + t.equal(subSecond.microseconds, 456) + + t.throws(function () { + interval('1 mon -1 days').toTemporalDuration() + }, RangeError, 'mixed-sign interval throws') + + const negative = interval('-01:02:03').toTemporalDuration() + t.equal(negative.sign, -1) + t.equal(negative.hours, -1) + t.equal(negative.minutes, -2) + t.equal(negative.seconds, -3) + + t.end() + }) + } else { + t.skip('toTemporalDuration (Temporal unavailable)') + } + t.end() })