diff --git a/CHANGELOG.md b/CHANGELOG.md index 12c2192d..2476e3bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,12 @@ per-file diff commands. "newly expired" by the sweep, then re-ranked by `apply` from its fresh deadline, so the same job appeared as both expired and shortlisted. Keys are now stripped before matching. +- **Salary workbook conversion handles wrapped header cells** (#643, `tools/convert_salary_excel.py`, + `tests/test_salary_wrapped_header_cli.py`) - a header cell with a line break inside it + ("Median⏎salary", "Løn-⏎indeks") kept the break in the derived category name, so the column no + longer paired with its count column and `salary_lookup.py` printed the label across two lines. + Header whitespace runs now collapse to one space, so a wrapped header converts like its one-line spelling. + ## [1.7.3] - 2026-10-19 ### Added diff --git a/tests/test_salary_wrapped_header_cli.py b/tests/test_salary_wrapped_header_cli.py new file mode 100644 index 00000000..a437c3bb --- /dev/null +++ b/tests/test_salary_wrapped_header_cli.py @@ -0,0 +1,50 @@ +"""Wrapped workbook header cells must convert like their one-line spelling.""" +import json +from pathlib import Path +import subprocess +import sys +from tempfile import TemporaryDirectory +import unittest + +try: + import openpyxl +except ImportError: + openpyxl = None + + +TOOL = Path(__file__).resolve().parent.parent / "tools" / "convert_salary_excel.py" + + +@unittest.skipUnless(openpyxl, "requires optional openpyxl (installed in CI)") +class WrappedHeaderCLI(unittest.TestCase): + def convert(self, header): + with TemporaryDirectory() as directory: + root = Path(directory) + workbook, output = root / "salary.xlsx", root / "salary_data.json" + book = openpyxl.Workbook() + try: + book.active.append(header) + book.active.append(["Acme", "Aarhus", 12, 104.5]) + book.save(workbook) + finally: + book.close() + result = subprocess.run( + [sys.executable, str(TOOL), str(workbook), "--output", str(output)], + cwd=root, capture_output=True, text=True, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + return json.loads(output.read_text(encoding="utf-8"))["companies"][0]["categories"] + + def test_wrapped_headers_pair_like_their_single_line_spelling(self): + expected = {"all_employees": {"count": 12, "index": 104.5}} + self.assertEqual(self.convert(["Company", "City", "Employees", "Median salary"]), expected) + self.assertEqual(self.convert(["Company", "City", "Employees", "Median\nsalary"]), expected) + self.assertEqual(self.convert(["Firma", "By", "Antal\nmedarbejdere", "Løn-\nindeks"]), expected) + + def test_wrapped_category_name_has_no_line_break(self): + categories = self.convert(["Company", "City", "Count\nengineers", "Index\nengineers"]) + self.assertEqual(categories, {"engineers": {"count": 12, "index": 104.5}}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/convert_salary_excel.py b/tools/convert_salary_excel.py index 3d4822cf..065365a8 100644 --- a/tools/convert_salary_excel.py +++ b/tools/convert_salary_excel.py @@ -186,10 +186,12 @@ def parse_sheet(ws, sheet_label=None): print(f"Warning: Could not find header row in sheet '{ws.title}'. Skipping.", file=sys.stderr) return [] - # Read headers + # Read headers. Wrapped header cells carry a line break ("Median\nsalary"), + # which would survive into the category name and keep the column from + # pairing with its count, so collapse every whitespace run to one space. headers = [] for cell in ws[header_row]: - headers.append(str(cell.value).strip() if cell.value else "") + headers.append(" ".join(str(cell.value).split()) if cell.value else "") # Find company and city columns company_col = None