feat: duration parser and date utilities
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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'));
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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`;
|
||||
}
|
||||
Reference in New Issue
Block a user