1use std::fmt;
7
8use serde::{Deserialize, Serialize};
9
10pub const MAX_ROWS: u32 = 1_048_576;
11pub const MAX_COLS: u32 = 16_384;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, Default)]
15pub struct CellRef {
16 pub row: u32,
17 pub col: u32,
18}
19
20impl CellRef {
21 pub const fn new(row: u32, col: u32) -> Self {
22 CellRef { row, col }
23 }
24 pub fn a1(&self) -> String {
26 format!("{}{}", col_to_letters(self.col), self.row as u64 + 1)
27 }
28 pub fn is_valid(&self) -> bool {
29 self.row < MAX_ROWS && self.col < MAX_COLS
30 }
31 pub fn offset(&self, dr: i64, dc: i64) -> Option<CellRef> {
33 let r = (self.row as i64).checked_add(dr)?;
34 let c = (self.col as i64).checked_add(dc)?;
35 if (0..MAX_ROWS as i64).contains(&r) && (0..MAX_COLS as i64).contains(&c) { Some(CellRef::new(r as u32, c as u32)) } else { None }
36 }
37 pub fn offset_clamped(&self, dr: i64, dc: i64) -> CellRef {
39 let r = (self.row as i64).saturating_add(dr).clamp(0, MAX_ROWS as i64 - 1);
40 let c = (self.col as i64).saturating_add(dc).clamp(0, MAX_COLS as i64 - 1);
41 CellRef::new(r as u32, c as u32)
42 }
43 pub fn parse(s: &str) -> Option<CellRef> {
45 let (c, r, _, _, rest) = parse_a1_prefix(s)?;
46 if !rest.is_empty() {
47 return None;
48 }
49 Some(CellRef::new(r, c))
50 }
51}
52
53impl fmt::Display for CellRef {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 write!(f, "{}{}", col_to_letters(self.col), self.row as u64 + 1)
56 }
57}
58
59pub fn parse_a1_prefix(s: &str) -> Option<(u32, u32, bool, bool, &str)> {
61 let b = s.as_bytes();
62 let mut i = 0;
63 let col_abs = b.first() == Some(&b'$');
64 if col_abs {
65 i += 1;
66 }
67 let cs = i;
68 while i < b.len() && b[i].is_ascii_alphabetic() {
69 i += 1;
70 }
71 if i == cs || i - cs > 3 {
72 return None;
73 }
74 let col = letters_to_col(s.get(cs..i)?)?;
75 let row_abs = b.get(i) == Some(&b'$');
76 if row_abs {
77 i += 1;
78 }
79 let rs = i;
80 while i < b.len() && b[i].is_ascii_digit() {
81 i += 1;
82 }
83 if i == rs || i - rs > 7 {
84 return None;
85 }
86 let row: u32 = s.get(rs..i)?.parse().ok()?;
87 if row == 0 || row > MAX_ROWS {
88 return None;
89 }
90 Some((col, row - 1, col_abs, row_abs, s.get(i..)?))
91}
92
93pub fn col_to_letters(col: u32) -> String {
95 let mut n = col as u64 + 1;
96 let mut out = Vec::with_capacity(3);
97 while n > 0 {
98 let rem = ((n - 1) % 26) as u8;
99 out.push(b'A' + rem);
100 n = (n - 1) / 26;
101 }
102 out.reverse();
103 String::from_utf8(out).unwrap_or_default()
104}
105
106pub fn letters_to_col(s: &str) -> Option<u32> {
108 if s.is_empty() || s.len() > 3 {
109 return None;
110 }
111 let mut n: u32 = 0;
112 for ch in s.bytes() {
113 if !ch.is_ascii_alphabetic() {
114 return None;
115 }
116 n = n * 26 + (ch.to_ascii_uppercase() - b'A' + 1) as u32;
117 }
118 if n == 0 || n > MAX_COLS {
119 return None;
120 }
121 Some(n - 1)
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
126pub struct RangeRef {
127 pub start: CellRef,
128 pub end: CellRef,
129}
130
131impl RangeRef {
132 pub fn new(a: CellRef, b: CellRef) -> Self {
133 RangeRef { start: CellRef::new(a.row.min(b.row), a.col.min(b.col)), end: CellRef::new(a.row.max(b.row), a.col.max(b.col)) }
134 }
135 pub fn cell(c: CellRef) -> Self {
136 RangeRef { start: c, end: c }
137 }
138 pub fn rows(r0: u32, r1: u32) -> Self {
140 RangeRef::new(CellRef::new(r0, 0), CellRef::new(r1, MAX_COLS - 1))
141 }
142 pub fn cols(c0: u32, c1: u32) -> Self {
144 RangeRef::new(CellRef::new(0, c0), CellRef::new(MAX_ROWS - 1, c1))
145 }
146 pub fn all() -> Self {
147 RangeRef::new(CellRef::new(0, 0), CellRef::new(MAX_ROWS - 1, MAX_COLS - 1))
148 }
149 pub fn height(&self) -> u32 {
150 self.end.row - self.start.row + 1
151 }
152 pub fn width(&self) -> u32 {
153 self.end.col - self.start.col + 1
154 }
155 pub fn count(&self) -> u64 {
156 self.height() as u64 * self.width() as u64
157 }
158 pub fn is_single(&self) -> bool {
159 self.start == self.end
160 }
161 pub fn is_full_rows(&self) -> bool {
162 self.start.col == 0 && self.end.col == MAX_COLS - 1
163 }
164 pub fn is_full_cols(&self) -> bool {
165 self.start.row == 0 && self.end.row == MAX_ROWS - 1
166 }
167 pub fn contains(&self, c: CellRef) -> bool {
168 c.row >= self.start.row && c.row <= self.end.row && c.col >= self.start.col && c.col <= self.end.col
169 }
170 pub fn contains_range(&self, o: &RangeRef) -> bool {
171 self.contains(o.start) && self.contains(o.end)
172 }
173 pub fn intersects(&self, o: &RangeRef) -> bool {
174 self.start.row <= o.end.row && o.start.row <= self.end.row && self.start.col <= o.end.col && o.start.col <= self.end.col
175 }
176 pub fn intersection(&self, o: &RangeRef) -> Option<RangeRef> {
177 if !self.intersects(o) {
178 return None;
179 }
180 Some(RangeRef {
181 start: CellRef::new(self.start.row.max(o.start.row), self.start.col.max(o.start.col)),
182 end: CellRef::new(self.end.row.min(o.end.row), self.end.col.min(o.end.col)),
183 })
184 }
185 pub fn union(&self, o: &RangeRef) -> RangeRef {
186 RangeRef {
187 start: CellRef::new(self.start.row.min(o.start.row), self.start.col.min(o.start.col)),
188 end: CellRef::new(self.end.row.max(o.end.row), self.end.col.max(o.end.col)),
189 }
190 }
191 pub fn top_left(ranges: &[RangeRef]) -> Option<CellRef> {
195 ranges.iter().copied().reduce(|a, b| a.union(&b)).map(|r| r.start)
196 }
197 pub fn iter(&self) -> impl Iterator<Item = CellRef> + '_ {
199 let (r0, r1, c0, c1) = (self.start.row, self.end.row, self.start.col, self.end.col);
200 (r0..=r1).flat_map(move |r| (c0..=c1).map(move |c| CellRef::new(r, c)))
201 }
202 pub fn a1(&self) -> String {
204 if self.is_full_rows() && !self.is_full_cols() {
205 return format!("{}:{}", self.start.row + 1, self.end.row + 1);
206 }
207 if self.is_full_cols() && !self.is_full_rows() {
208 return format!("{}:{}", col_to_letters(self.start.col), col_to_letters(self.end.col));
209 }
210 if self.is_single() { self.start.a1() } else { format!("{}:{}", self.start.a1(), self.end.a1()) }
211 }
212 pub fn parse(s: &str) -> Option<RangeRef> {
214 let s = s.trim();
215 if let Some((a, b)) = s.split_once(':') {
216 if let (Some(x), Some(y)) = (CellRef::parse(a), CellRef::parse(b)) {
217 return Some(RangeRef::new(x, y));
218 }
219 let strip = |t: &str| t.strip_prefix('$').unwrap_or(t).to_string();
220 if let (Some(x), Some(y)) = (letters_to_col(&strip(a)), letters_to_col(&strip(b))) {
221 return Some(RangeRef::cols(x.min(y), x.max(y)));
222 }
223 let row_num = |t: &str| {
224 let t = t.strip_prefix('$').unwrap_or(t);
225 if t.is_empty() || !t.bytes().all(|c| c.is_ascii_digit()) {
226 return None;
227 }
228 t.parse::<u32>().ok()
229 };
230 let (pa, pb) = (row_num(a)?, row_num(b)?);
231 if pa == 0 || pb == 0 || pa > MAX_ROWS || pb > MAX_ROWS {
232 return None;
233 }
234 return Some(RangeRef::rows(pa.min(pb) - 1, pa.max(pb) - 1));
235 }
236 CellRef::parse(s).map(RangeRef::cell)
237 }
238}
239
240impl fmt::Display for RangeRef {
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.write_str(&self.a1())
243 }
244}
245
246fn is_r1c1_token(name: &str) -> bool {
249 let mut chars = name.chars().peekable();
250 let mut letters = 0;
251 for letter in ['r', 'c'] {
252 if chars.peek().is_some_and(|c| c.to_ascii_lowercase() == letter) {
253 chars.next();
254 letters += 1;
255 while chars.peek().is_some_and(char::is_ascii_digit) {
256 chars.next();
257 }
258 }
259 }
260 letters > 0 && chars.next().is_none()
261}
262
263pub fn sheet_needs_quotes(name: &str) -> bool {
267 let Some(first) = name.chars().next() else {
268 return true;
269 };
270 if first.is_ascii_digit() || first == '.' || name.chars().any(|c| !(c.is_alphanumeric() || c == '_' || c == '.')) {
271 return true;
272 }
273 CellRef::parse(name).is_some() || is_r1c1_token(name) || name.eq_ignore_ascii_case("TRUE") || name.eq_ignore_ascii_case("FALSE")
274}
275
276pub fn quote_sheet(name: &str) -> String {
278 if sheet_needs_quotes(name) { format!("'{}'", name.replace('\'', "''")) } else { name.to_string() }
279}
280
281pub fn unquote_sheet(text: &str) -> String {
284 match text.trim().strip_prefix('\'').and_then(|t| t.strip_suffix('\'')) {
285 Some(inner) => inner.replace("''", "'"),
286 None => text.trim().to_string(),
287 }
288}
289
290pub fn split_sheet(reference: &str) -> (Option<String>, &str) {
293 match reference.rsplit_once('!') {
294 Some((sheet, body)) => (Some(unquote_sheet(sheet)), body),
295 None => (None, reference),
296 }
297}
298
299pub fn split_areas(reference: &str) -> Vec<&str> {
304 let mut areas = Vec::new();
305 let (mut start, mut quoted) = (0, false);
306 for (i, c) in reference.char_indices() {
307 match c {
308 '\'' => quoted = !quoted,
309 ',' if !quoted => {
310 areas.push(reference.get(start..i).unwrap_or_default());
311 start = i.saturating_add(1);
312 }
313 _ => {}
314 }
315 }
316 areas.push(reference.get(start..).unwrap_or_default());
317 areas
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn areas_split_only_at_commas_outside_quoted_sheet_names() {
326 assert_eq!(split_areas("B2:B4,D1"), ["B2:B4", "D1"]);
327 assert_eq!(split_areas("'Sales, West'!$A$1"), ["'Sales, West'!$A$1"]);
328 assert_eq!(split_areas("'O''Brien, Jr'!A1:B2,D4"), ["'O''Brien, Jr'!A1:B2", "D4"]);
329 assert_eq!(split_areas("'Q1, Q2'!A1,'Q3, Q4'!B2"), ["'Q1, Q2'!A1", "'Q3, Q4'!B2"]);
330 assert_eq!(split_areas(""), [""]);
331 }
332
333 #[test]
334 fn range_parse_rejects_signed_rows() -> Result<(), std::io::Error> {
335 assert!(RangeRef::parse("+1:+3").is_none());
336 assert!(RangeRef::parse("-1:3").is_none());
337 assert!(RangeRef::parse("$$1:3").is_none());
338 assert!(RangeRef::parse(":3").is_none());
339 assert!(RangeRef::parse("$$A:$$C").is_none());
340 assert!(RangeRef::parse("$A:$C").is_some());
341 let r = RangeRef::parse("$1:$3").ok_or_else(|| std::io::Error::other("valid row range"))?;
342 assert_eq!((r.start.row, r.end.row), (0, 2));
343 Ok(())
344 }
345
346 #[test]
347 fn top_left_spans_every_area_in_any_order() {
348 let areas: Vec<RangeRef> = ["H62:H68", "H26:H27", "H93:H95"].iter().filter_map(|a| RangeRef::parse(a)).collect();
349 assert_eq!(RangeRef::top_left(&areas), CellRef::parse("H26"));
350 let areas: Vec<RangeRef> = ["C5:D6", "E2", "B9"].iter().filter_map(|a| RangeRef::parse(a)).collect();
352 assert_eq!(RangeRef::top_left(&areas), CellRef::parse("B2"));
353 assert_eq!(RangeRef::top_left(&[]), None);
354 }
355
356 #[test]
357 fn sheet_quoting_follows_excel() {
358 for quoted in [
359 "A1",
360 "XFD1",
361 "AB1048576",
362 "R1",
363 "C1",
364 "RC1",
365 "R",
366 "C",
367 "RC",
368 "R1C",
369 "R1C1",
370 "r2c3",
371 "CR1",
372 ".x",
373 "TRUE",
374 "false",
375 "1abc",
376 "My Sheet",
377 "Bob's",
378 "a-b",
379 "",
380 ] {
381 assert!(sheet_needs_quotes(quoted), "{quoted:?} should be quoted");
382 }
383 for bare in ["Sheet1", "XFE1", "AB1048577", "Ärger", "日本", "Σ", "Sheet.1", "_x", "A1A", "Data", "RR", "CRX"] {
384 assert!(!sheet_needs_quotes(bare), "{bare:?} should stay bare");
385 }
386 }
387
388 #[test]
389 fn quote_unquote_and_split_sheet() {
390 assert_eq!(quote_sheet("Bob's"), "'Bob''s'");
391 assert_eq!(quote_sheet("Data"), "Data");
392 assert_eq!(unquote_sheet("'Bob''s'"), "Bob's");
393 assert_eq!(unquote_sheet("Data"), "Data");
394 assert_eq!(unquote_sheet("'"), "'");
395 assert_eq!(split_sheet("'a!b'!A1"), (Some("a!b".to_string()), "A1"));
396 assert_eq!(split_sheet("'Bob''s'!$A$1:$B$2"), (Some("Bob's".to_string()), "$A$1:$B$2"));
397 assert_eq!(split_sheet("A1"), (None, "A1"));
398 }
399
400 #[test]
405 fn split_sheet_leaves_device_names_inert_in_the_body() {
406 for s in ["C:\\a\\file!A1", "CON!A1", "NUL!A1", "a[b]!A1"] {
407 let (sheet, rest) = split_sheet(s);
408 assert_eq!(CellRef::parse(rest), Some(CellRef::new(0, 0)), "{s:?}: body must stay a bare reference");
409 let _ = sheet;
410 }
411 assert_eq!(split_sheet("C!A1").0, Some("C".to_string()));
413 }
414
415 #[test]
416 fn letters_roundtrip() {
417 for (c, s) in [(0, "A"), (25, "Z"), (26, "AA"), (51, "AZ"), (52, "BA"), (701, "ZZ"), (702, "AAA"), (16383, "XFD")] {
418 assert_eq!(col_to_letters(c), s);
419 assert_eq!(letters_to_col(s), Some(c));
420 }
421 assert_eq!(letters_to_col("XFE"), None);
422 assert_eq!(letters_to_col(""), None);
423 assert_eq!(letters_to_col("A1"), None);
424 }
425
426 #[test]
427 fn parse_cells() {
428 assert_eq!(CellRef::parse("B7"), Some(CellRef::new(6, 1)));
429 assert_eq!(CellRef::parse("$b$7"), Some(CellRef::new(6, 1)));
430 assert_eq!(CellRef::parse("XFD1048576"), Some(CellRef::new(MAX_ROWS - 1, MAX_COLS - 1)));
431 assert_eq!(CellRef::parse("A0"), None);
432 assert_eq!(CellRef::parse("A1048577"), None);
433 assert_eq!(CellRef::parse("A1x"), None);
434 assert_eq!(CellRef::parse(""), None);
435 assert_eq!(CellRef::parse("$"), None);
436 }
437
438 #[test]
439 fn ranges() -> Result<(), std::io::Error> {
440 let parse = |s| RangeRef::parse(s).ok_or_else(|| std::io::Error::other("valid range"));
441 let r = parse("C3:A1")?;
442 assert_eq!(r.a1(), "A1:C3");
443 assert_eq!(r.count(), 9);
444 assert_eq!(parse("A:C")?.a1(), "A:C");
445 assert_eq!(parse("3:5")?.a1(), "3:5");
446 assert!(RangeRef::parse("0:5").is_none());
447 let a = parse("A1:C3")?;
448 let b = parse("B2:D4")?;
449 assert_eq!(a.intersection(&b).ok_or_else(|| std::io::Error::other("overlap expected"))?.a1(), "B2:C3");
450 assert_eq!(a.union(&b).a1(), "A1:D4");
451 assert_eq!(a.iter().count(), 9);
452 Ok(())
453 }
454
455 #[test]
456 fn extreme_offsets() {
457 let cell = CellRef::new(1, 1);
458 for delta in [i64::MIN, i64::MAX] {
459 assert_eq!(cell.offset(delta, 0), None);
460 assert_eq!(cell.offset(0, delta), None);
461 assert_eq!(cell.offset(delta, delta), None);
462 }
463 assert_eq!(cell.offset_clamped(i64::MAX, i64::MAX), CellRef::new(MAX_ROWS - 1, MAX_COLS - 1));
464 assert_eq!(cell.offset_clamped(i64::MIN, i64::MIN), CellRef::new(0, 0));
465 assert_eq!(cell.offset_clamped(i64::MAX, i64::MIN), CellRef::new(MAX_ROWS - 1, 0));
466 assert_eq!(cell.offset_clamped(i64::MIN, i64::MAX), CellRef::new(0, MAX_COLS - 1));
467 assert_eq!(cell.offset(2, -1), Some(CellRef::new(3, 0)));
468 }
469
470 #[test]
471 fn offsets() {
472 assert_eq!(CellRef::new(0, 0).offset(-1, 0), None);
473 assert_eq!(CellRef::new(0, 0).offset_clamped(-5, 3), CellRef::new(0, 3));
474 }
475}
476
477#[derive(Clone, Copy, Debug, PartialEq, Eq)]
481pub struct CategoryLevels {
482 pub by_columns: bool,
485 pub outer: u32,
488}
489
490pub fn category_levels(categories: &RangeRef, values: Option<&RangeRef>) -> Option<CategoryLevels> {
493 if categories.is_full_rows() || categories.is_full_cols() {
494 return None;
495 }
496 let (w, h) = (categories.width(), categories.height());
497 let vertical = match values {
498 Some(v) => v.height() >= v.width(),
499 None => h >= w,
500 };
501 let outer = if vertical { w.saturating_sub(1) } else { h.saturating_sub(1) };
502 (outer > 0).then_some(CategoryLevels { by_columns: vertical, outer: outer.min(3) })
503}
504
505#[cfg(test)]
506mod category_level_tests {
507 use super::*;
508
509 #[test]
510 fn a_wide_range_beside_a_column_of_values_is_a_multi_level_axis() -> Result<(), std::io::Error> {
511 let r = |s| RangeRef::parse(s).ok_or_else(|| std::io::Error::other("valid test range"));
512 assert_eq!(category_levels(&r("A4:B39")?, Some(&r("C4:C39")?)), Some(CategoryLevels { by_columns: true, outer: 1 }));
513 assert_eq!(category_levels(&r("A4:D39")?, Some(&r("C4:C39")?)), Some(CategoryLevels { by_columns: true, outer: 3 }));
514 assert_eq!(category_levels(&r("A4:F39")?, Some(&r("C4:C39")?)).map(|l| l.outer), Some(3), "capped");
515 assert_eq!(category_levels(&r("A4:A39")?, Some(&r("C4:C39")?)), None, "one column: a plain list");
516 assert_eq!(category_levels(&r("B2:M3")?, Some(&r("B4:M4")?)), Some(CategoryLevels { by_columns: false, outer: 1 }));
518 assert_eq!(category_levels(&r("B3:M3")?, Some(&r("B4:M4")?)), None);
519 assert_eq!(category_levels(&r("A1:B6")?, None), Some(CategoryLevels { by_columns: true, outer: 1 }));
521 assert_eq!(category_levels(&r("A1:F2")?, None), Some(CategoryLevels { by_columns: false, outer: 1 }));
522 assert_eq!(category_levels(&r("A:B")?, Some(&r("C:C")?)), None, "whole columns");
523 Ok(())
524 }
525}