diff --git a/tests/test_convert_salary_excel.py b/tests/test_convert_salary_excel.py index 5b6a858..1b954c2 100644 --- a/tests/test_convert_salary_excel.py +++ b/tests/test_convert_salary_excel.py @@ -189,6 +189,46 @@ class DetectColumnTypeTests(unittest.TestCase): self.assertIn("salary_index", companies[0]["categories"]) self.assertEqual(companies[0]["categories"]["salary_index"], {"index": 105.5}) + def test_parse_sheet_accepts_comma_decimal_string_values(self): + # Locale-formatted Excel exports can carry numeric cells as strings. + # Danish decimal commas must not be silently dropped by float(). + ws = FakeWorksheet([ + ("Company", "Engineering Count", "Engineering Index"), + ("Example Corp", "12,0", "108,5"), + ]) + + companies = parse_sheet(ws) + + self.assertEqual( + companies[0]["categories"]["engineering"], + {"count": 12, "index": 108.5}, + ) + + def test_parse_sheet_accepts_danish_thousands_and_decimal_string(self): + ws = FakeWorksheet([ + ("Company", "Salary Index"), + ("Example Corp", "1.234,5"), + ]) + + companies = parse_sheet(ws) + + self.assertEqual( + companies[0]["categories"]["salary_index"], + {"index": 1234.5}, + ) + + def test_parse_sheet_skips_ambiguous_single_comma_thousands_string(self): + # In an English-locale export, "1,234" is probably 1234, but in a + # decimal-comma locale it could be 1.234. Preserve the old safe-skip + # behavior instead of guessing and writing a 1000x-wrong salary value. + ws = FakeWorksheet([ + ("Company", "Salary Index"), + ("Example Corp", "1,234"), + ]) + + companies = parse_sheet(ws) + + self.assertEqual(companies[0]["categories"], {}) def test_parse_sheet_pairs_interleaved_count_index_columns_by_name(self): ws = FakeWorksheet([ diff --git a/tools/convert_salary_excel.py b/tools/convert_salary_excel.py index be190ca..f334ef4 100644 --- a/tools/convert_salary_excel.py +++ b/tools/convert_salary_excel.py @@ -54,6 +54,25 @@ COMPOUND_PATTERNS = {"antal", "indeks", "løn", "gennemsnit", "medarbejdere"} ID_PATTERNS = {"id", "personnummer"} +def parse_numeric_cell(value): + """Parse numeric Excel values, including localized string cells.""" + if isinstance(value, (int, float)): + return float(value) + if not isinstance(value, str): + raise ValueError("not numeric") + + text = value.strip().replace("\u00a0", " ").replace(" ", "") + if not text: + raise ValueError("not numeric") + if "," in text and "." in text: + text = text.replace(".", "").replace(",", ".") + elif "," in text: + if re.fullmatch(r"[+-]?\d+,\d{3}", text): + raise ValueError("ambiguous comma separator") + text = text.replace(",", ".") + return float(text) + + def header_matches(header, patterns): """Return True when a header contains a meaningful pattern match. @@ -211,12 +230,12 @@ def parse_sheet(ws, sheet_label=None): index_val = None if cat["count_col"] < len(row) and row[cat["count_col"]] is not None: try: - count_val = int(row[cat["count_col"]]) + count_val = int(parse_numeric_cell(row[cat["count_col"]])) except (ValueError, TypeError): pass if cat["index_col"] < len(row) and row[cat["index_col"]] is not None: try: - index_val = float(row[cat["index_col"]]) + index_val = parse_numeric_cell(row[cat["index_col"]]) except (ValueError, TypeError): pass # A count/index pair that is entirely empty for this row carries @@ -228,7 +247,7 @@ def parse_sheet(ws, sheet_label=None): if cat["value_col"] < len(row) and row[cat["value_col"]] is not None: val = row[cat["value_col"]] try: - val = float(val) + val = parse_numeric_cell(val) except (ValueError, TypeError): # Non-numeric standalone value (e.g. a free-text "Notes" # column) is not salary data; skip it for this row.