mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 08:36:25 +00:00
fix(salary): parse localized numeric strings (#272)
This commit is contained in:
@@ -189,6 +189,46 @@ class DetectColumnTypeTests(unittest.TestCase):
|
|||||||
self.assertIn("salary_index", companies[0]["categories"])
|
self.assertIn("salary_index", companies[0]["categories"])
|
||||||
self.assertEqual(companies[0]["categories"]["salary_index"], {"index": 105.5})
|
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):
|
def test_parse_sheet_pairs_interleaved_count_index_columns_by_name(self):
|
||||||
ws = FakeWorksheet([
|
ws = FakeWorksheet([
|
||||||
|
|||||||
@@ -54,6 +54,25 @@ COMPOUND_PATTERNS = {"antal", "indeks", "løn", "gennemsnit", "medarbejdere"}
|
|||||||
ID_PATTERNS = {"id", "personnummer"}
|
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):
|
def header_matches(header, patterns):
|
||||||
"""Return True when a header contains a meaningful pattern match.
|
"""Return True when a header contains a meaningful pattern match.
|
||||||
|
|
||||||
@@ -211,12 +230,12 @@ def parse_sheet(ws, sheet_label=None):
|
|||||||
index_val = None
|
index_val = None
|
||||||
if cat["count_col"] < len(row) and row[cat["count_col"]] is not None:
|
if cat["count_col"] < len(row) and row[cat["count_col"]] is not None:
|
||||||
try:
|
try:
|
||||||
count_val = int(row[cat["count_col"]])
|
count_val = int(parse_numeric_cell(row[cat["count_col"]]))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
if cat["index_col"] < len(row) and row[cat["index_col"]] is not None:
|
if cat["index_col"] < len(row) and row[cat["index_col"]] is not None:
|
||||||
try:
|
try:
|
||||||
index_val = float(row[cat["index_col"]])
|
index_val = parse_numeric_cell(row[cat["index_col"]])
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
# A count/index pair that is entirely empty for this row carries
|
# 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:
|
if cat["value_col"] < len(row) and row[cat["value_col"]] is not None:
|
||||||
val = row[cat["value_col"]]
|
val = row[cat["value_col"]]
|
||||||
try:
|
try:
|
||||||
val = float(val)
|
val = parse_numeric_cell(val)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
# Non-numeric standalone value (e.g. a free-text "Notes"
|
# Non-numeric standalone value (e.g. a free-text "Notes"
|
||||||
# column) is not salary data; skip it for this row.
|
# column) is not salary data; skip it for this row.
|
||||||
|
|||||||
Reference in New Issue
Block a user