{"record":{"id":"32d4dc84d1bd315e","repo":"MadsLorentzen/ai-job-search","slug":"not-numeric","errorCode":null,"errorMessage":"not numeric","messagePattern":"not numeric","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"tools/convert_salary_excel.py","lineNumber":62,"sourceCode":"INDEX_PATTERNS = {\"indeks\", \"index\", \"idx\", \"salary\", \"løn\", \"median\", \"average\", \"gennemsnit\"}\n# \"Compound\" tokens: pattern words allowed to match as a substring of a larger\n# header token, for languages that glue words together (e.g. Danish \"lønindeks\"\n# -> løn + indeks). Languages that write headers as separate words need none.\n# Ships populated for this repo's Danish demonstration data; a fork targeting\n# another locale edits this constant.\nCOMPOUND_PATTERNS = {\"antal\", \"indeks\", \"løn\", \"gennemsnit\", \"medarbejdere\"}\n# Identifier columns (employee id, Danish \"personnummer\", etc.) are never salary\n# data. They are dropped at classification so they are not mistaken for a salary\n# category. Matched as whole tokens only, like other pattern sets.\nID_PATTERNS = {\"id\", \"personnummer\"}\n\n\ndef parse_numeric_cell(value):\n    \"\"\"Parse numeric Excel values, including localized string cells.\"\"\"\n    if isinstance(value, (int, float)):\n        return float(value)\n    if not isinstance(value, str):\n        raise ValueError(\"not numeric\")\n\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):","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/MadsLorentzen/ai-job-search/blob/79cd383e58f0af7948c7c6462a3a289e9b67421e/tools/convert_salary_excel.py#L44-L80","documentation":"Raised by parse_numeric_cell when the value passed is neither a number nor a string (e.g. None, datetime, bool-handled elsewhere), so no numeric conversion is possible. The function only accepts int/float directly or str after localization normalization. Any other cell type is rejected as non-numeric.","triggerScenarios":"Calling parse_numeric_cell with a None (empty Excel cell), a datetime.datetime object, a bool, or a cell object from openpyxl/xlrd that wasn't unwrapped to a raw value first. Happens via parse_sheet when a spreadsheet column contains blanks or dates.","commonSituations":"Empty cells in a salary column read with openpyxl (which yields None for blanks), date-formatted cells, or forgetting to use .value on cell objects. Also pandas NaN leaking through if the sheet was loaded with na values kept.","solutions":["Skip or default empty/None cells before calling parse_numeric_cell (e.g. `if value is None or value == '': continue`)","Convert datetimes to numbers or filter them before parsing","If reading via openpyxl, ensure you pass cell.value, not the Cell object","Wrap the call in try/except ValueError and report the offending row"],"exampleFix":"// before\nnum = parse_numeric_cell(row['salary'])\n\n// after\nif row['salary'] is None:\n    continue\nnum = parse_numeric_cell(row['salary'])","handlingStrategy":"validation","validationCode":"raw = row['salary']\nif raw is None or isinstance(raw, (str, int, float)) is False:\n    continue  # or: raw = raw.value if hasattr(raw, 'value') else None\nif isinstance(raw, str) and not raw.strip():\n    continue","typeGuard":"def is_parseable_cell(v) -> bool:\n    return v is None or isinstance(v, (int, float, str))","tryCatchPattern":"try:\n    num = parse_numeric_cell(value)\nexcept ValueError as e:\n    logger.warning('skipping non-numeric cell %r: %s', value, e)\n    continue","preventionTips":["Always unwrap openpyxl cells to .value before parsing","Skip None cells in the sheet loop rather than letting them reach the parser","Log raw values on failure so bad rows are traceable"],"tags":["python","excel","parsing","type-error","valueerror"],"backgroundTag":"type-conversion-failed","analyzedSha":"79cd383e58f0af7948c7c6462a3a289e9b67421e","analyzedAt":"2026-08-27T21:51:12.330Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}