1use std::borrow::Cow;
4use std::error::Error;
5use std::fmt;
6
7use crate::coord::{A1ParseError, CoordError, RelativeCoord};
8
9pub fn format_a1_sheet_name(name: &str) -> Cow<'_, str> {
14 if !a1_sheet_name_needs_quoting(name) {
15 return Cow::Borrowed(name);
16 }
17 Cow::Owned(format!("'{}'", name.replace('\'', "''")))
18}
19
20pub fn a1_sheet_name_needs_quoting(name: &str) -> bool {
24 if name.is_empty() {
25 return false;
26 }
27
28 if name.as_bytes()[0].is_ascii_digit() {
29 return true;
30 }
31
32 for &byte in name.as_bytes() {
33 match byte {
34 b' ' | b'!' | b'"' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+'
35 | b',' | b'-' | b'.' | b'/' | b':' | b';' | b'<' | b'=' | b'>' | b'?' | b'@' | b'['
36 | b'\\' | b']' | b'^' | b'`' | b'{' | b'|' | b'}' | b'~' => {
37 return true;
38 }
39 _ => {}
40 }
41 }
42
43 matches!(
44 name.to_uppercase().as_str(),
45 "TRUE" | "FALSE" | "NULL" | "REF" | "DIV" | "NAME" | "NUM" | "VALUE" | "N/A"
46 )
47}
48
49pub type SheetId = u16;
51
52#[repr(transparent)]
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
66pub struct PackedSheetCell(u64);
67
68impl PackedSheetCell {
69 const ROW_BITS: u32 = 20;
70 const COL_BITS: u32 = 14;
71 const SHEET_BITS: u32 = 16;
72
73 const COL_SHIFT: u32 = Self::ROW_BITS;
74 const SHEET_SHIFT: u32 = Self::ROW_BITS + Self::COL_BITS;
75
76 const ROW_MASK: u64 = (1u64 << Self::ROW_BITS) - 1;
77 const COL_MASK: u64 = (1u64 << Self::COL_BITS) - 1;
78 const SHEET_MASK: u64 = (1u64 << Self::SHEET_BITS) - 1;
79
80 pub const MAX_ROW0: u32 = Self::ROW_MASK as u32;
81 pub const MAX_COL0: u32 = Self::COL_MASK as u32;
82 const USED_BITS: u32 = Self::ROW_BITS + Self::COL_BITS + Self::SHEET_BITS;
83 const USED_MASK: u64 = (1u64 << Self::USED_BITS) - 1;
84
85 pub const fn try_new(sheet_id: SheetId, row0: u32, col0: u32) -> Option<Self> {
89 if row0 > Self::MAX_ROW0 || col0 > Self::MAX_COL0 {
90 return None;
91 }
92 let packed = (row0 as u64)
93 | ((col0 as u64) << Self::COL_SHIFT)
94 | ((sheet_id as u64) << Self::SHEET_SHIFT);
95 Some(Self(packed))
96 }
97
98 pub const fn as_u64(self) -> u64 {
100 self.0
101 }
102
103 pub const fn try_from_u64(raw: u64) -> Option<Self> {
107 if (raw & !Self::USED_MASK) != 0 {
108 return None;
109 }
110 let row0 = (raw & Self::ROW_MASK) as u32;
111 let col0 = ((raw >> Self::COL_SHIFT) & Self::COL_MASK) as u32;
112 if row0 > Self::MAX_ROW0 || col0 > Self::MAX_COL0 {
113 return None;
114 }
115 Some(Self(raw))
116 }
117
118 pub fn try_from_excel_1based(sheet_id: SheetId, row: u32, col: u32) -> Option<Self> {
120 let row0 = row.checked_sub(1)?;
121 let col0 = col.checked_sub(1)?;
122 Self::try_new(sheet_id, row0, col0)
123 }
124
125 pub const fn sheet_id(self) -> SheetId {
126 ((self.0 >> Self::SHEET_SHIFT) & Self::SHEET_MASK) as SheetId
127 }
128
129 pub const fn row0(self) -> u32 {
130 (self.0 & Self::ROW_MASK) as u32
131 }
132
133 pub const fn col0(self) -> u32 {
134 ((self.0 >> Self::COL_SHIFT) & Self::COL_MASK) as u32
135 }
136
137 pub const fn to_excel_1based(self) -> (SheetId, u32, u32) {
138 (self.sheet_id(), self.row0() + 1, self.col0() + 1)
139 }
140}
141
142#[non_exhaustive]
144#[derive(Clone, Debug, Eq, PartialEq)]
145pub enum SheetAddressError {
146 ZeroIndex,
148 RowOutOfBounds,
150 ColumnOutOfBounds,
152 NonSingleCellRange,
154 RangeOrder,
156 MismatchedSheets,
158 MissingSheetName,
160 UnboundedRange,
162 Coord(CoordError),
164 Parse(A1ParseError),
166}
167
168impl fmt::Display for SheetAddressError {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 SheetAddressError::ZeroIndex => {
172 write!(f, "row and column indices must be 1-based (>= 1)")
173 }
174 SheetAddressError::RowOutOfBounds => {
175 write!(f, "row index exceeds the 1,048,576-row grid")
176 }
177 SheetAddressError::ColumnOutOfBounds => {
178 write!(f, "column index exceeds the 16,384-column grid")
179 }
180 SheetAddressError::NonSingleCellRange => {
181 write!(f, "range must contain exactly one cell")
182 }
183 SheetAddressError::RangeOrder => {
184 write!(
185 f,
186 "range must be ordered so the start is above/left of the end"
187 )
188 }
189 SheetAddressError::MismatchedSheets => {
190 write!(f, "range bounds refer to different sheets")
191 }
192 SheetAddressError::MissingSheetName => {
193 write!(f, "sheet name required to materialise textual address")
194 }
195 SheetAddressError::UnboundedRange => {
196 write!(f, "range requires explicit bounds")
197 }
198 SheetAddressError::Coord(err) => err.fmt(f),
199 SheetAddressError::Parse(err) => err.fmt(f),
200 }
201 }
202}
203
204impl Error for SheetAddressError {}
205
206impl From<CoordError> for SheetAddressError {
207 fn from(value: CoordError) -> Self {
208 SheetAddressError::Coord(value)
209 }
210}
211
212impl From<A1ParseError> for SheetAddressError {
213 fn from(value: A1ParseError) -> Self {
214 SheetAddressError::Parse(value)
215 }
216}
217
218#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
220pub enum SheetLocator<'a> {
221 #[default]
223 Current,
224 Id(SheetId),
226 Name(Cow<'a, str>),
228}
229
230impl<'a> SheetLocator<'a> {
231 pub const fn current() -> Self {
233 SheetLocator::Current
234 }
235
236 pub const fn from_id(id: SheetId) -> Self {
238 SheetLocator::Id(id)
239 }
240
241 pub fn from_name(name: impl Into<Cow<'a, str>>) -> Self {
243 SheetLocator::Name(name.into())
244 }
245
246 pub const fn id(&self) -> Option<SheetId> {
248 match self {
249 SheetLocator::Id(id) => Some(*id),
250 SheetLocator::Current | SheetLocator::Name(_) => None,
251 }
252 }
253
254 pub fn name(&self) -> Option<&str> {
256 match self {
257 SheetLocator::Name(name) => Some(name.as_ref()),
258 SheetLocator::Current | SheetLocator::Id(_) => None,
259 }
260 }
261
262 pub const fn is_current(&self) -> bool {
264 matches!(self, SheetLocator::Current)
265 }
266
267 pub fn as_ref(&self) -> SheetLocator<'_> {
269 match self {
270 SheetLocator::Current => SheetLocator::Current,
271 SheetLocator::Id(id) => SheetLocator::Id(*id),
272 SheetLocator::Name(name) => SheetLocator::Name(Cow::Borrowed(name.as_ref())),
273 }
274 }
275
276 pub fn into_owned(self) -> SheetLocator<'static> {
278 match self {
279 SheetLocator::Current => SheetLocator::Current,
280 SheetLocator::Id(id) => SheetLocator::Id(id),
281 SheetLocator::Name(name) => SheetLocator::Name(Cow::Owned(name.into_owned())),
282 }
283 }
284}
285
286impl<'a> From<SheetId> for SheetLocator<'a> {
287 fn from(value: SheetId) -> Self {
288 SheetLocator::from_id(value)
289 }
290}
291
292impl<'a> From<&'a str> for SheetLocator<'a> {
293 fn from(value: &'a str) -> Self {
294 SheetLocator::from_name(value)
295 }
296}
297
298impl<'a> From<String> for SheetLocator<'a> {
299 fn from(value: String) -> Self {
300 SheetLocator::from_name(value)
301 }
302}
303
304#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
306pub struct AxisBound {
307 pub index: u32,
309 pub abs: bool,
311}
312
313impl AxisBound {
314 pub const fn new(index: u32, abs: bool) -> Self {
315 AxisBound { index, abs }
316 }
317
318 pub fn from_excel_1based(index: u32, abs: bool) -> Result<Self, SheetAddressError> {
320 let index0 = index.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
321 Ok(AxisBound::new(index0, abs))
322 }
323
324 pub const fn to_excel_1based(self) -> u32 {
326 self.index + 1
327 }
328}
329
330#[derive(Clone, Debug, Eq, PartialEq, Hash)]
332pub struct SheetCellRef<'a> {
333 pub sheet: SheetLocator<'a>,
334 pub coord: RelativeCoord,
335}
336
337impl<'a> SheetCellRef<'a> {
338 pub const fn new(sheet: SheetLocator<'a>, coord: RelativeCoord) -> Self {
339 SheetCellRef { sheet, coord }
340 }
341
342 pub fn from_excel(
344 sheet: SheetLocator<'a>,
345 row: u32,
346 col: u32,
347 row_abs: bool,
348 col_abs: bool,
349 ) -> Result<Self, SheetAddressError> {
350 let row0 = row.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
351 let col0 = col.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
352 let coord = RelativeCoord::try_new(row0, col0, row_abs, col_abs)?;
353 Ok(SheetCellRef::new(sheet, coord))
354 }
355
356 pub fn try_from_a1(
358 sheet: SheetLocator<'a>,
359 reference: &str,
360 ) -> Result<Self, SheetAddressError> {
361 let coord = RelativeCoord::try_from_a1(reference)?;
362 Ok(SheetCellRef::new(sheet, coord))
363 }
364
365 pub fn as_ref(&self) -> SheetCellRef<'_> {
367 SheetCellRef {
368 sheet: self.sheet.as_ref(),
369 coord: self.coord,
370 }
371 }
372
373 pub fn into_owned(self) -> SheetCellRef<'static> {
375 SheetCellRef {
376 sheet: self.sheet.into_owned(),
377 coord: self.coord,
378 }
379 }
380}
381
382#[derive(Clone, Debug, Eq, PartialEq, Hash)]
384pub struct SheetRangeRef<'a> {
385 pub sheet: SheetLocator<'a>,
386 pub start_row: Option<AxisBound>,
387 pub start_col: Option<AxisBound>,
388 pub end_row: Option<AxisBound>,
389 pub end_col: Option<AxisBound>,
390}
391
392impl<'a> SheetRangeRef<'a> {
393 pub const fn new(
394 sheet: SheetLocator<'a>,
395 start_row: Option<AxisBound>,
396 start_col: Option<AxisBound>,
397 end_row: Option<AxisBound>,
398 end_col: Option<AxisBound>,
399 ) -> Self {
400 SheetRangeRef {
401 sheet,
402 start_row,
403 start_col,
404 end_row,
405 end_col,
406 }
407 }
408
409 pub fn from_cells(
411 start: SheetCellRef<'a>,
412 end: SheetCellRef<'a>,
413 ) -> Result<Self, SheetAddressError> {
414 if start.sheet != end.sheet {
415 return Err(SheetAddressError::MismatchedSheets);
416 }
417 let sr = AxisBound::new(start.coord.row(), start.coord.row_abs());
418 let sc = AxisBound::new(start.coord.col(), start.coord.col_abs());
419 let er = AxisBound::new(end.coord.row(), end.coord.row_abs());
420 let ec = AxisBound::new(end.coord.col(), end.coord.col_abs());
421 SheetRangeRef::from_parts(start.sheet, Some(sr), Some(sc), Some(er), Some(ec))
422 }
423
424 #[allow(clippy::too_many_arguments)]
426 pub fn from_excel_rect(
427 sheet: SheetLocator<'a>,
428 start_row: u32,
429 start_col: u32,
430 end_row: u32,
431 end_col: u32,
432 start_row_abs: bool,
433 start_col_abs: bool,
434 end_row_abs: bool,
435 end_col_abs: bool,
436 ) -> Result<Self, SheetAddressError> {
437 let sr = AxisBound::from_excel_1based(start_row, start_row_abs)?;
438 let sc = AxisBound::from_excel_1based(start_col, start_col_abs)?;
439 let er = AxisBound::from_excel_1based(end_row, end_row_abs)?;
440 let ec = AxisBound::from_excel_1based(end_col, end_col_abs)?;
441 SheetRangeRef::from_parts(sheet, Some(sr), Some(sc), Some(er), Some(ec))
442 }
443
444 pub fn from_parts(
446 sheet: SheetLocator<'a>,
447 start_row: Option<AxisBound>,
448 start_col: Option<AxisBound>,
449 end_row: Option<AxisBound>,
450 end_col: Option<AxisBound>,
451 ) -> Result<Self, SheetAddressError> {
452 if let (Some(sr), Some(er)) = (start_row, end_row) {
453 if sr.index > er.index {
454 return Err(SheetAddressError::RangeOrder);
455 }
456 }
457 if let (Some(sc), Some(ec)) = (start_col, end_col) {
458 if sc.index > ec.index {
459 return Err(SheetAddressError::RangeOrder);
460 }
461 }
462 Ok(SheetRangeRef::new(
463 sheet, start_row, start_col, end_row, end_col,
464 ))
465 }
466
467 pub fn as_ref(&self) -> SheetRangeRef<'_> {
469 SheetRangeRef {
470 sheet: self.sheet.as_ref(),
471 start_row: self.start_row,
472 start_col: self.start_col,
473 end_row: self.end_row,
474 end_col: self.end_col,
475 }
476 }
477
478 pub fn into_owned(self) -> SheetRangeRef<'static> {
480 SheetRangeRef {
481 sheet: self.sheet.into_owned(),
482 start_row: self.start_row,
483 start_col: self.start_col,
484 end_row: self.end_row,
485 end_col: self.end_col,
486 }
487 }
488}
489
490#[derive(Clone, Debug, Eq, PartialEq, Hash)]
492pub enum SheetRef<'a> {
493 Cell(SheetCellRef<'a>),
494 Range(SheetRangeRef<'a>),
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500
501 #[test]
502 fn sheet_locator_roundtrip() {
503 let loc = SheetLocator::from_id(7);
504 assert_eq!(loc.id(), Some(7));
505 assert_eq!(loc.name(), None);
506 assert_eq!(loc.as_ref(), SheetLocator::Id(7));
507
508 let name = SheetLocator::from_name("Data");
509 assert_eq!(name.id(), None);
510 assert_eq!(name.name(), Some("Data"));
511 let owned = name.clone().into_owned();
512 assert_eq!(owned.name(), Some("Data"));
513 assert_eq!(name, owned.as_ref());
514
515 let current = SheetLocator::current();
516 assert!(current.is_current());
517 assert_eq!(current.id(), None);
518 }
519
520 #[test]
521 fn cell_from_excel_preserves_flags() {
522 let a1 = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 1, 1, false, false)
523 .expect("valid cell");
524 assert_eq!(a1.coord.row(), 0);
525 assert_eq!(a1.coord.col(), 0);
526 assert!(!a1.coord.row_abs());
527 assert!(!a1.coord.col_abs());
528
529 let abs = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 3, 2, true, false)
530 .expect("valid absolute cell");
531 assert_eq!(abs.coord.row(), 2);
532 assert!(abs.coord.row_abs());
533 assert!(!abs.coord.col_abs());
534 }
535
536 #[test]
537 fn cell_from_excel_rejects_zero() {
538 let err = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 0, 1, false, false)
539 .unwrap_err();
540 assert_eq!(err, SheetAddressError::ZeroIndex);
541 }
542
543 #[test]
544 fn range_from_cells_validates_sheet_and_order() {
545 let sheet = SheetLocator::from_name("Sheet1");
546 let start = SheetCellRef::try_from_a1(sheet.as_ref(), "A1").unwrap();
547 let end = SheetCellRef::try_from_a1(sheet.as_ref(), "$B$3").unwrap();
548 let range = SheetRangeRef::from_cells(start.clone(), end.clone()).unwrap();
549 assert_eq!(range.start_row.unwrap().index, 0);
550 assert_eq!(range.end_row.unwrap().index, 2);
551
552 let other_sheet =
553 SheetCellRef::try_from_a1(SheetLocator::from_name("Other"), "C2").unwrap();
554 assert_eq!(
555 SheetRangeRef::from_cells(start, other_sheet).unwrap_err(),
556 SheetAddressError::MismatchedSheets
557 );
558
559 let inverted = SheetRangeRef::from_parts(
560 SheetLocator::from_name("Sheet1"),
561 Some(AxisBound::new(end.coord.row(), end.coord.row_abs())),
562 Some(AxisBound::new(end.coord.col(), end.coord.col_abs())),
563 Some(AxisBound::new(0, false)),
564 Some(AxisBound::new(0, false)),
565 );
566 assert_eq!(inverted.unwrap_err(), SheetAddressError::RangeOrder);
567 }
568
569 #[test]
570 fn packed_sheet_cell_roundtrip() {
571 let packed = PackedSheetCell::try_new(7, 10, 8).unwrap();
572 assert_eq!(packed.sheet_id(), 7);
573 assert_eq!(packed.row0(), 10);
574 assert_eq!(packed.col0(), 8);
575 assert_eq!(packed.to_excel_1based(), (7, 11, 9));
576 assert_eq!(
577 PackedSheetCell::try_from_excel_1based(7, 11, 9),
578 Some(packed)
579 );
580 assert_eq!(PackedSheetCell::try_from_excel_1based(7, 0, 1), None);
581 assert_eq!(PackedSheetCell::try_from_u64(packed.as_u64()), Some(packed));
582 assert_eq!(
583 PackedSheetCell::try_from_u64(packed.as_u64() | (1u64 << 63)),
584 None
585 );
586 }
587}