feat(server): time duration helper (#12562)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
forehalo
2025-05-30 03:21:31 +00:00
parent 88eec2cdfb
commit 3c3a8bb107
5 changed files with 251 additions and 0 deletions

View File

@@ -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

View File

@@ -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);
});

View File

@@ -0,0 +1,78 @@
type DurationUnit = 'd' | 'w' | 'M' | 'y' | 'h' | 'm' | 's' | 'ms';
type DurationInput = Partial<Record<DurationUnit, number>>;
const UnitToSecMap: Record<DurationUnit, number> = {
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<number, DurationUnit> = {
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);
},
};

View File

@@ -1,3 +1,4 @@
export * from './duration';
export * from './promise';
export * from './request';
export * from './stream';