diff --git a/src/utils.rs b/src/utils.rs index b4f4248b..d70d685f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -65,20 +65,27 @@ pub fn read_f64(s: &[u8]) -> f64 { f64::from_le_bytes(s[..8].try_into().unwrap()) } -/// Push literal column into a String buffer -pub fn push_column(mut col: u32, buf: &mut String) { - if col < 26 { - buf.push((b'A' + col as u8) as char); - } else { - let mut rev = String::new(); - while col >= 26 { - let c = col % 26; - rev.push((b'A' + c as u8) as char); - col -= c; - col /= 26; - } - buf.extend(rev.chars().rev()); +/// Convert a 0-based column index to an Excel column name (0 -> "A", 26 -> "AA"). +pub fn push_column(col: u32, buf: &mut String) { + let mut digits = [0u8; 6]; + let len = column_name_digits(col, &mut digits); + for &d in &digits[..len] { + buf.push(d as char); + } +} + +/// Write the Excel letters for a 0-based column index into `out` (most +/// significant first) and return how many were written. +pub(crate) fn column_name_digits(col: u32, out: &mut [u8]) -> usize { + let mut num = col + 1; + let mut n = 0; + while num > 0 { + out[n] = b'A' + ((num - 1) % 26) as u8; + n += 1; + num = (num - 1) / 26; } + out[..n].reverse(); + n } // Utility function to unescape standard XML entities or character references @@ -1165,6 +1172,24 @@ pub const FTAB_ARGC: [u8; FTAB_LEN] = [ mod tests { use super::*; + #[test] + fn test_push_column() { + let check = |col: u32, expected: &str| { + let mut got = String::new(); + push_column(col, &mut got); + assert_eq!(got, expected, "push_column({col})"); + }; + check(0, "A"); + check(25, "Z"); + check(26, "AA"); + check(27, "AB"); + check(51, "AZ"); + check(53, "BB"); + check(701, "ZZ"); + check(702, "AAA"); + check(16383, "XFD"); + } + #[test] fn sound_to_u32() { let data = b"ABCDEFGH"; diff --git a/src/xls.rs b/src/xls.rs index 45bf0263..c788bd44 100644 --- a/src/xls.rs +++ b/src/xls.rs @@ -1773,11 +1773,27 @@ fn parse_formula( write!(&mut formula, "${row_last}").unwrap(); rgce = &rgce[6..]; } else { - formula.push('$'); - push_column(read_u16(&rgce[4..6]) as u32, &mut formula); - write!(&mut formula, "${}:$", read_u16(&rgce[0..2]) as u32 + 1).unwrap(); - push_column(read_u16(&rgce[6..8]) as u32, &mut formula); - write!(&mut formula, "${}", read_u16(&rgce[2..4]) as u32 + 1).unwrap(); + // columnFirst/columnLast are ColRelU: 14-bit column + fColRel/fRwRel flags. + let col_first = read_u16(&[rgce[4], rgce[5] & 0x3F]); + let col_last = read_u16(&[rgce[6], rgce[7] & 0x3F]); + let row_first = read_u16(&rgce[0..2]) as u32 + 1; + let row_last = read_u16(&rgce[2..4]) as u32 + 1; + if rgce[5] & 0x80 != 0x80 { + formula.push('$'); + } + push_column(col_first as u32, &mut formula); + if rgce[5] & 0x40 != 0x40 { + formula.push('$'); + } + write!(&mut formula, "{row_first}:").unwrap(); + if rgce[7] & 0x80 != 0x80 { + formula.push('$'); + } + push_column(col_last as u32, &mut formula); + if rgce[7] & 0x40 != 0x40 { + formula.push('$'); + } + write!(&mut formula, "{row_last}").unwrap(); rgce = &rgce[8..]; } } diff --git a/src/xlsx/mod.rs b/src/xlsx/mod.rs index bd3854ec..8b887fc2 100644 --- a/src/xlsx/mod.rs +++ b/src/xlsx/mod.rs @@ -3340,14 +3340,9 @@ pub(crate) fn column_number_to_name(num: u32, buf: &mut Vec) -> Result<(), X if num >= MAX_COLUMNS { return Err(XlsxError::ColumnNumberOverflow); } - let start = buf.len(); - let mut num = num + 1; - while num > 0 { - let integer = ((num - 1) % 26 + 65) as u8; - buf.push(integer); - num = (num - 1) / 26; - } - buf[start..].reverse(); + let mut digits = [0u8; 6]; + let len = crate::utils::column_name_digits(num, &mut digits); + buf.extend_from_slice(&digits[..len]); Ok(()) } diff --git a/tests/test.rs b/tests/test.rs index 76806d0c..6f4b4b83 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -3617,6 +3617,18 @@ fn xls_empty_string() { assert_eq!(range.get_value((0, 0)), Some(&String("".to_string()))); } +#[test] +fn xls_formula_columns_beyond_z() { + // Formula column references at/after column AA (index 26) exercise the + // column-name rendering and the BIFF8 PtgArea column masking. + let mut wb: Xls<_> = wb("xls_formula_columns_beyond_z.xls"); + let formula = wb.worksheet_formula("Sheet1").unwrap(); + let mut rows = formula.rows(); + assert_eq!(rows.next(), Some(&["SUM(AA1:AA3)".to_owned()][..])); + assert_eq!(rows.next(), Some(&["AA1+AB1".to_owned()][..])); + assert_eq!(rows.next(), None); +} + #[test] fn xls_embedded_cross_sheet_chart_does_not_leak_cells() { // Regression test: the chart on "Report" is a nested substream whose cached diff --git a/tests/xls_formula_columns_beyond_z.xls b/tests/xls_formula_columns_beyond_z.xls new file mode 100644 index 00000000..f83f8626 Binary files /dev/null and b/tests/xls_formula_columns_beyond_z.xls differ