Prompt
Write a TypeScript function `parseDuration(input: string): number` that parses human-readable duration strings into milliseconds.
Requirements:
- Accept formats like "5m", "2h30m", "1d", "500ms", "1w2d3h4m5s"
- Support units: ms, s, m, h, d, w (week)
- Whitespace between components should be allowed but not required
- Return milliseconds as a number
- Throw a descriptive error for invalid input (e.g., "5x", "abc", empty string)
- Handle decimal values (e.g., "1.5h" → 5400000)
Include a few test cases demonstrating it works.
Responses with Opus 4.7 vs Qwen3.6 27B — parseDuration (1 rows)
claude-opus-4.7
anthropic/claude-opus-4.7
const UNIT_MS: Record<string, number> = {
ms: 1,
s: 1000,
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000,
w: 7 * 24 * 60 * 60 * 1000,
};
function parseDuration(input: string): number {
if (typeof input !== "string") {
throw new TypeError(`parseDuration: expected string, got ${typeof input}`);
}
const trimmed = input.trim();
if (trimmed.length === 0) {
throw new Error("parseDuration: input is empty");
}
// Remove all whitespace within the string
const normalized = trimmed.replace(/\s+/g, "");
// Match repeated <number><unit> pairs.
// Number: integer or decimal (e.g., 1, 1.5, .5)
// Unit: ms, s, m, h, d, w (note: ms must be matched before m)
const tokenRegex = /(\d+\.\d+|\.\d+|\d+)(ms|s|m|h|d|w)/gy;
let totalMs = 0;
let lastIndex = 0;
let matched = false;
let match: RegExpExecArray | null;
while ((match = tokenRegex.exec(normalized)) !== null) {
matched = true;
const value = parseFloat(match[1]);
const unit = match[2];
if (!Number.isFinite(value)) {
throw new Error(`parseDuration: invalid number "${match[1]}"`);
}
totalMs += value * UNIT_MS[unit];
lastIndex = tokenRegex.lastIndex;
}
if (!matched || lastIndex !== normalized.length) {
throw new Error(
`parseDuration: invalid duration string "${input}" ` +
`(expected format like "5m", "2h30m", "1.5h", "1w2d3h4m5s")`
);
}
return totalMs;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
function assertEqual(actual: number, expected: number, label: string): void {
if (actual !== expected) {
console.error(`✗ ${label}: expected ${expected}, got ${actual}`);
} else {
console.log(`✓ ${label} = ${actual}`);
}
}
function assertThrows(fn: () => unknown, label: string): void {
try {
fn();
console.error(`✗ ${label}: expected to throw, but did not`);
} catch (err) {
console.log(`✓ ${label} threw: ${(err as Error).message}`);
}
}
// Valid inputs
assertEqual(parseDuration("500ms"), 500, '"500ms"');
assertEqual(parseDuration("5m"), 5 * 60_000, '"5m"');
assertEqual(parseDuration("2h30m"), 2 * 3_600_000 + 30 * 60_000, '"2h30m"');
assertEqual(parseDuration("1d"), 86_400_000, '"1d"');
assertEqual(parseDuration("1.5h"), 5_400_000, '"1.5h"');
assertEqual(
parseDuration("1w2d3h4m5s"),
7 * 86_400_000 + 2 * 86_400_000 + 3 * 3_600_000 + 4 * 60_000 + 5 * 1000,
'"1w2d3h4m5s"'
);
assertEqual(parseDuration(" 2h 30m "), 9_000_000, '" 2h 30m "');
assertEqual(parseDuration("100ms200ms"), 300, '"100ms200ms"');
assertEqual(parseDuration(".5s"), 500, '".5s"');
// Invalid inputs
assertThrows(() => parseDuration(""), '""');
assertThrows(() => parseDuration(" "), '" "');
assertThrows(() => parseDuration("5x"), '"5x"');
assertThrows(() => parseDuration("abc"), '"abc"');
assertThrows(() => parseDuration("5"), '"5" (no unit)');
assertThrows(() => parseDuration("h5"), '"h5"');
assertThrows(() => parseDuration("5m garbage"), '"5m garbage"');
export { parseDuration };
How it works
- Normalization – the input is trimmed and any internal whitespace removed, so
"2h 30m"becomes"2h30m". - Sticky regex –
/(\d+\.\d+|\.\d+|\d+)(ms|s|m|h|d|w)/gymatches one<number><unit>token at a time, anchored to where the previous match ended (theyflag). This guarantees there are no gaps or stray characters between tokens. - Unit table – each unit's millisecond value is multiplied by the parsed number and accumulated.
- Validation – if no tokens matched, or the regex didn't consume the entire string, a descriptive error is thrown.
- Decimals –
parseFloathandles1.5h,.5s, etc. - Order matters –
msis listed beforemin the alternation so"500ms"isn't misread as500m + s.
12.60sfastest$0.047priciest
qwen3.6-27b
qwen/qwen3.6-27b
/**
* Parses a human-readable duration string into milliseconds.
*
* Supported formats: "5m", "2h30m", "1d", "500ms", "1w2d3h4m5s"
* Supported units: ms (milliseconds), s (seconds), m (minutes), h (hours), d (days), w (weeks)
*
* @param input - The duration string to parse.
* @returns The duration in milliseconds.
* @throws Error if the input string is invalid, empty, or contains unknown units.
*/
export function parseDuration(input: string): number {
if (typeof input !== 'string') {
throw new TypeError('Input must be a string');
}
const trimmedInput = input.trim();
if (trimmedInput.length === 0) {
throw new Error('Input duration string cannot be empty.');
}
// Map of units to their millisecond equivalent
const unitMap: Record<string, number> = {
ms: 1,
s: 1000,
m: 1000 * 60,
h: 1000 * 60 * 60,
d: 1000 * 60 * 60 * 24,
w: 1000 * 60 * 60 * 24 * 7,
};
// Regex to match a number (integer or float) immediately followed by a unit.
// Group 1: The numeric value (e.g., "1.5" or "500")
// Group 2: The unit (e.g., "h", "ms")
// Note: This regex does not allow whitespace between the number and the unit
// (e.g., "5 m" is invalid, "5m" is valid), but whitespace between components
// (e.g., "5m 2s") is handled by the validation step.
const parseRegex = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g;
let match: RegExpExecArray | null;
let totalMs = 0;
let hasValidComponent = false;
// Use exec to find all matches and sum them up
while ((match = parseRegex.exec(trimmedInput)) !== null) {
const valueStr = match[1];
const unit = match[2];
const value = parseFloat(valueStr);
if (isNaN(value)) {
throw new Error(`Invalid number format detected: "${valueStr}"`);
}
if (!(unit in unitMap)) {
// This block is theoretically unreachable due to the strict regex unit group,
// but kept for safety.
throw new Error(`Unknown unit encountered: "${unit}"`);
}
totalMs += value * unitMap[unit];
hasValidComponent = true;
}
if (!hasValidComponent) {
throw new Error(`Invalid duration format: "${input}". No valid time components found. Use units like ms, s, m, h, d, w.`);
}
// Validation Step:
// Ensure that the input string didn't contain any invalid text outside the matched components.
// We create a regex that matches valid components (number + unit) OR any whitespace.
// If we replace all valid tokens and whitespace with an empty string, the result should be empty.
const validationRegex = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)|\s/g;
const remainingChars = trimmedInput.replace(validationRegex, '');
if (remainingChars.length > 0) {
throw new Error(`Invalid characters found in duration string: "${remainingChars}".`);
}
return totalMs;
}
// ==========================================
// Test Cases
// ==========================================
const runTests = () => {
const tests: { input: string; expected: number | string }[] = [
// Basic units
{ input: "500ms", expected: 500 },
{ input: "1s", expected: 1000 },
{ input: "5m", expected: 300000 },
{ input: "1h", expected: 3600000 },
{ input: "1d", expected: 86400000 },
{ input: "1w", expected: 604800000 },
// Combined units
{ input: "2h30m", expected: 9000000 }, // 2 hours + 30 mins
{ input: "1h 5m", expected: 3900000 }, // Whitespace allowed
{ input: "1w2d3h4m5s", expected: 675925000 }, // Complex mix
// Decimals
{ input: "1.5h", expected: 5400000 }, // 1.5 hours
{ input: "0.5s", expected: 500 },
// Whitespace edge cases
{ input: " 1h ", expected: 3600000 }, // Leading/trailing whitespace
// Error cases
{ input: "", expected: "Error" },
{ input: "abc", expected: "Error" },
{ input: "5", expected: "Error" }, // Missing unit
{ input: "5x", expected: "Error" }, // Invalid unit
{ input: "1h 2bad", expected: "Error" }, // Invalid text inside
];
console.log("Running Tests...\n");
tests.forEach((test, index) => {
try {
const result = parseDuration(test.input);
if (typeof test.expected === 'number') {
const passed = result === test.expected;
console.log(
`${passed ? '✅ PASS' : '❌ FAIL'} [Test ${index + 1}] "${test.input}" -> ${result} (Expected: ${test.expected})`
);
if (!passed) {
console.log(` Difference: ${result - test.expected}`);
}
} else {
console.log(`❌ FAIL [Test ${index + 1}] "${test.input}" -> Expected Error, got ${result}`);
}
} catch (error) {
if (test.expected === "Error") {
console.log(`✅ PASS [Test ${index + 1}] "${test.input}" -> Threw Error: "${error}"`);
} else {
console.log(`❌ FAIL [Test ${index + 1}] "${test.input}" -> Unexpected Error: "${error}"`);
}
}
});
};
// Execute tests
runTests();
160.19sslowest$0.030cheapest
Want to run your own AI model comparisons?
Try Evvl