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.
This commit is contained in:
Dylan M. Taylor
2026-11-03 06:45:15 -05:00
committed by GitHub
parent 178f3eb745
commit 92cdc2a975
+51 -6
View File
@@ -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<String>, n: u32) -> Stri
pub(super) fn table_columns(t: &Table) -> Vec<String> {
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<String, u32> = 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("</xml>");
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<TableColumn> = (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::<HashSet<_>>().len(), 10_000);
}
}