duration-parser.ts•2.77 kB
// src/services/duration-parser.ts
import type { Duration } from '../types/index.js';
/**
* Parse duration string into decimal hours
* Supports multiple formats:
* - "2h", "2 hours", "2.5h"
* - "90m", "90 minutes"
* - "PT2H30M" (ISO 8601)
* - "half an hour", "quarter hour"
*/
export function parseDuration(input: string): Duration {
const normalized = input.toLowerCase().trim();
// Handle natural language
if (normalized.match(/half\s*(an?\s*)?hour/)) {
return { hours: 0.5, formatted: '0.5h' };
}
if (normalized.match(/quarter\s*(of\s*an?\s*)?hour/)) {
return { hours: 0.25, formatted: '0.25h' };
}
// Parse hours: "2h", "2 hours", "2.5h"
const hoursMatch = normalized.match(/^(\d+(?:\.\d+)?)\s*(hour|hr|h)s?$/);
if (hoursMatch) {
const hours = parseFloat(hoursMatch[1]);
return { hours, formatted: `${hours}h` };
}
// Parse minutes: "90m", "90 minutes"
const minutesMatch = normalized.match(/^(\d+)\s*(minute|min|m)s?$/);
if (minutesMatch) {
const minutes = parseInt(minutesMatch[1]);
const hours = minutes / 60;
return { hours, formatted: `${hours.toFixed(2)}h` };
}
// Parse ISO 8601 duration: PT2H30M
const isoMatch = normalized.match(/^pt(?:(\d+)h)?(?:(\d+)m)?(?:(\d+(?:\.\d+)?)s)?$/);
if (isoMatch) {
const [, hoursStr, minutesStr, secondsStr] = isoMatch;
let totalHours = 0;
if (hoursStr) totalHours += parseInt(hoursStr);
if (minutesStr) totalHours += parseInt(minutesStr) / 60;
if (secondsStr) totalHours += parseFloat(secondsStr) / 3600;
return { hours: totalHours, formatted: `${totalHours.toFixed(2)}h` };
}
// Try just a number (assume hours)
const numberMatch = normalized.match(/^(\d+(?:\.\d+)?)$/);
if (numberMatch) {
const hours = parseFloat(numberMatch[1]);
return { hours, formatted: `${hours}h` };
}
throw new Error(`Unable to parse duration: "${input}". Try formats like "2h", "90m", or "1.5h"`);
}
/**
* Format hours as readable duration
* Examples: 2.5 → "2.5h", 0.75 → "0.75h"
*/
export function formatDuration(hours: number): string {
// Use 1 decimal place if needed, otherwise no decimals
if (hours % 1 === 0) {
return `${hours}h`;
}
return `${hours.toFixed(1)}h`;
}
/**
* Convert hours to hours and minutes
* Example: 2.5 → "2h 30m"
*/
export function formatDurationDetailed(hours: number): string {
const wholeHours = Math.floor(hours);
const minutes = Math.round((hours - wholeHours) * 60);
if (minutes === 0) {
return `${wholeHours}h`;
}
if (wholeHours === 0) {
return `${minutes}m`;
}
return `${wholeHours}h ${minutes}m`;
}