Claude Opus 4.6
Unrated38,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 |