1use std::borrow::Cow;
4use std::error::Error;
5use std::fmt;
6
7use crate::coord::{A1ParseError, CoordError, RelativeCoord};
8
9pub type SheetId = u16;
11
12#[repr(transparent)]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
26pub struct PackedSheetCell(u64);
27
28impl PackedSheetCell {
29 const ROW_BITS: u32 = 20;
30 const COL_BITS: u32 = 14;
31 const SHEET_BITS: u32 = 16;
32
33 const COL_SHIFT: u32 = Self::ROW_BITS;
34 const SHEET_SHIFT: u32 = Self::ROW_BITS + Self::COL_BITS;
35
36 const ROW_MASK: u64 = (1u64 << Self::ROW_BITS) - 1;
37 const COL_MASK: u64 = (1u64 << Self::COL_BITS) - 1;
38 const SHEET_MASK: u64 = (1u64 << Self::SHEET_BITS) - 1;
39
40 pub const MAX_ROW0: u32 = Self::ROW_MASK as u32;
41 pub const MAX_COL0: u32 = Self::COL_MASK as u32;
42 const USED_BITS: u32 = Self::ROW_BITS + Self::COL_BITS + Self::SHEET_BITS;
43 const USED_MASK: u64 = (1u64 << Self::USED_BITS) - 1;
44
45 pub const fn try_new(sheet_id: SheetId, row0: u32, col0: u32) -> Option<Self> {
49 if row0 > Self::MAX_ROW0 || col0 > Self::MAX_COL0 {
50 return None;
51 }
52 let packed = (row0 as u64)
53 | ((col0 as u64) << Self::COL_SHIFT)
54 | ((sheet_id as u64) << Self::SHEET_SHIFT);
55 Some(Self(packed))
56 }
57
58 pub const fn as_u64(self) -> u64 {
60 self.0
61 }
62
63 pub const fn try_from_u64(raw: u64) -> Option<Self> {
67 if (raw & !Self::USED_MASK) != 0 {
68 return None;
69 }
70 let row0 = (raw & Self::ROW_MASK) as u32;
71 let col0 = ((raw >> Self::COL_SHIFT) & Self::COL_MASK) as u32;
72 if row0 > Self::MAX_ROW0 || col0 > Self::MAX_COL0 {
73 return None;
74 }
75 Some(Self(raw))
76 }
77
78 pub fn try_from_excel_1based(sheet_id: SheetId, row: u32, col: u32) -> Option<Self> {
80 let row0 = row.checked_sub(1)?;
81 let col0 = col.checked_sub(1)?;
82 Self::try_new(sheet_id, row0, col0)
83 }
84
85 pub const fn sheet_id(self) -> SheetId {
86 ((self.0 >> Self::SHEET_SHIFT) & Self::SHEET_MASK) as SheetId
87 }
88
89 pub const fn row0(self) -> u32 {
90 (self.0 & Self::ROW_MASK) as u32
91 }
92
93 pub const fn col0(self) -> u32 {
94 ((self.0 >> Self::COL_SHIFT) & Self::COL_MASK) as u32
95 }
96
97 pub const fn to_excel_1based(self) -> (SheetId, u32, u32) {
98 (self.sheet_id(), self.row0() + 1, self.col0() + 1)
99 }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
104pub enum SheetAddressError {
105 ZeroIndex,
107 RangeOrder,
109 MismatchedSheets,
111 MissingSheetName,
113 UnboundedRange,
115 Coord(CoordError),
117 Parse(A1ParseError),
119}
120
121impl fmt::Display for SheetAddressError {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 match self {
124 SheetAddressError::ZeroIndex => {
125 write!(f, "row and column indices must be 1-based (>= 1)")
126 }
127 SheetAddressError::RangeOrder => {
128 write!(
129 f,
130 "range must be ordered so the start is above/left of the end"
131 )
132 }
133 SheetAddressError::MismatchedSheets => {
134 write!(f, "range bounds refer to different sheets")
135 }
136 SheetAddressError::MissingSheetName => {
137 write!(f, "sheet name required to materialise textual address")
138 }
139 SheetAddressError::UnboundedRange => {
140 write!(f, "range requires explicit bounds")
141 }
142 SheetAddressError::Coord(err) => err.fmt(f),
143 SheetAddressError::Parse(err) => err.fmt(f),
144 }
145 }
146}
147
148impl Error for SheetAddressError {}
149
150impl From<CoordError> for SheetAddressError {
151 fn from(value: CoordError) -> Self {
152 SheetAddressError::Coord(value)
153 }
154}
155
156impl From<A1ParseError> for SheetAddressError {
157 fn from(value: A1ParseError) -> Self {
158 SheetAddressError::Parse(value)
159 }
160}
161
162#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
164pub enum SheetLocator<'a> {
165 #[default]
167 Current,
168 Id(SheetId),
170 Name(Cow<'a, str>),
172}
173
174impl<'a> SheetLocator<'a> {
175 pub const fn current() -> Self {
177 SheetLocator::Current
178 }
179
180 pub const fn from_id(id: SheetId) -> Self {
182 SheetLocator::Id(id)
183 }
184
185 pub fn from_name(name: impl Into<Cow<'a, str>>) -> Self {
187 SheetLocator::Name(name.into())
188 }
189
190 pub const fn id(&self) -> Option<SheetId> {
192 match self {
193 SheetLocator::Id(id) => Some(*id),
194 SheetLocator::Current | SheetLocator::Name(_) => None,
195 }
196 }
197
198 pub fn name(&self) -> Option<&str> {
200 match self {
201 SheetLocator::Name(name) => Some(name.as_ref()),
202 SheetLocator::Current | SheetLocator::Id(_) => None,
203 }
204 }
205
206 pub const fn is_current(&self) -> bool {
208 matches!(self, SheetLocator::Current)
209 }
210
211 pub fn as_ref(&self) -> SheetLocator<'_> {
213 match self {
214 SheetLocator::Current => SheetLocator::Current,
215 SheetLocator::Id(id) => SheetLocator::Id(*id),
216 SheetLocator::Name(name) => SheetLocator::Name(Cow::Borrowed(name.as_ref())),
217 }
218 }
219
220 pub fn into_owned(self) -> SheetLocator<'static> {
222 match self {
223 SheetLocator::Current => SheetLocator::Current,
224 SheetLocator::Id(id) => SheetLocator::Id(id),
225 SheetLocator::Name(name) => SheetLocator::Name(Cow::Owned(name.into_owned())),
226 }
227 }
228}
229
230impl<'a> From<SheetId> for SheetLocator<'a> {
231 fn from(value: SheetId) -> Self {
232 SheetLocator::from_id(value)
233 }
234}
235
236impl<'a> From<&'a str> for SheetLocator<'a> {
237 fn from(value: &'a str) -> Self {
238 SheetLocator::from_name(value)
239 }
240}
241
242impl<'a> From<String> for SheetLocator<'a> {
243 fn from(value: String) -> Self {
244 SheetLocator::from_name(value)
245 }
246}
247
248#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
250pub struct AxisBound {
251 pub index: u32,
253 pub abs: bool,
255}
256
257impl AxisBound {
258 pub const fn new(index: u32, abs: bool) -> Self {
259 AxisBound { index, abs }
260 }
261
262 pub fn from_excel_1based(index: u32, abs: bool) -> Result<Self, SheetAddressError> {
264 let index0 = index.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
265 Ok(AxisBound::new(index0, abs))
266 }
267
268 pub const fn to_excel_1based(self) -> u32 {
270 self.index + 1
271 }
272}
273
274#[derive(Clone, Debug, Eq, PartialEq, Hash)]
276pub struct SheetCellRef<'a> {
277 pub sheet: SheetLocator<'a>,
278 pub coord: RelativeCoord,
279}
280
281impl<'a> SheetCellRef<'a> {
282 pub const fn new(sheet: SheetLocator<'a>, coord: RelativeCoord) -> Self {
283 SheetCellRef { sheet, coord }
284 }
285
286 pub fn from_excel(
288 sheet: SheetLocator<'a>,
289 row: u32,
290 col: u32,
291 row_abs: bool,
292 col_abs: bool,
293 ) -> Result<Self, SheetAddressError> {
294 let row0 = row.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
295 let col0 = col.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
296 let coord = RelativeCoord::try_new(row0, col0, row_abs, col_abs)?;
297 Ok(SheetCellRef::new(sheet, coord))
298 }
299
300 pub fn try_from_a1(
302 sheet: SheetLocator<'a>,
303 reference: &str,
304 ) -> Result<Self, SheetAddressError> {
305 let coord = RelativeCoord::try_from_a1(reference)?;
306 Ok(SheetCellRef::new(sheet, coord))
307 }
308
309 pub fn as_ref(&self) -> SheetCellRef<'_> {
311 SheetCellRef {
312 sheet: self.sheet.as_ref(),
313 coord: self.coord,
314 }
315 }
316
317 pub fn into_owned(self) -> SheetCellRef<'static> {
319 SheetCellRef {
320 sheet: self.sheet.into_owned(),
321 coord: self.coord,
322 }
323 }
324}
325
326#[derive(Clone, Debug, Eq, PartialEq, Hash)]
328pub struct SheetRangeRef<'a> {
329 pub sheet: SheetLocator<'a>,
330 pub start_row: Option<AxisBound>,
331 pub start_col: Option<AxisBound>,
332 pub end_row: Option<AxisBound>,
333 pub end_col: Option<AxisBound>,
334}
335
336impl<'a> SheetRangeRef<'a> {
337 pub const fn new(
338 sheet: SheetLocator<'a>,
339 start_row: Option<AxisBound>,
340 start_col: Option<AxisBound>,
341 end_row: Option<AxisBound>,
342 end_col: Option<AxisBound>,
343 ) -> Self {
344 SheetRangeRef {
345 sheet,
346 start_row,
347 start_col,
348 end_row,
349 end_col,
350 }
351 }
352
353 pub fn from_cells(
355 start: SheetCellRef<'a>,
356 end: SheetCellRef<'a>,
357 ) -> Result<Self, SheetAddressError> {
358 if start.sheet != end.sheet {
359 return Err(SheetAddressError::MismatchedSheets);
360 }
361 let sr = AxisBound::new(start.coord.row(), start.coord.row_abs());
362 let sc = AxisBound::new(start.coord.col(), start.coord.col_abs());
363 let er = AxisBound::new(end.coord.row(), end.coord.row_abs());
364 let ec = AxisBound::new(end.coord.col(), end.coord.col_abs());
365 SheetRangeRef::from_parts(start.sheet, Some(sr), Some(sc), Some(er), Some(ec))
366 }
367
368 #[allow(clippy::too_many_arguments)]
370 pub fn from_excel_rect(
371 sheet: SheetLocator<'a>,
372 start_row: u32,
373 start_col: u32,
374 end_row: u32,
375 end_col: u32,
376 start_row_abs: bool,
377 start_col_abs: bool,
378 end_row_abs: bool,
379 end_col_abs: bool,
380 ) -> Result<Self, SheetAddressError> {
381 let sr = AxisBound::from_excel_1based(start_row, start_row_abs)?;
382 let sc = AxisBound::from_excel_1based(start_col, start_col_abs)?;
383 let er = AxisBound::from_excel_1based(end_row, end_row_abs)?;
384 let ec = AxisBound::from_excel_1based(end_col, end_col_abs)?;
385 SheetRangeRef::from_parts(sheet, Some(sr), Some(sc), Some(er), Some(ec))
386 }
387
388 pub fn from_parts(
390 sheet: SheetLocator<'a>,
391 start_row: Option<AxisBound>,
392 start_col: Option<AxisBound>,
393 end_row: Option<AxisBound>,
394 end_col: Option<AxisBound>,
395 ) -> Result<Self, SheetAddressError> {
396 if let (Some(sr), Some(er)) = (start_row, end_row)
397 && sr.index > er.index
398 {
399 return Err(SheetAddressError::RangeOrder);
400 }
401 if let (Some(sc), Some(ec)) = (start_col, end_col)
402 && sc.index > ec.index
403 {
404 return Err(SheetAddressError::RangeOrder);
405 }
406 Ok(SheetRangeRef::new(
407 sheet, start_row, start_col, end_row, end_col,
408 ))
409 }
410
411 pub fn as_ref(&self) -> SheetRangeRef<'_> {
413 SheetRangeRef {
414 sheet: self.sheet.as_ref(),
415 start_row: self.start_row,
416 start_col: self.start_col,
417 end_row: self.end_row,
418 end_col: self.end_col,
419 }
420 }
421
422 pub fn into_owned(self) -> SheetRangeRef<'static> {
424 SheetRangeRef {
425 sheet: self.sheet.into_owned(),
426 start_row: self.start_row,
427 start_col: self.start_col,
428 end_row: self.end_row,
429 end_col: self.end_col,
430 }
431 }
432}
433
434#[derive(Clone, Debug, Eq, PartialEq, Hash)]
436pub enum SheetRef<'a> {
437 Cell(SheetCellRef<'a>),
438 Range(SheetRangeRef<'a>),
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn sheet_locator_roundtrip() {
447 let loc = SheetLocator::from_id(7);
448 assert_eq!(loc.id(), Some(7));
449 assert_eq!(loc.name(), None);
450 assert_eq!(loc.as_ref(), SheetLocator::Id(7));
451
452 let name = SheetLocator::from_name("Data");
453 assert_eq!(name.id(), None);
454 assert_eq!(name.name(), Some("Data"));
455 let owned = name.clone().into_owned();
456 assert_eq!(owned.name(), Some("Data"));
457 assert_eq!(name, owned.as_ref());
458
459 let current = SheetLocator::current();
460 assert!(current.is_current());
461 assert_eq!(current.id(), None);
462 }
463
464 #[test]
465 fn cell_from_excel_preserves_flags() {
466 let a1 = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 1, 1, false, false)
467 .expect("valid cell");
468 assert_eq!(a1.coord.row(), 0);
469 assert_eq!(a1.coord.col(), 0);
470 assert!(!a1.coord.row_abs());
471 assert!(!a1.coord.col_abs());
472
473 let abs = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 3, 2, true, false)
474 .expect("valid absolute cell");
475 assert_eq!(abs.coord.row(), 2);
476 assert!(abs.coord.row_abs());
477 assert!(!abs.coord.col_abs());
478 }
479
480 #[test]
481 fn cell_from_excel_rejects_zero() {
482 let err = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 0, 1, false, false)
483 .unwrap_err();
484 assert_eq!(err, SheetAddressError::ZeroIndex);
485 }
486
487 #[test]
488 fn range_from_cells_validates_sheet_and_order() {
489 let sheet = SheetLocator::from_name("Sheet1");
490 let start = SheetCellRef::try_from_a1(sheet.as_ref(), "A1").unwrap();
491 let end = SheetCellRef::try_from_a1(sheet.as_ref(), "$B$3").unwrap();
492 let range = SheetRangeRef::from_cells(start.clone(), end.clone()).unwrap();
493 assert_eq!(range.start_row.unwrap().index, 0);
494 assert_eq!(range.end_row.unwrap().index, 2);
495
496 let other_sheet =
497 SheetCellRef::try_from_a1(SheetLocator::from_name("Other"), "C2").unwrap();
498 assert_eq!(
499 SheetRangeRef::from_cells(start, other_sheet).unwrap_err(),
500 SheetAddressError::MismatchedSheets
501 );
502
503 let inverted = SheetRangeRef::from_parts(
504 SheetLocator::from_name("Sheet1"),
505 Some(AxisBound::new(end.coord.row(), end.coord.row_abs())),
506 Some(AxisBound::new(end.coord.col(), end.coord.col_abs())),
507 Some(AxisBound::new(0, false)),
508 Some(AxisBound::new(0, false)),
509 );
510 assert_eq!(inverted.unwrap_err(), SheetAddressError::RangeOrder);
511 }
512
513 #[test]
514 fn packed_sheet_cell_roundtrip() {
515 let packed = PackedSheetCell::try_new(7, 10, 8).unwrap();
516 assert_eq!(packed.sheet_id(), 7);
517 assert_eq!(packed.row0(), 10);
518 assert_eq!(packed.col0(), 8);
519 assert_eq!(packed.to_excel_1based(), (7, 11, 9));
520 assert_eq!(
521 PackedSheetCell::try_from_excel_1based(7, 11, 9),
522 Some(packed)
523 );
524 assert_eq!(PackedSheetCell::try_from_excel_1based(7, 0, 1), None);
525 assert_eq!(PackedSheetCell::try_from_u64(packed.as_u64()), Some(packed));
526 assert_eq!(
527 PackedSheetCell::try_from_u64(packed.as_u64() | (1u64 << 63)),
528 None
529 );
530 }
531}