diff --git a/package-lock.json b/package-lock.json index 45d4934..fb92880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.4", "@github/prettier-config": "0.0.4", + "@js-temporal/polyfill": "^0.4.3", "@open-wc/testing": "^3.1.6", "@web/dev-server-esbuild": "^0.3.2", "@web/test-runner": "^0.14.0", @@ -279,6 +280,25 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@js-temporal/polyfill": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.4.3.tgz", + "integrity": "sha512-6Fmjo/HlkyVCmJzAPnvtEWlcbQUSRhi8qlN9EtJA/wP7FqXsevLLrlojR44kzNzrRkpf7eDJ+z7b4xQD/Ycypw==", + "dev": true, + "dependencies": { + "jsbi": "^4.1.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@js-temporal/polyfill/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + }, "node_modules/@lit/reactive-element": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.4.1.tgz", @@ -3986,6 +4006,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", + "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6267,6 +6293,24 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@js-temporal/polyfill": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.4.3.tgz", + "integrity": "sha512-6Fmjo/HlkyVCmJzAPnvtEWlcbQUSRhi8qlN9EtJA/wP7FqXsevLLrlojR44kzNzrRkpf7eDJ+z7b4xQD/Ycypw==", + "dev": true, + "requires": { + "jsbi": "^4.1.0", + "tslib": "^2.3.1" + }, + "dependencies": { + "tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", + "dev": true + } + } + }, "@lit/reactive-element": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.4.1.tgz", @@ -9046,6 +9090,12 @@ "argparse": "^2.0.1" } }, + "jsbi": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz", + "integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", diff --git a/package.json b/package.json index 6adacad..aac8833 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@custom-elements-manifest/analyzer": "^0.6.4", "@github/prettier-config": "0.0.4", + "@js-temporal/polyfill": "^0.4.3", "@open-wc/testing": "^3.1.6", "@web/dev-server-esbuild": "^0.3.2", "@web/test-runner": "^0.14.0", diff --git a/src/duration.ts b/src/duration.ts index ab40b37..aac8f51 100644 --- a/src/duration.ts +++ b/src/duration.ts @@ -1,32 +1,62 @@ -const duration = /^[-+]?P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/ +const durationRe = /^[-+]?P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/ -export const isDuration = (str: string) => duration.test(str) +export const isDuration = (str: string) => durationRe.test(str) -export function applyDuration(date: Date, str: string): Date | null { - const r = new Date(date) - str = String(str).trim() - const factor = str.startsWith('-') ? -1 : 1 - const parsed = String(str) - .trim() - .match(duration) - ?.slice(1) - .map(x => (Number(x) || 0) * factor) - if (!parsed) return null - const [years, months, weeks, days, hours, minutes, seconds] = parsed +export class Duration { + constructor( + public years = 0, + public months = 0, + public weeks = 0, + public days = 0, + public hours = 0, + public minutes = 0, + public seconds = 0 + ) {} + + abs() { + return new Duration( + Math.abs(this.years), + Math.abs(this.months), + Math.abs(this.weeks), + Math.abs(this.days), + Math.abs(this.hours), + Math.abs(this.minutes), + Math.abs(this.seconds) + ) + } - r.setFullYear(r.getFullYear() + years) - r.setMonth(r.getMonth() + months) - r.setDate(r.getDate() + weeks * 7 + days) - r.setHours(r.getHours() + hours) - r.setMinutes(r.getMinutes() + minutes) - r.setSeconds(r.getSeconds() + seconds) + static from(durationLike: unknown): Duration { + if (typeof durationLike === 'string') { + const str = String(durationLike).trim() + const factor = str.startsWith('-') ? -1 : 1 + const parsed = str + .match(durationRe) + ?.slice(1) + .map(x => (Number(x) || 0) * factor) + if (!parsed) return new Duration() + return new Duration(...parsed) + } else if (typeof durationLike === 'object') { + const {years, months, weeks, days, hours, minutes, seconds} = durationLike as Record + return new Duration(years, months, weeks, days, hours, minutes, seconds) + } + throw new RangeError('invalid duration') + } +} + +export function applyDuration(date: Date, duration: Duration): Date | null { + const r = new Date(date) + r.setFullYear(r.getFullYear() + duration.years) + r.setMonth(r.getMonth() + duration.months) + r.setDate(r.getDate() + duration.weeks * 7 + duration.days) + r.setHours(r.getHours() + duration.hours) + r.setMinutes(r.getMinutes() + duration.minutes) + r.setSeconds(r.getSeconds() + duration.seconds) return r } export function withinDuration(a: Date, b: Date, str: string): boolean { - const absStr = str.replace(/^[-+]/, '') - const sign = a < b ? '-' : '' - const threshold = applyDuration(a, `${sign}${absStr}`) + const duration = Duration.from(str).abs() + const threshold = applyDuration(a, duration) if (!threshold) return true return Math.abs(Number(threshold) - Number(a)) > Math.abs(Number(a) - Number(b)) } diff --git a/test/duration.ts b/test/duration.ts index f3c1e80..9b6f6df 100644 --- a/test/duration.ts +++ b/test/duration.ts @@ -1,7 +1,42 @@ import {assert} from '@open-wc/testing' -import {applyDuration, withinDuration} from '../src/duration.ts' +import {Duration, applyDuration, withinDuration} from '../src/duration.ts' +import {Temporal} from '@js-temporal/polyfill' suite('duration', function () { + suite('Duration class', () => { + const tests = new Set([ + {input: 'P4Y', years: 4}, + {input: '-P4Y', years: -4}, + {input: '-P3MT5M', months: -3, minutes: -5}, + {input: 'P1Y2M3DT4H5M6S', years: 1, months: 2, days: 3, hours: 4, minutes: 5, seconds: 6}, + {input: 'P5W', weeks: 5}, + {input: '-P5W', weeks: -5} + ]) + + const extractValues = x => ({ + years: x.years || 0, + months: x.months || 0, + weeks: x.weeks || 0, + days: x.days || 0, + hours: x.hours || 0, + minutes: x.minutes || 0, + seconds: x.seconds || 0 + }) + for (const {input, ...expected} of tests) { + test(`${input} -> from(${JSON.stringify(expected)})`, () => { + assert.deepEqual(extractValues(Temporal.Duration.from(input)), extractValues(expected)) + assert.deepEqual(extractValues(Duration.from(input)), extractValues(expected)) + }) + } + for (const {input} of tests) { + test(`${input} -> abs()`, () => { + const temporalAbs = extractValues(Temporal.Duration.from(input).abs()) + const abs = extractValues(Duration.from(input).abs()) + assert.deepEqual(temporalAbs, abs) + }) + } + }) + suite('applyDuration', function () { const referenceDate = '2022-10-21T16:48:44.104Z' const tests = new Set([ @@ -14,7 +49,7 @@ suite('duration', function () { ]) for (const {input, expected} of tests) { test(`${referenceDate} -> ${input} -> ${expected}`, () => { - assert.equal(applyDuration(new Date(referenceDate), input)?.toISOString(), expected) + assert.equal(applyDuration(new Date(referenceDate), Duration.from(input))?.toISOString(), expected) }) } })