{"record":{"id":"3d7907b79ca6c64f","repo":"MadsLorentzen/ai-job-search","slug":"ambiguous-dot-separator","errorCode":null,"errorMessage":"ambiguous dot separator","messagePattern":"ambiguous dot separator","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"tools/convert_salary_excel.py","lineNumber":81,"sourceCode":"\n    text = value.strip().replace(\"\\u00a0\", \" \").replace(\" \", \"\")\n    if not text:\n        raise ValueError(\"not numeric\")\n    if \",\" in text and \".\" in text:\n        # The separator that appears last is the decimal separator: European\n        # \"1.234,56\" and US \"1,234.56\" are both unambiguous here, unlike the\n        # single-separator cases below.\n        if text.rfind(\",\") > text.rfind(\".\"):\n            text = text.replace(\".\", \"\").replace(\",\", \".\")\n        else:\n            text = text.replace(\",\", \"\")\n    elif \",\" in text:\n        if re.fullmatch(r\"[+-]?\\d+,\\d{3}\", text):\n            raise ValueError(\"ambiguous comma separator\")\n        text = text.replace(\",\", \".\")\n    elif \".\" in text:\n        if re.fullmatch(r\"[+-]?\\d+\\.\\d{3}\", text):\n            raise ValueError(\"ambiguous dot separator\")\n    return float(text)\n\n\ndef header_matches(header, patterns):\n    \"\"\"Return True when a header contains a meaningful pattern match.\n\n    Patterns match whole tokens; any pattern also listed in\n    ``COMPOUND_PATTERNS`` may additionally match as a substring, to handle\n    languages that form compound words.\n    \"\"\"\n    h = header.lower().strip()\n    tokens = set(re.findall(r\"[a-zæøåöäü0-9]+\", h))\n\n    for p in patterns:\n        if p in tokens:\n            return True\n        if p in COMPOUND_PATTERNS and p in h:\n            return True","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/MadsLorentzen/ai-job-search/blob/79cd383e58f0af7948c7c6462a3a289e9b67421e/tools/convert_salary_excel.py#L63-L99","documentation":"Raised when a string contains exactly one dot in the pattern digits.3-digits (e.g. '1.234'). This is ambiguous between a US decimal ('1.234' = 1.234) and a European thousands separator ('1.234' = 1234), so the function refuses to guess and raises rather than risking a 1000x misinterpretation.","triggerScenarios":"Passing a string that fullmatches r'[+-]?\\d+\\.\\d{3}' such as '1.234', '65.500', or '2.000'. Typical for European-formatted salaries ('2.000' meaning two thousand) or US decimals with exactly three fraction digits ('1.234').","commonSituations":"Danish/European Excel exports where '.' is the thousands separator ('55.000' kr), or US sheets with values like '0.125'. Without a second separator or locale hint the format cannot be inferred from the string alone.","solutions":["Apply the correct locale transform first: European sheets -> remove the dot ('1.234' -> '1234'); US sheets with 3-decimal fractions -> keep as-is after confirming intent","Normalize the source column to include a decimal comma/dot that disambiguates (e.g. '1.234,5' or '1,234.5')","Catch the ValueError, log the raw cell and row, and resolve via a locale flag or manual review"],"exampleFix":"// before\nnum = parse_numeric_cell('55.000')  # raises: 55000 or 55.0?\n\n after\nnum = parse_numeric_cell('55.000'.replace('.', ''))  # European sheet: 55000.0","handlingStrategy":"try-catch","validationCode":"import re\nAMBIG = re.compile(r'[+-]?\\d+\\.\\d{3}$')\nif isinstance(v, str) and AMBIG.fullmatch(v.strip()):\n    v = v.replace('.', '' if locale == 'eu' else '.')  # eu: thousands sep","typeGuard":"def is_ambiguous_dot(v) -> bool:\n    return isinstance(v, str) and bool(re.fullmatch(r'[+-]?\\d+\\.\\d{3}', v.strip()))","tryCatchPattern":"try:\n    num = parse_numeric_cell(v)\nexcept ValueError as e:\n    if 'ambiguous dot' in str(e):\n        raise LocaleNeeded(v)\n    raise","preventionTips":["For European sheets strip dots used as thousands separators beforehand","Require source values to carry a disambiguating second separator","Log ambiguous values and review the magnitude against expectations (a 1000x error is the risk)"],"tags":["python","excel","locale","number-formatting","ambiguity"],"backgroundTag":"locale-number-parsing","analyzedSha":"79cd383e58f0af7948c7c6462a3a289e9b67421e","analyzedAt":"2026-08-27T21:51:12.330Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}