mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 10:52:40 +08:00
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:
@@ -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
|
||||||
Binary file not shown.
@@ -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);
|
||||||
|
});
|
||||||
78
packages/backend/server/src/base/utils/duration.ts
Normal file
78
packages/backend/server/src/base/utils/duration.ts
Normal 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);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './duration';
|
||||||
export * from './promise';
|
export * from './promise';
|
||||||
export * from './request';
|
export * from './request';
|
||||||
export * from './stream';
|
export * from './stream';
|
||||||
|
|||||||
Reference in New Issue
Block a user