From 92cdc2a975b6e014da6ff25477232b500e3c1cf6 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 06:45:15 -0500 Subject: [PATCH] xlsx: rename repeated table headers without retrying handed-out suffixes (#3213) table_columns restarted its suffix search at 2 for every duplicate header, lowercasing and hashing every candidate already in use, so a table of repeated headers was exported in quadratic time. The next suffix to try is now remembered per lowercased base. --- crates/xlsx/src/sheet_write/parts.rs | 57 +++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/crates/xlsx/src/sheet_write/parts.rs b/crates/xlsx/src/sheet_write/parts.rs index 2e40d0b26..040e2b9bd 100644 --- a/crates/xlsx/src/sheet_write/parts.rs +++ b/crates/xlsx/src/sheet_write/parts.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use super::*; /// Valid table name: letters, digits, `_` and `.`, starting with a letter, `_` or `\` @@ -21,16 +23,24 @@ pub(super) fn table_name(name: &str, used: &mut HashSet, n: u32) -> Stri pub(super) fn table_columns(t: &Table) -> Vec { let w = t.range.width() as usize; let mut used = HashSet::new(); + // The next suffix to try per lowercased base, so repeated headers don't retry every suffix + // already handed out. + let mut next: HashMap = HashMap::new(); (0..w) .map(|i| { let base = t.columns.get(i).map(|c| c.name.clone()).filter(|n| !n.trim().is_empty()).unwrap_or_else(|| format!("Column{}", i + 1)); - let mut name = base.clone(); - let mut k = 2; - while !used.insert(name.to_lowercase()) { - name = format!("{base}{k}"); - k += 1; + let lower = base.to_lowercase(); + if used.insert(lower.clone()) { + return base; + } + let k = next.entry(lower).or_insert(2); + loop { + let name = format!("{base}{k}"); + *k += 1; + if used.insert(name.to_lowercase()) { + return name; + } } - name }) .collect() } @@ -371,3 +381,38 @@ pub(super) fn vml_xml(sheet: &Sheet, n: u32) -> String { s.push_str(""); s } + +#[cfg(test)] +mod tests { + use gridcraft_model::TableColumn; + + use super::*; + + #[test] + fn repeated_headers_are_renamed_without_retrying_suffixes() { + let col = |n: &str| TableColumn { name: n.into(), totals: TotalsFn::None, totals_label: None, formula: None, unique_name: None, xml: None }; + let mut columns: Vec = (0..10_000).map(|_| col("Total")).collect(); + columns[1] = col("total3"); + let t = Table { + id: 1, + name: "T".into(), + range: RangeRef::parse("A1:NTP2").unwrap(), + header_row: true, + totals_row: false, + columns, + style: String::new(), + banded_rows: false, + banded_cols: false, + first_col: false, + last_col: false, + filter_button: false, + filter: vec![], + table_type: None, + connection_id: None, + }; + let names = table_columns(&t); + assert_eq!(names.len(), 10_000); + assert_eq!(&names[..4], ["Total", "total3", "Total2", "Total4"]); + assert_eq!(names.iter().map(|n| n.to_lowercase()).collect::>().len(), 10_000); + } +}