From 3c3a8bb1075bbfb75f5f0b7aa6f3611f126935e5 Mon Sep 17 00:00:00 2001 From: forehalo Date: Fri, 30 May 2025 03:21:31 +0000 Subject: [PATCH] feat(server): time duration helper (#12562) ## Summary by CodeRabbit - **New Features** - Introduced support for parsing and converting duration strings (e.g., "1h30m") into milliseconds and seconds. - Added utility methods to handle a wide range of time units and their combinations. - Added functions to calculate dates offset before or after a given date by specified durations. - **Tests** - Implemented comprehensive automated tests to ensure accurate parsing and conversion of duration strings. --- .../__snapshots__/duration.spec.ts.md | 135 ++++++++++++++++++ .../__snapshots__/duration.spec.ts.snap | Bin 0 -> 507 bytes .../src/base/utils/__tests__/duration.spec.ts | 37 +++++ .../backend/server/src/base/utils/duration.ts | 78 ++++++++++ .../backend/server/src/base/utils/index.ts | 1 + 5 files changed, 251 insertions(+) create mode 100644 packages/backend/server/src/base/utils/__tests__/__snapshots__/duration.spec.ts.md create mode 100644 packages/backend/server/src/base/utils/__tests__/__snapshots__/duration.spec.ts.snap create mode 100644 packages/backend/server/src/base/utils/__tests__/duration.spec.ts create mode 100644 packages/backend/server/src/base/utils/duration.ts diff --git a/packages/backend/server/src/base/utils/__tests__/__snapshots__/duration.spec.ts.md b/packages/backend/server/src/base/utils/__tests__/__snapshots__/duration.spec.ts.md new file mode 100644 index 000000000..6e8f9384d --- /dev/null +++ b/packages/backend/server/src/base/utils/__tests__/__snapshots__/duration.spec.ts.md @@ -0,0 +1,135 @@ +# Snapshot report for `src/base/utils/__tests__/duration.spec.ts` + +The actual snapshot is saved in `duration.spec.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## should parse duration strings correctly + +> parser - 1ms + + '{"ms":1}' + +> ms - 1ms + + 1 + +> parser - 1s + + '{"s":1}' + +> ms - 1s + + 1000 + +> parser - 1m + + '{"m":1}' + +> ms - 1m + + 60000 + +> parser - 1h + + '{"h":1}' + +> ms - 1h + + 3600000 + +> parser - 1d + + '{"d":1}' + +> ms - 1d + + 86400000 + +> parser - 1w + + '{"w":1}' + +> ms - 1w + + 604800000 + +> parser - 1M + + '{"M":1}' + +> ms - 1M + + 2592000000 + +> parser - 1y + + '{"y":1}' + +> ms - 1y + + 31536000000 + +> parser - 1000ms + + '{"ms":1000}' + +> ms - 1000ms + + 1000 + +> parser - 60s + + '{"s":60}' + +> ms - 60s + + 60000 + +> parser - 30m + + '{"m":30}' + +> ms - 30m + + 1800000 + +> parser - 1h30m + + '{"h":1,"m":30}' + +> ms - 1h30m + + 5400000 + +> parser - 15d + + '{"d":15}' + +> ms - 15d + + 1296000000 + +> parser - 1y + + '{"y":1}' + +> ms - 1y + + 31536000000 + +> parser - 12M + + '{"M":12}' + +> ms - 12M + + 31104000000 + +> parser - 1y1M1d1h1m1s1ms + + '{"y":1,"M":1,"d":1,"h":1,"m":1,"s":1,"ms":1}' + +> ms - 1y1M1d1h1m1s1ms + + 34218061001 diff --git a/packages/backend/server/src/base/utils/__tests__/__snapshots__/duration.spec.ts.snap b/packages/backend/server/src/base/utils/__tests__/__snapshots__/duration.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..7cb8486014f75a6d3394802df8d09737727fd6b4 GIT binary patch literal 507 zcmVn@I>QR}A zmI2X`aL861? xHo)}Cb&#ww!0pJv3Ueae)RFG!zK*kP$PMxi9)8k6NP0Z(mtR265VqL`003x!_ErD@ literal 0 HcmV?d00001 diff --git a/packages/backend/server/src/base/utils/__tests__/duration.spec.ts b/packages/backend/server/src/base/utils/__tests__/duration.spec.ts new file mode 100644 index 000000000..f3ce8e76a --- /dev/null +++ b/packages/backend/server/src/base/utils/__tests__/duration.spec.ts @@ -0,0 +1,37 @@ +import test from 'ava'; + +import { Due } from '../duration'; + +test('should parse duration strings correctly', t => { + const testcases = [ + '1ms', + '1s', + '1m', + '1h', + '1d', + '1w', + '1M', + '1y', + '1000ms', + '60s', + '30m', + '1h30m', + '15d', + '1y', + '12M', + '1y1M1d1h1m1s1ms', + ]; + + for (const str of testcases) { + t.snapshot(JSON.stringify(Due.parse(str)), `parser - ${str}`); + t.snapshot(Due.ms(str), `ms - ${str}`); + } +}); + +test('should calc relative time correctly', t => { + const date = new Date(); + t.is(Due.before('1d', date).getTime(), date.getTime() - 1000 * 60 * 60 * 24); + + const date2 = new Date(); + t.is(Due.after('1d', date2).getTime(), date2.getTime() + 1000 * 60 * 60 * 24); +}); diff --git a/packages/backend/server/src/base/utils/duration.ts b/packages/backend/server/src/base/utils/duration.ts new file mode 100644 index 000000000..e6fb5abf4 --- /dev/null +++ b/packages/backend/server/src/base/utils/duration.ts @@ -0,0 +1,78 @@ +type DurationUnit = 'd' | 'w' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms'; +type DurationInput = Partial>; + +const UnitToSecMap: Record = { + ms: 0.001, + s: 1, + m: 60, + h: 3600, + d: 24 * 3600, + w: 7 * 24 * 3600, + M: 30 * 24 * 3600, + y: 365 * 24 * 3600, +}; + +const KnownCharCodeToCharMap: Record = { + 100: 'd', + 119: 'w', + 77: 'M', + 121: 'y', + 104: 'h', + 109: 'm', + 115: 's', +}; + +function parse(str: string): DurationInput { + let input: DurationInput = {}; + + let acc = 0; + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + const code = ch.charCodeAt(0); + + // number [0..9] + if (code >= 48 && code <= 57) { + acc = acc * 10 + code - 48; + } else { + let unit = KnownCharCodeToCharMap[code]; + if (!unit) { + throw new Error(`Invalid duration string unit ${ch}`); + } + + // look ahead a char for 'ms' checking if unit met 'm' + if (unit === 'm' && str[i + 1] === 's') { + unit = 'ms'; + i++; + } + + input[unit] = acc; + acc = 0; + } + } + + return input; +} + +export const Due = { + ms: (dueStr: string | DurationInput) => { + const input = typeof dueStr === 'string' ? parse(dueStr) : dueStr; + return Object.entries(input).reduce((duration, [unit, val]) => { + return duration + UnitToSecMap[unit as DurationUnit] * (val || 0) * 1000; + }, 0); + }, + s: (dueStr: string | DurationInput) => { + const input = typeof dueStr === 'string' ? parse(dueStr) : dueStr; + return Object.entries(input).reduce((duration, [unit, val]) => { + return duration + UnitToSecMap[unit as DurationUnit] * (val || 0); + }, 0); + }, + parse, + after: (dueStr: string | number | DurationInput, date?: Date) => { + const timestamp = typeof dueStr === 'number' ? dueStr : Due.ms(dueStr); + return new Date((date?.getTime() ?? Date.now()) + timestamp); + }, + before: (dueStr: string | number | DurationInput, date?: Date) => { + const timestamp = typeof dueStr === 'number' ? dueStr : Due.ms(dueStr); + return new Date((date?.getTime() ?? Date.now()) - timestamp); + }, +}; diff --git a/packages/backend/server/src/base/utils/index.ts b/packages/backend/server/src/base/utils/index.ts index f1129b735..9b0dd5cf5 100644 --- a/packages/backend/server/src/base/utils/index.ts +++ b/packages/backend/server/src/base/utils/index.ts @@ -1,3 +1,4 @@ +export * from './duration'; export * from './promise'; export * from './request'; export * from './stream';