From 000ebf82bee04367456229290a3f4ac79d90eda7 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 14 Jul 2026 09:20:56 +0000 Subject: [PATCH] =?UTF-8?q?Sanitise=20and=20deduplicate=20scenario=20sheet?= =?UTF-8?q?=20names=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../xlsx/scenario_export_workbook.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/infrastructure/xlsx/scenario_export_workbook.py b/infrastructure/xlsx/scenario_export_workbook.py index f86614a08..4f971d050 100644 --- a/infrastructure/xlsx/scenario_export_workbook.py +++ b/infrastructure/xlsx/scenario_export_workbook.py @@ -8,6 +8,7 @@ dynamic number of scenario sheets is handled natively. from __future__ import annotations +import re from io import BytesIO from typing import Sequence @@ -15,6 +16,24 @@ from openpyxl import Workbook from domain.scenario_export.scenario_sheet import ExportSheet +# Excel forbids these in a sheet title and caps titles at 31 characters. +_ILLEGAL_SHEET_CHARS = re.compile(r"[:\\/?*\[\]]") +_MAX_SHEET_NAME = 31 + + +def _safe_sheet_name(name: str, used: set[str]) -> str: + """An Excel-legal, unique sheet name: illegal characters stripped, capped at + 31 chars, and suffixed on collision (two scenarios can share a name).""" + clean = _ILLEGAL_SHEET_CHARS.sub("", name or "scenario").strip() or "scenario" + clean = clean[:_MAX_SHEET_NAME] + base, index = clean, 1 + while clean in used: + suffix = f" ({index})" + clean = base[: _MAX_SHEET_NAME - len(suffix)] + suffix + index += 1 + used.add(clean) + return clean + def render_workbook(sheets: Sequence[tuple[str, ExportSheet]]) -> bytes: """Render the scenario sheets into a single branded ``.xlsx`` as bytes. Each @@ -25,8 +44,9 @@ def render_workbook(sheets: Sequence[tuple[str, ExportSheet]]) -> bytes: if default is not None: workbook.remove(default) + used_names: set[str] = set() for name, sheet in sheets: - worksheet = workbook.create_sheet(title=name) + worksheet = workbook.create_sheet(title=_safe_sheet_name(name, used_names)) worksheet.append(list(sheet.columns)) for row in sheet.rows: worksheet.append([row.get(column, "") for column in sheet.columns])