Edge Case Handling

Code GenerationDifficulty: HARD11 models comparedTotal cost: $0.048289

Prompt

Write a TypeScript function `parsePrice(input: string): number | null` that extracts a numeric price from strings like "$1,234.56", "EUR 99.9", "1.234,56€" (European format), "free", "TBD". Return null for non-numeric prices. Handle edge cases.

Model Responses

Claude Opus 4.6

Unrated
38,250ms2433 tokens
```typescript function parsePrice(input: string): number | null { // Trim whitespace const trimmed = input.trim(); // Handle empty string if (trimmed === '') { return null; } // Handle non-numeric keywords const nonNumericKeywords = ['free', 'tbd', 'n/a', 'na', 'none', 'call', 'contact', '-', '—']; if (nonNumericKeywords.includes(trimmed.toLowerCase())) { return null; } // Remove currency symbols and currency codes // Common currency symbols: $, €, £, ¥, ₹, ₽, ₩, ₪, ฿, ₫, ₴, ₦, ₡, ₲, ₵, ₸, ₺, ₼, ₾, ₿ // Common currency codes: USD, EUR, GBP, JPY, etc. let cleaned = trimmed; // Remove currency codes (3-letter codes like USD, EUR, GBP, etc.) cleaned = cleaned.replace(/^[A-Z]{3}\s*/i, ''); cleaned = cleaned.replace(/\s*[A-Z]{3}$/i, ''); // Remove currency symbols cleaned = cleaned.replace(/[$€£¥₹₽₩₪฿₫₴₦₡₲₵₸₺₼₾₿]/g, ''); // Remove any remaining whitespace cleaned = cleaned.trim(); // If nothing left after removing currency info, return null if (cleaned === '') { return null; } // Determine if this is European format (e.g., "1.234,56") or standard format (e.g., "1,234.56") // European format uses dots as thousand separators and comma as decimal separator // Standard format uses commas as thousand separators and dot as decimal separator const lastCommaIndex = cleaned.lastIndexOf(','); const lastDotIndex = cleaned.lastIndexOf('.'); let normalizedString: string; if (lastCommaIndex !== -1 && lastDotIndex !== -1) { if (lastCommaIndex > lastDotIndex) { // European format: dots are thousand separators, comma is decimal separator // e.g., "1.234,56" normalizedString = cleaned.replace(/\./g, '').replace(',', '.'); } else { // Standard format: commas are thousand separators, dot is decimal separator // e.g., "1,234.56" normalizedString = cleaned.replace(/,/g, ''); } } else if (lastCommaIndex !== -1 && lastDotIndex === -1) { // Only commas present // Need to determine if comma is decimal separator or thousand separator // Heuristic: if there's exactly one comma and the part after it has 1-2 digits, treat as decimal // If part after comma has exactly 3 digits and there could be more groups, treat as thousand separator const parts = cleaned.split(','); const afterLastComma = parts[parts.length - 1]; if (parts.length === 2 && (afterLastComma.length === 1 || afterLastComma.length === 2)) { // Likely a decimal separator: "99,9" or "99,99" normalizedString = cleaned.replace(',', '.'); } else if (afterLastComma.length === 3) { // Could be thousand separator: "1,234" or "1,234,567" // Verify all groups except possibly the first have exactly 3 digits const allGroupsValid = parts.slice(1).every(p => p.length === 3); if (allGroupsValid) { normalizedString = cleaned.replace(/,/g, ''); } else { // Ambiguous, treat comma as decimal separator normalizedString = cleaned.replace(/,/g, '.'); } } else { // Ambiguous or unusual; try treating comma as decimal // But if there are multiple commas, that doesn't make sense as decimal if (parts.length > 2) { // Multiple commas — treat as thousand separators normalizedString = cleaned.replace(/,/g, ''); } else { // Single comma with unusual digit count after it — treat as decimal normalizedString = cleaned.replace(',', '.'); } } } else if (lastDotIndex !== -1 && lastCommaIndex === -1) { // Only dots present // Similar heuristic const parts = cleaned.split('.'); if (parts.length === 2) { // Single dot — likely decimal separator: "1234.56" // But could be European thousand separator: "1.234" const afterDot = parts[1]; if (afterDot.length === 3 && parts[0].length <= 3) { // Ambiguous: "1.234" could be 1234 (European) or 1.234 // Default to standard decimal interpretation normalizedString = cleaned; } else { normalizedString = cleaned; } } else if (parts.length > 2) { // Multiple dots — likely European thousand separators: "1.234.567" const allGroupsValid = parts.slice(1).every(p => p.length === 3); if (allGroupsValid) { normalizedString = cleaned.replace(/\./g, ''); } else { // Invalid format return null; } } else { normalizedString = cleaned; } } else { // No commas or dots normalizedString = cleaned; } // Remove any remaining non-numeric characters except dot and minus normalizedString = normalizedString.replace(/[^0-9.\-+]/g, ''); // Handle multiple dots (shouldn't happen after normalization, but just in case) const dotCount = (normalizedString.match(/\./g) || []).length; if (dotCount > 1) { return null; } // Handle empty string after cleaning if (normalizedString === '' || normalizedString === '.' || normalizedString === '-' || normalizedString === '+') { return null; } const result = parseFloat(normalizedString); if (isNaN(result) || !isFinite(result)) { return null; } // Round to avoid floating point issues (max 2 decimal places for prices typically, // but we'll preserve what was given) return result; } // Test cases console.log(parsePrice("$1,234.56")); // 1234.56 console.log(parsePrice("EUR 99.9")); // 99.9 console.log(parsePrice("1.234,56€")); // 1234.56 console.log(parsePrice("free")); // null console.log(parsePrice("TBD")); // null console.log(parsePrice("£1,000")); // 1000 console.log(parsePrice("¥1234")); // 1234 console.log(parsePrice("99,9")); // 99.9 console.log(parsePrice("1.234.567,89")); // 1234567.89 console.log(parsePrice("$0.99")); // 0.99 console.log(parsePrice("")); // null console.log(parsePrice(" $ ")); // null console.log(parsePrice("N/A")); // null console.log(parsePrice("1,234,567.89")); // 1234567.89 console.log(parsePrice("1.234.567")); // 1234567 console.log(parsePrice("USD 49.99")); // 49.99 console.log(parsePrice("49.99 USD")); // 49.99 console.log(parsePrice("$-5.00")); // -5 ``` ## Key Design Decisions ### Format Detection Heuristics The function distinguishes between **standard** (`1,234.56`) and **European** (`1.234,56`) number formats by examining the relative positions of the last comma and last dot: - **Both present**: If the comma comes *after* the dot → European; otherwise → standard - **Only commas**: If the portion after the comma has 1–2 digits (`99,9`), it's a decimal separator. If it has 3 digits (`1,234`), it's a thousands separator - **Only dots**: Multiple dots with 3-digit groups (`1.234.567`) → European thousands separators. Single dot → decimal separator ### Edge Cases Handled | Input | Result | Reason | |-------|--------|--------| | `"free"`, `"TBD"`, `"N/A"` | `null` | Non-numeric keyword | | `""`, `" $ "` | `null` | No numeric content | | `"$-5.00"` | `-5` | Negative prices | | `"1.234.567,89"` | `1234567.89` | Multi-group European format | | `"49.99 USD"` | `49.99` | Trailing currency code | | Multiple currency symbols (₹, ¥, £, ₿, etc.) | Stripped cleanly | Broad Unicode currency support |

GPT-4.1

Unrated
10,111ms866 tokens
Here's a robust `parsePrice` function for your requirements: ```typescript function parsePrice(input: string): number | null { // Preprocessing if (!input || typeof input !== 'string') return null; // Handle common non-numeric price text const nonPrices = [ 'free', 'tbd', 'n/a', 'na', 'not available', '-', 'none', 'complimentary', 'no charge' ]; if (nonPrices.includes(input.trim().toLowerCase())) return null; // Extract number part including possible thousands and decimal separators // Match sequences with digits, either ',' or '.' or ' ', with optional trailing currency // Also allow for negative amounts const match = input.match(/-?[\d., ]+/); if (!match) return null; let numStr = match[0].replace(/\s/g, ''); // Remove spaces (may be thousand sep, eg "1 234,56") if (!numStr.match(/\d/)) return null; // Must have at least a digit // Heuristics to guess decimal/thousand separator: // If both , and . are present, the one that occurs last is the decimal. // If only one of , or . is present, decide based on context/length of trailing digits let decimalSeparator: string | null = null; const lastComma = numStr.lastIndexOf(','); const lastDot = numStr.lastIndexOf('.'); if (lastComma !== -1 && lastDot !== -1) { decimalSeparator = lastComma > lastDot ? ',' : '.'; } else if (lastComma !== -1) { // Only comma present // If there are 3 digits after comma, likely thousands separator, else decimal decimalSeparator = (numStr.length - lastComma - 1) === 3 ? null : ','; } else if (lastDot !== -1) { // Only dot present decimalSeparator = (numStr.length - lastDot - 1) === 3 ? null : '.'; } // Remove thousands separator if (decimalSeparator === ',' || decimalSeparator === '.') { const otherSep = decimalSeparator === ',' ? '.' : ','; numStr = numStr.replace(new RegExp('\\' + otherSep, 'g'), ''); // Replace decimal with . if (decimalSeparator === ',') { numStr = numStr.replace(/,/, '.'); } } else { // No decimal separator, just remove any , or . numStr = numStr.replace(/[,.]/g, ''); } const num = parseFloat(numStr); return isNaN(num) ? null : num; } ``` ### **Usage Examples:** ```typescript parsePrice("$1,234.56"); // 1234.56 parsePrice("EUR 99.9"); // 99.9 parsePrice("1.234,56€"); // 1234.56 parsePrice("£899"); // 899 parsePrice("1 234,00 Kč"); // 1234 parsePrice("free"); // null parsePrice("TBD"); // null parsePrice('-123,45'); // -123.45 parsePrice('N/A'); // null ``` --- **Edge cases handled:** - "free", "TBD", and similar → `null` - Both US ("$1,234.56") and EU ("1.234,56€") notation supported - Handles spaces as thousands separators - Ignores currency symbols before/after the number - Returns `null` on any non-numeric string

GPT-4.1 Mini

Unrated
18,718ms1231 tokens
Here's a TypeScript function `parsePrice` that tries to extract numeric prices from various formatted strings, handling US/European formats, currency symbols, and non-numeric values like "free" and "TBD": ```typescript function parsePrice(input: string): number | null { if (!input) return null; const str = input.trim().toLowerCase(); // Return null for known non-numeric price indicators if (["free", "tbd", "n/a", "na", "none"].includes(str)) { return null; } // Remove currency symbols and letters (e.g., $, €, EUR, USD) // Keep digits, commas, dots and possible minus sign let cleaned = str.replace(/[^0-9,.\-]/g, ''); // If empty after removing non-numeric characters, return null if (!cleaned) return null; // Determine the number format (US or European) // Strategy: // 1) If both comma and dot present: // - if dot comes after comma, treat comma as thousand separator, dot as decimal // - if comma comes after dot, treat dot as thousand separator, comma as decimal // 2) If only comma present: // - if comma occurs exactly once and position is last 3 or 2 chars, comma as decimal // - else comma as thousand separator // 3) If only dot present: // - if dot occurs exactly once and position is last 3 or 2 chars, dot as decimal // - else dot as thousand separator // 4) If no separator, parse as integer const hasComma = cleaned.indexOf(',') !== -1; const hasDot = cleaned.indexOf('.') !== -1; let normalized: string; if (hasComma && hasDot) { const lastComma = cleaned.lastIndexOf(','); const lastDot = cleaned.lastIndexOf('.'); if (lastDot > lastComma) { // comma as thousand separator, dot as decimal separator normalized = cleaned.replace(/,/g, ''); } else { // dot as thousand separator, comma as decimal separator normalized = cleaned.replace(/\./g, '').replace(',', '.'); } } else if (hasComma && !hasDot) { const commaCount = (cleaned.match(/,/g) || []).length; // Check if comma is likely decimal separator by position const lastCommaPos = cleaned.lastIndexOf(','); if (commaCount === 1 && (cleaned.length - lastCommaPos - 1 === 2 || cleaned.length - lastCommaPos - 1 === 1)) { // comma as decimal separator normalized = cleaned.replace(',', '.'); } else { // comma as thousand separator normalized = cleaned.replace(/,/g, ''); } } else if (!hasComma && hasDot) { const dotCount = (cleaned.match(/\./g) || []).length; const lastDotPos = cleaned.lastIndexOf('.'); if (dotCount === 1 && (cleaned.length - lastDotPos - 1 === 2 || cleaned.length - lastDotPos - 1 === 1)) { // dot as decimal separator normalized = cleaned; } else { // dot as thousand separator normalized = cleaned.replace(/\./g, ''); } } else { // No comma or dot normalized = cleaned; } // Handle possible leading minus sign (e.g., "-1234.56") if (/^-/.test(normalized)) { // Negative price? Probably invalid, but let's parse anyway } const parsed = Number(normalized); if (isNaN(parsed)) { return null; } return parsed; } // Example usage: console.log(parsePrice("$1,234.56")); // 1234.56 console.log(parsePrice("EUR 99.9")); // 99.9 console.log(parsePrice("1.234,56€")); // 1234.56 (European format) console.log(parsePrice("free")); // null console.log(parsePrice("TBD")); // null console.log(parsePrice("1,234,567.89"));// 1234567.89 console.log(parsePrice("1.234.567,89"));// 1234567.89 console.log(parsePrice(" €1234 ")); // 1234 console.log(parsePrice("NA")); // null console.log(parsePrice("none")); // null ``` ### Explanation - First, we trim and convert the input to lowercase for easier comparison. - If the string matches known non-numeric words like "free", "tbd", etc., return `null`. - Next, remove all non-numeric related characters except digits, commas, dots, and minus signs. - Then identify if the string contains comma and/or dot and infer which is the decimal separator: - If both present, the last occurring separator is assumed to be the decimal separator. - If only one type exists, we guess decimal separator by checking position and occurrence count. - Normalize the string by removing thousand separators and replacing decimal separator with a dot. - Finally, parse the resulting string to number. Return `null` if parsing fails. This approach handles common US and European number formats and ignores currency symbols and words effectively.

GPT-4.1 Nano

Unrated
7,903ms1019 tokens
Certainly! To achieve this, we'll write a `parsePrice` function that: - Handles different formats and currency signs. - Strips out non-numeric parts. - Deals with European formats (commas as decimal separators, dots as thousand separators). - Returns `null` for non-numeric, non-parsable strings like "free" or "TBD". Here's the implementation: ```typescript function parsePrice(input: string): number | null { // Trim and convert input to lowercase for easier detection const trimmed = input.trim(); // Check for special non-numeric cases const lower = trimmed.toLowerCase(); if (lower === 'free' || lower === 'tbd') { return null; } // Remove common currency symbols and non-numeric characters except for digits, dots, commas const cleaned = trimmed // Remove currency symbols and codes (assuming common symbols) .replace(/[\$\€\£\¥\₹]/g, '') // Remove currency codes like USD, EUR, etc. .replace(/\b[A-Z]{2,4}\b/g, '') // Remove all spaces .replace(/\s+/g, ''); // Check if the string has numeric characters after cleaning if (!/\d/.test(cleaned)) { return null; } // Determine format: European or US // We can infer this based on the presence of ',' and '.' positions // For example: // - "1.234,56" (European): '.' thousand separator, ',' decimal separator // - "1,234.56" (US): ',' thousand separator, '.' decimal separator // For simplicity, assume: // - If the string contains both '.' and ',', and ',' comes after '.', treat as European // - Else, default to US format // Count occurrences const commaCount = (cleaned.match(/,/g) || []).length; const dotCount = (cleaned.match(/\./g) || []).length; // Decide format let normalized = cleaned; if (commaCount > 0 && dotCount > 0) { // Both present if (cleaned.indexOf(',') > cleaned.indexOf('.')) { // European format: '.' as thousand, ',' as decimal normalized = cleaned .replace(/\./g, '') // remove thousand separator .replace(',', '.'); // decimal separator to dot } else { // US format: ',' as thousand, '.' as decimal normalized = cleaned .replace(/,/g, ''); // remove thousand separator } } else if (commaCount > 0 && dotCount === 0) { // Only commas // Decide based on position: if comma appears near end, treat as decimal const lastCommaIndex = cleaned.lastIndexOf(','); const digitsAfterComma = cleaned.length - lastCommaIndex - 1; if (digitsAfterComma === 2) { // e.g., "99,99" → decimal separator normalized = cleaned.replace(/\./g, '').replace(',', '.'); } else { // e.g., "1,234" → thousand separator normalized = cleaned.replace(/,/g, ''); } } else { // Only dots or no separators normalized = cleaned; } // Remove any remaining non-numeric characters normalized = normalized.replace(/[^0-9.-]/g, ''); // Parse to float const value = parseFloat(normalized); if (isNaN(value)) { return null; } return value; } ``` ### Usage Examples: ```typescript console.log(parsePrice("$1,234.56")); // 1234.56 console.log(parsePrice("EUR 99.9")); // 99.9 console.log(parsePrice("1.234,56€")); // 1234.56 console.log(parsePrice("free")); // null console.log(parsePrice("TBD")); // null console.log(parsePrice("€2.345,67")); // 2345.67 console.log(parsePrice("1,234")); // 1234 (assuming US format) console.log(parsePrice("1.234")); // 1234 (assuming US format) ``` This function makes some assumptions for simplicity, but it should handle most typical formats and edge cases. You can enhance it further based on specific requirements or locale detection.

o3

Unrated
24,018ms2284 tokens
```ts /** * Tries to pull a price out of an arbitrary string. * * • Understands both “US” (1,234.56) and “EU” (1.234,56) style separators * • Works with or without currency symbols / codes * • Recognises the usual “this is not a price” words (free, TBD, …) * • Returns `null` when it cannot be sure a number is really a price */ export function parsePrice(input: string): number | null { if (!input) { return null; } // ---- 1. Quick negative filters ---------------------------------------- const lowered = input.toLowerCase().replace(/\u00a0/g, " "); // normalise NBSP const nonPriceWords = [ "free", "gratis", "tbd", "n/a", "na", "no charge", "none", "complimentary" ]; if (nonPriceWords.some(w => lowered.includes(w))) { return null; } // ---- 2. Find the first “number–like” token ---------------------------- // optional ‑ sign, then at least one digit, then digits/space/./, allowed const numToken = input.match(/-?\d[\d.,\s]*/); if (!numToken) { return null; } let valuePart = numToken[0].trim(); // strip spaces that are obviously thousands separators valuePart = valuePart.replace(/\s+/g, ""); // ---- 3. Decide which separator (if any) is the decimal --------------- // strategy: // – when both “,” and “.” exist → the one that occurs last is decimal // – when only one exists → if its last group has 2 digits, // we treat it as decimal // → otherwise it is a thousands mark let decimalSep: "." | "," | null = null; const lastDot = valuePart.lastIndexOf("."); const lastComma = valuePart.lastIndexOf(","); if (lastDot !== -1 && lastComma !== -1) { decimalSep = lastDot > lastComma ? "." : ","; } else if (lastComma !== -1) { const decLen = valuePart.length - lastComma - 1; if (decLen === 2) { decimalSep = ","; } } else if (lastDot !== -1) { const decLen = valuePart.length - lastDot - 1; if (decLen === 2) { decimalSep = "."; } } // ---- 4. Build a canonical “plain-dot” number string ------------------- let canonical: string; if (decimalSep === ".") { canonical = valuePart .replace(/,/g, "") // thousands .replace(/(?<=\d)\.(?=\d{2}$)/, "."); // keep only the decimal dot } else if (decimalSep === ",") { canonical = valuePart .replace(/\./g, "") // thousands .replace(/,(?=\d{2}$)/, "."); // convert decimal comma to dot canonical = canonical.replace(/,/g, ""); // any stray commas } else { // no decimal part – just kill all separators canonical = valuePart.replace(/[.,]/g, ""); } // ---- 5. Parse ---------------------------------------------------------- const num = Number.parseFloat(canonical); return Number.isFinite(num) ? num : null; } ``` Usage examples ```ts parsePrice("$1,234.56"); // → 1234.56 parsePrice("EUR 99.9"); // → 99.9 parsePrice("1.234,56€"); // → 1234.56 parsePrice("-£2,000"); // → -2000 parsePrice("free"); // → null parsePrice("TBD"); // → null ``` The heuristics are purposely conservative: if the function cannot confidently interpret the number, it returns `null` instead of risking a wrong value.

o3 Mini

Unrated
19,384ms2785 tokens
Below is one complete solution. The function first “cleans” the input by removing any characters other than digits, commas, periods, and an optional minus sign. Then, using some heuristics, it decides which separator (if any) is used for decimals and which ones are thousand‐separators. For example, if both a comma and period are present the one appearing later in the string is assumed to be the decimal separator; if only a comma is present, it is treated as a decimal separator only if it is followed by one or two digits (otherwise it’s assumed to be a thousand‐separator). Finally, parseFloat is applied and if it fails (or if the input was “free” or “TBD”) the function returns null. Below is the complete TypeScript code: ------------------------------------------------ function parsePrice(input: string): number | null { // Normalize the input. const trimmed = input.trim(); // If the input indicates a non-numeric price return null. if (/^(free|tbd)$/i.test(trimmed)) { return null; } // Remove currency symbols and letters except for digits, comma, period, and the minus sign. // This also removes spaces. let cleaned = trimmed.replace(/[^0-9,.\-]/g, ''); if (cleaned === '') { return null; } // Count the occurrence of commas and periods. const commaCount = (cleaned.match(/,/g) || []).length; const periodCount = (cleaned.match(/\./g) || []).length; // Handle mixed styles: if both comma and period are present. if (commaCount > 0 && periodCount > 0) { // Determine which symbol is the decimal separator by comparing the rightmost positions. const lastComma = cleaned.lastIndexOf(','); const lastPeriod = cleaned.lastIndexOf('.'); if (lastComma > lastPeriod) { // Assume comma is the decimal separator. // Remove all periods (assumed to be thousand separators), then replace the comma with a period. cleaned = cleaned.replace(/\./g, ''); cleaned = cleaned.replace(/,/g, '.'); } else { // Assume period is the decimal separator. Remove commas which then are thousand separators. cleaned = cleaned.replace(/,/g, ''); } } // If only commas are present... else if (commaCount > 0) { if (commaCount === 1) { const commaIndex = cleaned.indexOf(','); const fractionPart = cleaned.substring(commaIndex + 1); // If there's one or two digits after the comma, treat it as a decimal separator. if (fractionPart.length === 1 || fractionPart.length === 2) { cleaned = cleaned.replace(/,/g, '.'); } else { // Otherwise assume the comma is a thousand separator and remove it. cleaned = cleaned.replace(/,/g, ''); } } else { // Multiple commas: assume they are all thousand separators. cleaned = cleaned.replace(/,/g, ''); } } // Only period(s) exist. else if (periodCount > 0) { if (periodCount > 1) { // Assume the last period is the decimal separator and any preceding ones are thousand separators. const lastPeriodIdx = cleaned.lastIndexOf('.'); const beforeDec = cleaned.slice(0, lastPeriodIdx).replace(/\./g, ''); const afterDec = cleaned.slice(lastPeriodIdx + 1); cleaned = beforeDec + '.' + afterDec; } // Otherwise, with a single period, we assume it is the decimal separator. } // Now attempt a parse. const price = parseFloat(cleaned); return isNaN(price) ? null : price; } // Example usage: console.log(parsePrice("$1,234.56")); // 1234.56 (English format) console.log(parsePrice("EUR 99.9")); // 99.9 console.log(parsePrice("1.234,56€")); // 1234.56 (European format) console.log(parsePrice("free")); // null console.log(parsePrice("TBD")); // null console.log(parsePrice("1,234")); // 1234 console.log(parsePrice("1,234,567.89")); // 1234567.89 // Edge case tests: console.log(parsePrice("1.234.567,89€")); // 1234567.89 console.log(parsePrice("-€1,234.56")); // -1234.56 ------------------------------------------------ Explanation: 1. The function first checks if the trimmed (case‐insensitive) string is “free” or “TBD” and returns null if so. 2. It then “cleans” the string by removing everything except digits, commas, periods, and the minus sign. 3. It counts commas and periods to decide on the formatting style:   • If both exist, the rightmost occurrence is assumed to be the decimal separator.   • If only a comma exists and it is followed by one or two digits, it is treated as the decimal separator; otherwise it is assumed to be a thousand separator.   • When multiple periods are present, it assumes that only the last one is the decimal point. 4. Finally, parseFloat is used to convert the normalized string into a number and an appropriate value is returned. This implementation should handle the common and edge cases as requested.

o4 Mini

Unrated
27,743ms2759 tokens
Here’s one possible implementation in TypeScript. It will 1. Trim and normalize whitespace 2. Short‐circuit on “free”/“tbd” 3. Strip out everything except digits, the separators `.` , `,` and an optional sign 4. Heuristically decide if it’s US‐style (`,` thousands / `.` decimal) or EU‐style (`.` thousands / `,` decimal) 5. Normalize to a single `.` decimal and parse with `parseFloat` 6. Return `null` if it can’t find a valid number ```ts function parsePrice(input: string): number | null { if (!input) return null; // 1) Trim, collapse whitespace const s = input.trim().toLowerCase(); // 2) Handle literal non‐numeric values if (/^(free|tbd)$/i.test(s)) { return null; } // 3) Remove ALL whitespace, then strip out // anything that's not digit, ".", ",", "+" or "-" let cleaned = s .replace(/\s+/g, "") // remove spaces .replace(/[^0-9\.,+-]/g, ""); // keep only digits, separators, signs if (!cleaned) { return null; } // 4) Decide whether it's a mixed US/EU format const hasDot = cleaned.includes("."); const hasComma = cleaned.includes(","); if (hasDot && hasComma) { // whichever appears last is probably the decimal separator if (cleaned.lastIndexOf(".") > cleaned.lastIndexOf(",")) { // US style: remove commas cleaned = cleaned.replace(/,/g, ""); } else { // EU style: remove dots, turn commas into dots cleaned = cleaned.replace(/\./g, "").replace(/,/g, "."); } } else if (hasComma) { // only commas const parts = cleaned.split(","); // if exactly one comma and <=2 digits after, treat as decimal if (parts.length === 2 && parts[1].length <= 2) { cleaned = cleaned.replace(",", "."); } else { // otherwise strip all commas (thousands separators) cleaned = cleaned.replace(/,/g, ""); } } else if (hasDot) { // only dots const dotCount = (cleaned.match(/\./g) || []).length; if (dotCount > 1) { // assume last dot is decimal, earlier are thousands separators const lastDot = cleaned.lastIndexOf("."); const intPart = cleaned.slice(0, lastDot).replace(/\./g, ""); const decPart = cleaned.slice(lastDot + 1); cleaned = intPart + "." + decPart; } // else exactly one dot → treat as decimal } // 5) Parse const num = parseFloat(cleaned); // 6) If it isn't a valid number, give up if (isNaN(num)) { return null; } return num; } // --- Examples / Tests --- console.log(parsePrice("$1,234.56")); // 1234.56 console.log(parsePrice("EUR 99.9")); // 99.9 console.log(parsePrice("1.234,56€")); // 1234.56 console.log(parsePrice("free")); // null console.log(parsePrice("TBD")); // null console.log(parsePrice("¥ 2 345 678"));// 2345678 (handles whitespace) console.log(parsePrice("(€1.234)")); // 1234 (strips parentheses) console.log(parsePrice("1,234")); // 1234 (ambiguous, treated as thousands) console.log(parsePrice("99,9")); // 99.9 (treated as decimal) ``` Explanation of the main heuristics: - If both `.` and `,` appear, assume the rightmost one is the decimal separator. - If only commas appear, a single comma with at most two digits after → decimal; otherwise → thousands. - If only dots appear multiple times, assume all but the last are thousands separators. - Everything else (letters, currency symbols, extra punctuation) is stripped out before parsing.

GPT-4o Mini

Unrated
39,831ms712 tokens$0.000393
To create a TypeScript function that parses different formats of prices from strings, we can use regular expressions and some string manipulation. Below is the implementation of the `parsePrice` function while considering the requirements you've provided: ```typescript function parsePrice(input: string): number | null { // Trim the input string to remove leading/trailing whitespace input = input.trim().toLowerCase(); // Handle specific cases early if (input === 'free' || input === 'tbd') { return null; } // Define a regex pattern to match price formats const pricePattern = /(?:[^\d.]*(\d{1,3}(?:[,.\s]\d{3})*(?:[,.\s]\d+)?)(?:[^\d.]*)?)/; const match = input.match(pricePattern); if (!match || match.length < 2) { return null; // No valid price found } // Extract the matched price string let priceStr = match[1].replace(/[,.\s]/g, (match) => { // Replace comma as thousand separator with an empty string // Replace dot or space as decimal separator with a dot return match === ',' ? '' : '.'; }); // Convert the extracted price string to a number const parsedPrice = parseFloat(priceStr); // Return null if conversion failed or if the result is NaN return isNaN(parsedPrice) ? null : parsedPrice; } // Example usages: console.log(parsePrice("$1,234.56")); // 1234.56 console.log(parsePrice("EUR 99.9")); // 99.9 console.log(parsePrice("1.234,56€")); // 1234.56 console.log(parsePrice("free")); // null console.log(parsePrice("TBD")); // null ``` ### Explanation: 1. **Input Trimming and Normalization**: The input string is trimmed of whitespace and converted to lowercase to simplify matching against specific strings like "free" and "TBD". 2. **Early Return for Special Cases**: If the string matches "free" or "TBD", the function returns `null` immediately since these are specifically defined not to have a numeric price. 3. **Regex Pattern**: We use a regex pattern to match typical price formats. The pattern captures sequences that: - May have currency symbols or other characters around the digits. - Allow for commas (`,`) and periods (`.`) as part of the number. 4. **Replacement of Non-Numeric Characters**: Any commas used as thousand separators are stripped out, while the decimal separator (which can be a dot or comma depending on the locale) is standardized to a dot. 5. **Final Conversion**: The cleaned price string is then converted to a floating-point number. If the conversion results in `NaN`, the function correctly returns `null`. 6. **Return Value**: The function returns either the parsed number or `null` when appropriate.

GPT-4o

Unrated
10,219ms852 tokens$0.007950
To implement a TypeScript function `parsePrice` that extracts a numeric price from a variety of formatted strings, you can leverage regular expressions to identify and extract numeric values, while also handling different currency and number formatting cases. The function should also handle special strings like "free" and "TBD" where a numeric price isn't present, returning `null` for these non-numeric cases. Here's an example implementation: ```typescript function parsePrice(input: string): number | null { // Normalize the input by trimming whitespace const normalizedInput = input.trim().toLowerCase(); // Special case: Check for non-numeric phrases if (normalizedInput === 'free' || normalizedInput === 'tbd') { return null; } // Remove common currency symbols and letters (USD, EUR, $) from input const cleanedInput = normalizedInput.replace(/[^\d.,-]/g, ''); // Try to detect European vs. non-European formatting const euroFormat = /^[\d.]+,\d{2}$/; // Like 1.234,56 which is €1234.56 const usFormat = /^[\d,]+\.\d{2}$/; // Like 1,234.56 which is $1234.56 let numericValue: number | null = null; if (euroFormat.test(cleanedInput)) { // European format: Treat '.' as thousands separator and ',' as decimal separator numericValue = parseFloat(cleanedInput.replace(/\./g, '').replace(',', '.')); } else if (usFormat.test(cleanedInput)) { // US format: Treat ',' as thousands separator and '.' as decimal separator numericValue = parseFloat(cleanedInput.replace(/,/g, '')); } else { // Attempt to parse where a single non-standard character has been used // like a case where people use ',' instead of '.' or vice versa. if (cleanedInput.includes(',')) { // Treat ',' as decimal if there's no '.' numericValue = parseFloat(cleanedInput.replace(',', '.')); } else if (cleanedInput.includes('.')) { // Treat '.' as decimal numericValue = parseFloat(cleanedInput); } else { // If there are no commas or periods, it might be an integer-like string numericValue = parseFloat(cleanedInput); } } // If the parsed value is a valid number, return it return isNaN(numericValue) ? null : numericValue; } // Example usage: console.log(parsePrice("$1,234.56")); // Output: 1234.56 console.log(parsePrice("EUR 99.9")); // Output: 99.9 console.log(parsePrice("1.234,56€")); // Output: 1234.56 console.log(parsePrice("free")); // Output: null console.log(parsePrice("TBD")); // Output: null ``` ### Important Points: 1. **Input Normalization**: Convert the input to lowercase and trim any surrounding whitespace. 2. **Currency Handling**: Strip out currency symbols and irrelevant characters before processing. 3. **Format Detection**: Use regular expressions to determine if the format is European (e.g., "1.234,56") or commonly used in the US (e.g., "1,234.56"), and convert accordingly. 4. **Error Handling**: Return `null` if parsing results in `NaN`, or if the input is a known non-numeric phrase like "free" or "TBD". This implementation aims to be robust enough for common cases and some edge cases by handling diverse input formats.

Claude Haiku 4.5

Unrated
11,671ms1861 tokens$0.008989
# Price Parser Function ```typescript function parsePrice(input: string): number | null { if (!input || typeof input !== 'string') { return null; } const trimmed = input.trim().toLowerCase(); // Handle special cases if (trimmed === 'free' || trimmed === 'gratis' || trimmed === '0') { return 0; } if (/^(tbd|n\/a|na|unknown|pending|contact|call|quote)$/.test(trimmed)) { return null; } // Remove common currency symbols and text let cleaned = trimmed .replace(/[$£€¥₹₽₩฿]/g, '') .replace(/\b(usd|eur|gbp|jpy|cny|inr|rub|krw|thb|aed)\b/g, '') .trim(); if (!cleaned) { return null; } // Detect European format (comma as decimal separator) // European format: 1.234,56 or 1234,56 const isEuropeanFormat = /[.,]/.test(cleaned) && cleaned.lastIndexOf('.') < cleaned.lastIndexOf(','); let normalizedPrice: string; if (isEuropeanFormat) { // European format: remove dots (thousands), replace comma with dot normalizedPrice = cleaned .replace(/\./g, '') // Remove thousand separators (dots) .replace(/,/g, '.'); // Replace decimal comma with dot } else { // US/Standard format: remove commas (thousands separator) normalizedPrice = cleaned.replace(/,/g, ''); } // Remove any remaining non-numeric characters except the decimal point normalizedPrice = normalizedPrice.replace(/[^\d.]/g, ''); // Validate format: should be digits with optional single decimal point if (!/^\d+(\.\d{1,2})?$/.test(normalizedPrice)) { return null; } const price = parseFloat(normalizedPrice); // Validate the parsed number if (!isFinite(price) || price < 0) { return null; } // Round to 2 decimal places to handle floating point precision return Math.round(price * 100) / 100; } // ============= Test Cases ============= const testCases = [ // Standard formats { input: '$1,234.56', expected: 1234.56 }, { input: '$ 1,234.56', expected: 1234.56 }, { input: 'USD 1,234.56', expected: 1234.56 }, { input: '1234.56', expected: 1234.56 }, { input: '99.99', expected: 99.99 }, // European format { input: 'EUR 99.90', expected: 99.9 }, { input: '1.234,56€', expected: 1234.56 }, { input: '€1.234,56', expected: 1234.56 }, { input: '1.234,56', expected: 1234.56 }, { input: '99,99€', expected: 99.99 }, { input: '1.000.000,50', expected: 1000000.5 }, // Other currencies { input: '£19.99', expected: 19.99 }, { input: '¥1000', expected: 1000 }, { input: '₹500.50', expected: 500.5 }, // Special cases (non-numeric prices) { input: 'free', expected: 0 }, { input: 'FREE', expected: 0 }, { input: 'Gratis', expected: 0 }, { input: '0', expected: 0 }, { input: 'TBD', expected: null }, { input: 'N/A', expected: null }, { input: 'contact us', expected: null }, { input: 'call for pricing', expected: null }, { input: 'quote required', expected: null }, { input: 'pending', expected: null }, // Edge cases { input: '', expected: null }, { input: ' ', expected: null }, { input: null as any, expected: null }, { input: 'abc', expected: null }, { input: '$', expected: null }, { input: 'price: unknown', expected: null }, { input: '-10', expected: null }, { input: '$1.234.567,89', expected: 1234567.89 }, // European format { input: '1,234,567.89', expected: 1234567.89 }, // US format { input: ' $99.99 ', expected: 99.99 }, // With whitespace { input: '$0.99', expected: 0.99 }, { input: '100', expected: 100 }, { input: '.50', expected: null }, // Invalid: no leading digit { input: '50.', expected: 50 }, // Valid: trailing dot ]; console.log('Running Price Parser Tests:\n'); let passed = 0; let failed = 0; testCases.forEach(({ input, expected }) => { const result = parsePrice(input); const isPass = result === expected; if (isPass) { passed++; console.log(`✓ parsePrice("${input}") = ${result}`); } else { failed++; console.log( `✗ parsePrice("${input}") = ${result}, expected ${expected}` ); } }); console.log(`\n${passed} passed, ${failed} failed`); ``` ## Key Features 1. **Multiple Format Support**: - US format: `$1,234.56` (comma as thousands, dot as decimal) - European format: `1.234,56€` (dot as thousands, comma as decimal) - Auto-detection based on last occurrence of separators 2. **Currency Handling**: - Removes symbols: `$`, `£`, `€`, `¥`, `₹`, `₽`, `₩`, `฿` - Removes currency codes: `USD`, `EUR`, `GBP`, etc. 3. **Special Cases**: - `"free"`, `"gratis"` → `0` - `"TBD"`, `"N/A"`, `"pending"` → `null` 4. **Edge Cases**: - Null/undefined inputs - Negative prices (invalid) - Non-finite numbers - Multiple decimal points - Floating-point precision handled via rounding 5. **Validation**: - Only accepts valid numeric formats - Limits decimal places to 2 - Ensures price ≥ 0 The function returns `number` for valid prices and `null` for invalid/non-numeric inputs.

Claude Sonnet 4.6

Unrated
27,367ms2127 tokens$0.030957
```typescript /** * Parses a price string and extracts a numeric value. * * Supported formats: * - US/Standard: "$1,234.56", "USD 1,234.56", "1,234.56" * - European: "1.234,56€", "EUR 1.234,56" * - Simple decimals: "99.9", "99,9" * - Non-numeric: "free", "TBD", "N/A" → returns null * * @param input - The price string to parse * @returns A numeric price or null if the string doesn't represent a valid price */ function parsePrice(input: string): number | null { // Handle null, undefined, or empty input if (!input || typeof input !== "string") { return null; } const trimmed = input.trim(); if (trimmed === "") { return null; } // Define non-numeric keywords that explicitly mean "no price" const NON_NUMERIC_KEYWORDS = /^(free|gratis|tbd|tba|n\/a|na|none|null|undefined|-)$/i; if (NON_NUMERIC_KEYWORDS.test(trimmed)) { return null; } // Strip known currency symbols and codes // Currency codes: USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, INR, etc. const CURRENCY_PATTERN = /(?:[A-Z]{3}\s*|\s*[A-Z]{3}|[$€£¥₹₽₩₪฿₴₦₨¢])/gi; let cleaned = trimmed.replace(CURRENCY_PATTERN, "").trim(); if (cleaned === "") { return null; } // Detect format by analyzing separators const result = detectAndParseNumber(cleaned); return result; } /** * Detects whether the number uses US or European formatting and parses accordingly. * * US format: 1,234,567.89 (comma = thousands, dot = decimal) * EU format: 1.234.567,89 (dot = thousands, comma = decimal) * Ambiguous: 1,234 or 1.234 (could be either — resolved by digit count after separator) */ function detectAndParseNumber(input: string): number | null { // Remove any remaining whitespace and trailing/leading punctuation const cleaned = input.trim(); // Must contain at least one digit if (!/\d/.test(cleaned)) { return null; } // Contains both dot and comma — determine which is the decimal separator if (cleaned.includes(".") && cleaned.includes(",")) { const lastDot = cleaned.lastIndexOf("."); const lastComma = cleaned.lastIndexOf(","); if (lastComma > lastDot) { // EU format: "1.234,56" — comma is decimal separator return parseEuropean(cleaned); } else { // US format: "1,234.56" — dot is decimal separator return parseUS(cleaned); } } // Only commas — could be thousands or decimal if (cleaned.includes(",") && !cleaned.includes(".")) { return parseCommaOnly(cleaned); } // Only dots — could be thousands or decimal if (cleaned.includes(".") && !cleaned.includes(",")) { return parseDotOnly(cleaned); } // No separators — plain integer return parseFloat(cleaned) || null; } /** Parse US format: commas as thousands separators, dot as decimal */ function parseUS(input: string): number | null { // Remove thousands commas const normalized = input.replace(/,/g, ""); const value = parseFloat(normalized); return isValidNumber(value) ? value : null; } /** Parse European format: dots as thousands separators, comma as decimal */ function parseEuropean(input: string): number | null { // Remove thousands dots, replace decimal comma with dot const normalized = input.replace(/\./g, "").replace(",", "."); const value = parseFloat(normalized); return isValidNumber(value) ? value : null; } /** * Handles ambiguous comma-only cases: * - "1,234" → 1234 (thousands separator — 3 digits after comma) * - "1,2345" → 12345 (thousands separator — 4+ digits after comma) * - "1,99" → 1.99 (decimal separator — 1-2 digits after comma) * - "1,9" → 1.9 (decimal separator — 1 digit) * - "1,234,567" → 1234567 (multiple commas = thousands) */ function parseCommaOnly(input: string): number | null { const parts = input.split(","); // Multiple commas — treat as thousands separators (US-like but with commas) if (parts.length > 2) { const normalized = input.replace(/,/g, ""); const value = parseFloat(normalized); return isValidNumber(value) ? value : null; } // Single comma const afterComma = parts[1] ?? ""; const digitCountAfterComma = afterComma.length; // Exactly 3 digits after comma = likely thousands separator if (digitCountAfterComma === 3) { const value = parseFloat(input.replace(",", "")); return isValidNumber(value) ? value : null; } // 1-2 digits after comma = likely decimal separator const value = parseFloat(input.replace(",", ".")); return isValidNumber(value) ? value : null; } /** * Handles ambiguous dot-only cases: * - "1.234" → 1234 (thousands separator — exactly 3 digits after dot) * - "1.99" → 1.99 (decimal separator — 2 digits) * - "1.9" → 1.9 (decimal separator) * - "1.234.567" → 1234567 (multiple dots = thousands) */ function parseDotOnly(input: string): number | null { const parts = input.split("."); // Multiple dots — treat as thousands separators (European-like) if (parts.length > 2) { const normalized = input.replace(/\./g, ""); const value = parseFloat(normalized); return isValidNumber(value) ? value : null; } // Single dot const afterDot = parts[1] ?? ""; const digitCountAfterDot = afterDot.length; // Exactly 3 digits after dot = ambiguous, but lean towards thousands separator // e.g., "1.234" in European context means 1234 // However, "0.234" or numbers starting with 0 are clearly decimals const beforeDot = parts[0] ?? ""; if (digitCountAfterDot === 3 && beforeDot !== "0" && beforeDot.length > 1) { const value = parseFloat(input.replace(".", "")); return isValidNumber(value) ? value : null; } // Otherwise treat as decimal separator (standard float) const value = parseFloat(input); return isValidNumber(value) ? value : null; } /** Guard against NaN, Infinity, and negative prices where inappropriate */ function isValidNumber(value: number): boolean { return !isNaN(value) && isFinite(value); } // ─── Tests ────────────────────────────────────────────────────────────────── function runTests(): void { const tests: Array<{ input: string; expected: number | null }> = [ // Standard US format { input: "$1,234.56", expected: 1234.56 }, { input: "USD 1,234.56", expected: 1234.56 }, { input: "$0.99", expected: 0.99 }, { input: "$

Cost & Performance Comparison

ModelLatencyTokensCostVerdict
Claude Opus 4.638,250ms2,433
GPT-4.110,111ms866
GPT-4.1 Mini18,718ms1,231
GPT-4.1 Nano7,903ms1,019
o324,018ms2,284
o3 Mini19,384ms2,785
o4 Mini27,743ms2,759
GPT-4o Mini39,831ms712$0.000393
GPT-4o10,219ms852$0.007950
Claude Haiku 4.511,671ms1,861$0.008989
Claude Sonnet 4.627,367ms2,127$0.030957
Edge Case Handling — Model Lab — AISpendGuard