From ffc67884330a3f3d53d4da0d33a9d479e1653394 Mon Sep 17 00:00:00 2001 From: Hamza Hamidi <22576950+hamzahamidi@users.noreply.github.com> Date: Sat, 29 Aug 2026 21:46:53 +0200 Subject: [PATCH] feat(duration): parse time strings like HH:mm:ss into durations Matches moment.js behavior for creating durations from colon-separated time strings: HH:mm:ss, HH:mm, days.HH:mm:ss, and fractional seconds. Closes #1146 --- src/plugin/duration/index.js | 12 +++++++++++ test/plugin/duration.test.js | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/plugin/duration/index.js b/src/plugin/duration/index.js index 5aaacee21..4b9c60894 100644 --- a/src/plugin/duration/index.js +++ b/src/plugin/duration/index.js @@ -10,6 +10,7 @@ const MILLISECONDS_A_YEAR = MILLISECONDS_A_DAY * 365 const MILLISECONDS_A_MONTH = MILLISECONDS_A_YEAR / 12 const DURATION_REGEX_PARSE = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/ +const TIME_REGEX_PARSE = /^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/ const DURATION_REGEX_FORMAT = /\[([^\]]+)]|YYYY|YY|Y|M{1,2}|D{1,2}|H{1,2}|m{1,2}|s{1,2}|SSS/g const unitToMS = { @@ -97,6 +98,17 @@ class Duration { this.calMilliseconds() return this } + const t = input.match(TIME_REGEX_PARSE) + if (t) { + const sign = t[1] === '-' ? -1 : 1 + this.$d.days = sign * (t[2] ? Number(t[2]) : 0) + this.$d.hours = sign * Number(t[3]) + this.$d.minutes = sign * Number(t[4]) + this.$d.seconds = sign * (t[5] ? Number(t[5]) : 0) + this.$d.milliseconds = sign * (t[6] ? Math.round(Number(`0${t[6]}`) * 1000) : 0) + this.calMilliseconds() + return this + } } return this } diff --git a/test/plugin/duration.test.js b/test/plugin/duration.test.js index f9a4c7cf4..7f13d693e 100644 --- a/test/plugin/duration.test.js +++ b/test/plugin/duration.test.js @@ -88,6 +88,48 @@ describe('Creating', () => { }) }) +describe('Parse time string', () => { + it('HH:mm:ss', () => { + const d = dayjs.duration('02:04:33') + expect(d.hours()).toBe(2) + expect(d.minutes()).toBe(4) + expect(d.seconds()).toBe(33) + expect(d.asSeconds()).toBe(7473) + expect(d.toISOString()).toBe('PT2H4M33S') + }) + it('HH:mm', () => { + const d = dayjs.duration('23:59') + expect(d.hours()).toBe(23) + expect(d.minutes()).toBe(59) + expect(d.seconds()).toBe(0) + expect(d.toISOString()).toBe('PT23H59M') + }) + it('days.HH:mm:ss', () => { + const d = dayjs.duration('7.23:59:59') + expect(d.days()).toBe(7) + expect(d.hours()).toBe(23) + expect(d.minutes()).toBe(59) + expect(d.seconds()).toBe(59) + }) + it('HH:mm:ss.SSS (fractional seconds)', () => { + const d = dayjs.duration('23:59:59.999') + expect(d.hours()).toBe(23) + expect(d.minutes()).toBe(59) + expect(d.seconds()).toBe(59) + expect(d.milliseconds()).toBe(999) + }) + it('negative time string', () => { + const d = dayjs.duration('-23:59:59') + expect(d.hours()).toBe(-23) + expect(d.minutes()).toBe(-59) + expect(d.seconds()).toBe(-59) + expect(d.toISOString()).toBe('-PT23H59M59S') + }) + it('Invalid string still returns P0D', () => { + expect(dayjs.duration('Invalid').toISOString()).toBe('P0D') + }) +}) + describe('Parse ISO string', () => { it('Full ISO string', () => { expect(dayjs.duration('P7Y6M4DT3H2M1S').toISOString()).toBe('P7Y6M4DT3H2M1S')