Try Evvl

Opus 4.7 vs Qwen3.6 27B — parseDuration

Created May 9, 2026, 10:55 AM
Expires in 2 days

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)

openrouteropenrouter
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

  1. Normalization – the input is trimmed and any internal whitespace removed, so "2h 30m" becomes "2h30m".
  2. Sticky regex/(\d+\.\d+|\.\d+|\d+)(ms|s|m|h|d|w)/gy matches one <number><unit> token at a time, anchored to where the previous match ended (the y flag). This guarantees there are no gaps or stray characters between tokens.
  3. Unit table – each unit's millisecond value is multiplied by the parsed number and accumulated.
  4. Validation – if no tokens matched, or the regex didn't consume the entire string, a descriptive error is thrown.
  5. DecimalsparseFloat handles 1.5h, .5s, etc.
  6. Order mattersms is listed before m in the alternation so "500ms" isn't misread as 500m + s.
12.60sfastest$0.047priciest
openrouteropenrouter
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