Skip to content

Commit

Permalink
Merge pull request #198 from github/make-duration-class
Browse files Browse the repository at this point in the history
Make duration class
  • Loading branch information
keithamus authored Nov 4, 2022
2 parents ebfa819 + 6ab5725 commit 7eb4d8c
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 24 deletions.
50 changes: 50 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
74 changes: 52 additions & 22 deletions src/duration.ts
Original file line number Diff line number Diff line change
@@ -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<string, number>
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))
}
39 changes: 37 additions & 2 deletions test/duration.ts
Original file line number Diff line number Diff line change
@@ -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([
Expand All @@ -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)
})
}
})
Expand Down

0 comments on commit 7eb4d8c

Please sign in to comment.