feat: duration parser and date utilities

This commit is contained in:
2026-05-20 17:44:21 +02:00
parent 9a4e9c3e1e
commit 2e0c6f7f8f
4 changed files with 79 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
import { toDateString, todayString, addDays } from '../utils/date';
describe('date utils', () => {
it('formats a date as YYYY-MM-DD', () => {
expect(toDateString(new Date('2026-05-20T10:00:00'))).toBe('2026-05-20');
});
it('addDays adds days correctly', () => {
expect(addDays('2026-05-20', 3)).toBe('2026-05-23');
});
it('addDays handles month boundary', () => {
expect(addDays('2026-05-30', 2)).toBe('2026-06-01');
});
});
+19
View File
@@ -0,0 +1,19 @@
import { parseDuration, formatDuration } from '../utils/duration';
describe('parseDuration', () => {
it('parses hours', () => expect(parseDuration('2h')).toBe(120));
it('parses hours with space', () => expect(parseDuration('2 h')).toBe(120));
it('parses minutes with m', () => expect(parseDuration('90m')).toBe(90));
it('parses minutes with min', () => expect(parseDuration('45min')).toBe(45));
it('parses plain number as minutes', () => expect(parseDuration('60')).toBe(60));
it('parses decimal hours', () => expect(parseDuration('1.5h')).toBe(90));
it('returns null for invalid input', () => expect(parseDuration('abc')).toBeNull());
it('returns null for zero', () => expect(parseDuration('0m')).toBeNull());
it('returns null for empty string', () => expect(parseDuration('')).toBeNull());
});
describe('formatDuration', () => {
it('formats whole hours', () => expect(formatDuration(120)).toBe('2h'));
it('formats minutes only', () => expect(formatDuration(45)).toBe('45m'));
it('formats hours and minutes', () => expect(formatDuration(90)).toBe('1h 30m'));
});
+13
View File
@@ -0,0 +1,13 @@
export function toDateString(date: Date): string {
return date.toISOString().slice(0, 10);
}
export function todayString(): string {
return toDateString(new Date());
}
export function addDays(dateStr: string, days: number): string {
const d = new Date(dateStr + 'T12:00:00');
d.setDate(d.getDate() + days);
return toDateString(d);
}
+32
View File
@@ -0,0 +1,32 @@
export function parseDuration(input: string): number | null {
const s = input.trim().toLowerCase();
if (!s) return null;
const hoursMatch = s.match(/^(\d+(?:\.\d+)?)\s*h(?:ours?)?$/);
if (hoursMatch) {
const minutes = Math.round(parseFloat(hoursMatch[1]!) * 60);
return minutes > 0 ? minutes : null;
}
const minutesMatch = s.match(/^(\d+)\s*m(?:in(?:utes?)?)?$/);
if (minutesMatch) {
const minutes = parseInt(minutesMatch[1]!, 10);
return minutes > 0 ? minutes : null;
}
const plainMatch = s.match(/^(\d+)$/);
if (plainMatch) {
const minutes = parseInt(plainMatch[1]!, 10);
return minutes > 0 ? minutes : null;
}
return null;
}
export function formatDuration(minutes: number): string {
const h = Math.floor(minutes / 60);
const m = minutes % 60;
if (h === 0) return `${m}m`;
if (m === 0) return `${h}h`;
return `${h}h ${m}m`;
}