From 56747aeb5605d1b070de448410cfeffc860c88ca Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 31 Jul 2026 20:12:11 +0200 Subject: [PATCH 1/2] fix(utils): correct column-name rendering for indexes beyond Z push_column dropped the most-significant digit for columns after Z (e.g. index 26 rendered as "B"). Share one digit-fill implementation with the xlsx converter, without allocating a buffer per call. --- src/utils.rs | 51 ++++++++++++++++++++++++++++++++++++------------- src/xlsx/mod.rs | 11 +++-------- 2 files changed, 41 insertions(+), 21 deletions(-) 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/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(()) } From 060bed439bea0495681d701ba5cbe961c999a86d Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 31 Jul 2026 20:12:11 +0200 Subject: [PATCH 2/2] fix(xls): mask ColRelU column bits in area references PtgArea column fields pack 14-bit column numbers with fColRel/fRwRel flags; the old code read the raw 16 bits, so columns >= 64 (and the relative flags) were wrong. --- src/xls.rs | 26 ++++++++++++++++++++----- tests/test.rs | 12 ++++++++++++ tests/xls_formula_columns_beyond_z.xls | Bin 0 -> 5632 bytes 3 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 tests/xls_formula_columns_beyond_z.xls diff --git a/src/xls.rs b/src/xls.rs index 2bc5ca40..04d60952 100644 --- a/src/xls.rs +++ b/src/xls.rs @@ -1753,11 +1753,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/tests/test.rs b/tests/test.rs index 5dc12f30..c138d3fe 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -3616,3 +3616,15 @@ fn xls_empty_string() { let range = wb.worksheet_range("Sheet1").unwrap(); 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); +} diff --git a/tests/xls_formula_columns_beyond_z.xls b/tests/xls_formula_columns_beyond_z.xls new file mode 100644 index 0000000000000000000000000000000000000000..f83f862678ee629b5d0ab17883f1c2b0fce43b3a GIT binary patch literal 5632 zcmeHLO=uHA6#jPe(?pv#iK);8O02YM?WK6}(ry*CCrj0XB7#kAKxs)O^r8o?)*J=F zlj5b&iwCP-{6P=#=0T|FK@fyQkDdet57qU1Zxcy!2obTU%v<)&n>X)g_M4gAnSHq$ zD=pnmmeD{X@Zq}~MzBV2Q4Uw@QLx>0rCKdRl&LcG4MJc+z$ld+WXF2lw> z|1)ynJ?|4Zi}AT!HocCtSNo8$qz}X77H(0~fo|)LdCSvuP+W3(!f{ADdgeb#&K!XU$_0h0^CWR-~~?aJ3jFU z;>hW2c7A9L@=En+0}F4lM(Y-9L?>}O`7}CJj_Fj6(s%9lr&kB3CY+P1S*S#lncXLu z;o_2b(78jCXmX?|52(jZ<(I{VT3L!o9w=1M&3@j z@bS>2p27!b2j!Kq^7+Kd8)pf<{H#nfhg^#ov<9u4H-sKL)zacJmi+IOdoO|7KX%W#6CI} z$x=n1gt0V?gp)AR-`X%@!L&>yXph)&JP;F%3agO#8ZrnN1PlTO0fT@+z#w1{FbMoH z1Z1x(yHMF8%aI_!TlT^7GeG|9eB&*pq5l(s!!9AxYyb6Sokx)^rNn^S;adqHnV88?U(b{K7Xr8n%MnT{vU|F B#1H@g literal 0 HcmV?d00001