mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-03 08:51:30 -06:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user