1use core::{fmt, str::FromStr};
9
10const ROW_BITS: u32 = 20;
11const COL_BITS: u32 = 14;
12const ROW_MAX: u32 = (1 << ROW_BITS) - 1;
13const COL_MAX: u32 = (1 << COL_BITS) - 1;
14const ROW_MAX_1BASED: u32 = ROW_MAX + 1;
15const COL_MAX_1BASED: u32 = COL_MAX + 1;
16
17const ROW_SHIFT: u32 = 24;
18const COL_SHIFT: u32 = 10;
19
20const ROW_MASK: u64 = (ROW_MAX as u64) << ROW_SHIFT;
21const COL_MASK: u64 = (COL_MAX as u64) << COL_SHIFT;
22const RESERVED_HIGH_MASK: u64 = 0xFFFFF00000000000;
23const RESERVED_LOW_MASK: u64 = 0x3FF;
24
25const ROW_ABS_BIT: u64 = 1;
26const COL_ABS_BIT: u64 = 1 << 1;
27const RELATIVE_RESERVED_LOW_MASK: u64 = RESERVED_LOW_MASK & !(ROW_ABS_BIT | COL_ABS_BIT);
28
29#[non_exhaustive]
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
32pub enum CoordError {
33 RowOverflow(i64),
34 ColOverflow(i64),
35 NegativeRow(i64),
36 NegativeCol(i64),
37 ReservedBitsSet(u64),
38}
39
40impl fmt::Display for CoordError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 CoordError::RowOverflow(row) => write!(f, "row {row} exceeds {MAX}", MAX = ROW_MAX),
44 CoordError::ColOverflow(col) => write!(f, "col {col} exceeds {MAX}", MAX = COL_MAX),
45 CoordError::NegativeRow(row) => write!(f, "row {row} is negative"),
46 CoordError::NegativeCol(col) => write!(f, "col {col} is negative"),
47 CoordError::ReservedBitsSet(bits) => {
48 write!(f, "coordinate contains reserved bits: {bits:#x}")
49 }
50 }
51 }
52}
53
54#[non_exhaustive]
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub enum A1ParseError {
58 Empty,
59 MissingColumn,
60 MissingRow,
61 InvalidColumnChar(char),
62 InvalidRowChar(char),
63 TrailingCharacters(String),
64 ColumnOutOfRange(u32),
65 RowOutOfRange(u32),
66}
67
68impl fmt::Display for A1ParseError {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 A1ParseError::Empty => write!(f, "reference is empty"),
72 A1ParseError::MissingColumn => write!(f, "reference must start with a column"),
73 A1ParseError::MissingRow => write!(f, "reference must include a row number"),
74 A1ParseError::InvalidColumnChar(ch) => {
75 write!(f, "invalid column character `{ch}`; expected A-Z")
76 }
77 A1ParseError::InvalidRowChar(ch) => {
78 write!(f, "invalid row character `{ch}`; expected 0-9")
79 }
80 A1ParseError::TrailingCharacters(rest) => {
81 write!(f, "unexpected trailing characters `{rest}`")
82 }
83 A1ParseError::ColumnOutOfRange(col) => {
84 write!(
85 f,
86 "column {col} is outside Excel's supported range (1..={})",
87 COL_MAX_1BASED
88 )
89 }
90 A1ParseError::RowOutOfRange(row) => {
91 write!(
92 f,
93 "row {row} is outside Excel's supported range (1..={})",
94 ROW_MAX_1BASED
95 )
96 }
97 }
98 }
99}
100
101impl From<CoordError> for A1ParseError {
102 fn from(value: CoordError) -> Self {
103 match value {
104 CoordError::RowOverflow(row) => A1ParseError::RowOutOfRange(row as u32 + 1),
105 CoordError::ColOverflow(col) => A1ParseError::ColumnOutOfRange(col as u32 + 1),
106 CoordError::NegativeRow(_) => A1ParseError::RowOutOfRange(0),
107 CoordError::NegativeCol(_) => A1ParseError::ColumnOutOfRange(0),
108 CoordError::ReservedBitsSet(bits) => {
109 A1ParseError::TrailingCharacters(format!("reserved bits {bits:#x}"))
110 }
111 }
112 }
113}
114
115#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
117pub struct Coord(u64);
118
119impl Coord {
120 pub const INVALID: Self = Self(u64::MAX);
121
122 const RESERVED_MASK: u64 = RESERVED_HIGH_MASK | RESERVED_LOW_MASK;
123
124 pub fn new(row: u32, col: u32) -> Self {
132 assert!(row <= ROW_MAX, "Row {row} exceeds 20 bits");
133 assert!(col <= COL_MAX, "Col {col} exceeds 14 bits");
134 Self(((row as u64) << ROW_SHIFT) | ((col as u64) << COL_SHIFT))
135 }
136
137 #[inline(always)]
144 pub fn from_excel(row: u32, col: u32) -> Self {
145 let row0 = row.saturating_sub(1);
146 let col0 = col.saturating_sub(1);
147 Self::new(row0, col0)
148 }
149
150 pub fn try_new(row: u32, col: u32) -> Result<Self, CoordError> {
152 if row > ROW_MAX {
153 return Err(CoordError::RowOverflow(row as i64));
154 }
155 if col > COL_MAX {
156 return Err(CoordError::ColOverflow(col as i64));
157 }
158 Ok(Self::new(row, col))
159 }
160
161 pub fn from_raw(raw: u64) -> Result<Self, CoordError> {
163 if raw == u64::MAX {
164 return Ok(Self::INVALID);
165 }
166 if raw & Self::RESERVED_MASK != 0 {
167 return Err(CoordError::ReservedBitsSet(raw & Self::RESERVED_MASK));
168 }
169 Ok(Self(raw))
170 }
171
172 #[inline(always)]
173 pub fn row(self) -> u32 {
174 ((self.0 & ROW_MASK) >> ROW_SHIFT) as u32
175 }
176
177 #[inline(always)]
178 pub fn col(self) -> u32 {
179 ((self.0 & COL_MASK) >> COL_SHIFT) as u32
180 }
181
182 #[inline(always)]
183 pub fn as_u64(self) -> u64 {
184 self.0
185 }
186
187 #[inline(always)]
188 pub fn is_valid(self) -> bool {
189 self.0 != u64::MAX
190 }
191
192 #[inline(always)]
194 pub fn normalize(self) -> Self {
195 Self(self.0 & !Self::RESERVED_MASK)
196 }
197
198 #[inline(always)]
200 pub fn into_relative(self) -> RelativeCoord {
201 RelativeCoord::new(self.row(), self.col(), true, true)
202 }
203
204 pub fn try_from_a1(input: &str) -> Result<Self, A1ParseError> {
206 let (row, col, _, _) = parse_a1_components(input)?;
207 let row0 = row.checked_sub(1).ok_or(A1ParseError::RowOutOfRange(0))?;
208 let col0 = col
209 .checked_sub(1)
210 .ok_or(A1ParseError::ColumnOutOfRange(0))?;
211 Coord::try_new(row0, col0).map_err(A1ParseError::from)
212 }
213}
214
215impl From<Coord> for (u32, u32) {
216 fn from(coord: Coord) -> Self {
217 (coord.row(), coord.col())
218 }
219}
220
221impl TryFrom<(u32, u32)> for Coord {
222 type Error = CoordError;
223
224 fn try_from(value: (u32, u32)) -> Result<Self, Self::Error> {
225 Self::try_new(value.0, value.1)
226 }
227}
228
229impl TryFrom<(i64, i64)> for Coord {
230 type Error = CoordError;
231
232 fn try_from(value: (i64, i64)) -> Result<Self, Self::Error> {
233 let (row, col) = value;
234 if row < 0 {
235 return Err(CoordError::NegativeRow(row));
236 }
237 if col < 0 {
238 return Err(CoordError::NegativeCol(col));
239 }
240 let row = row as u32;
241 let col = col as u32;
242 Self::try_new(row, col)
243 }
244}
245
246impl From<RelativeCoord> for Coord {
247 fn from(value: RelativeCoord) -> Self {
248 Self::new(value.row(), value.col())
249 }
250}
251
252#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
258pub struct RelativeCoord(u64);
259
260impl RelativeCoord {
261 const RESERVED_MASK: u64 = RESERVED_HIGH_MASK | RELATIVE_RESERVED_LOW_MASK;
262
263 pub fn new(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Self {
271 assert!(row <= ROW_MAX, "Row {row} exceeds 20 bits");
272 assert!(col <= COL_MAX, "Col {col} exceeds 14 bits");
273 let mut raw = ((row as u64) << ROW_SHIFT) | ((col as u64) << COL_SHIFT);
274 if row_abs {
275 raw |= ROW_ABS_BIT;
276 }
277 if col_abs {
278 raw |= COL_ABS_BIT;
279 }
280 Self(raw)
281 }
282
283 pub fn try_new(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Result<Self, CoordError> {
285 if row > ROW_MAX {
286 return Err(CoordError::RowOverflow(row as i64));
287 }
288 if col > COL_MAX {
289 return Err(CoordError::ColOverflow(col as i64));
290 }
291 Ok(Self::new(row, col, row_abs, col_abs))
292 }
293
294 pub fn from_raw(raw: u64) -> Result<Self, CoordError> {
295 if raw & Self::RESERVED_MASK != 0 {
296 return Err(CoordError::ReservedBitsSet(raw & Self::RESERVED_MASK));
297 }
298 Ok(Self(raw))
299 }
300
301 #[inline(always)]
302 pub fn row(self) -> u32 {
303 ((self.0 & ROW_MASK) >> ROW_SHIFT) as u32
304 }
305
306 #[inline(always)]
307 pub fn col(self) -> u32 {
308 ((self.0 & COL_MASK) >> COL_SHIFT) as u32
309 }
310
311 #[inline(always)]
312 pub fn row_abs(self) -> bool {
313 self.0 & ROW_ABS_BIT != 0
314 }
315
316 #[inline(always)]
317 pub fn col_abs(self) -> bool {
318 self.0 & COL_ABS_BIT != 0
319 }
320
321 #[inline(always)]
322 pub fn with_row_abs(mut self, abs: bool) -> Self {
323 if abs {
324 self.0 |= ROW_ABS_BIT;
325 } else {
326 self.0 &= !ROW_ABS_BIT;
327 }
328 self
329 }
330
331 #[inline(always)]
332 pub fn with_col_abs(mut self, abs: bool) -> Self {
333 if abs {
334 self.0 |= COL_ABS_BIT;
335 } else {
336 self.0 &= !COL_ABS_BIT;
337 }
338 self
339 }
340
341 #[inline(always)]
343 pub fn offset(self, drow: i32, dcol: i32) -> Self {
344 let row = ((self.row() as i32) + drow) as u32;
345 let col = ((self.col() as i32) + dcol) as u32;
346 Self::new(row, col, self.row_abs(), self.col_abs())
347 }
348
349 #[inline(always)]
351 pub fn rebase(self, origin: RelativeCoord, target: RelativeCoord) -> Self {
352 let drow = target.row() as i32 - origin.row() as i32;
353 let dcol = target.col() as i32 - origin.col() as i32;
354 let new_row = if self.row_abs() {
355 self.row()
356 } else {
357 ((self.row() as i32) + drow) as u32
358 };
359 let new_col = if self.col_abs() {
360 self.col()
361 } else {
362 ((self.col() as i32) + dcol) as u32
363 };
364 Self::new(new_row, new_col, self.row_abs(), self.col_abs())
365 }
366
367 #[inline(always)]
368 pub fn into_absolute(self) -> Coord {
369 Coord::new(self.row(), self.col())
370 }
371
372 #[inline(always)]
373 pub fn as_u64(self) -> u64 {
374 self.0
375 }
376
377 pub fn col_to_letters(col: u32) -> String {
378 column_to_letters(col)
379 }
380
381 pub fn letters_to_col(s: &str) -> Option<u32> {
382 letters_to_column_index(s)
383 }
384
385 pub fn try_from_a1(input: &str) -> Result<Self, A1ParseError> {
387 let (row, col, row_abs, col_abs) = parse_a1_components(input)?;
388 let row0 = row.checked_sub(1).ok_or(A1ParseError::RowOutOfRange(0))?;
389 let col0 = col
390 .checked_sub(1)
391 .ok_or(A1ParseError::ColumnOutOfRange(0))?;
392 RelativeCoord::try_new(row0, col0, row_abs, col_abs).map_err(A1ParseError::from)
393 }
394}
395
396impl fmt::Display for RelativeCoord {
397 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
398 if self.col_abs() {
399 write!(f, "$")?;
400 }
401 write!(f, "{}", column_to_letters(self.col()))?;
402 if self.row_abs() {
403 write!(f, "$")?;
404 }
405 write!(f, "{}", self.row() + 1)
406 }
407}
408
409impl From<Coord> for RelativeCoord {
410 fn from(coord: Coord) -> Self {
411 Self::new(coord.row(), coord.col(), true, true)
412 }
413}
414
415impl TryFrom<(u32, u32, bool, bool)> for RelativeCoord {
416 type Error = CoordError;
417
418 fn try_from(value: (u32, u32, bool, bool)) -> Result<Self, Self::Error> {
419 Self::try_new(value.0, value.1, value.2, value.3)
420 }
421}
422
423fn column_to_letters(mut col: u32) -> String {
424 let mut buf = Vec::new();
425 loop {
426 let rem = (col % 26) as u8;
427 buf.push(b'A' + rem);
428 col /= 26;
429 if col == 0 {
430 break;
431 }
432 col -= 1;
433 }
434 buf.reverse();
435 String::from_utf8(buf).expect("only ASCII A-Z")
436}
437
438fn letters_to_column_index(s: &str) -> Option<u32> {
439 if s.is_empty() {
440 return None;
441 }
442 let mut col: u32 = 0;
443 for (idx, byte) in s.bytes().enumerate() {
444 let upper = byte.to_ascii_uppercase();
445 if !upper.is_ascii_uppercase() {
446 return None;
447 }
448 let val = (upper - b'A') as u32;
449 col = col.checked_mul(26)?;
450 col = col.checked_add(val)?;
451 if idx != s.len() - 1 {
452 col = col.checked_add(1)?;
453 }
454 }
455 Some(col)
456}
457
458pub fn col_letters_from_1based(col: u32) -> Result<String, A1ParseError> {
460 if col == 0 || col > COL_MAX_1BASED {
461 return Err(A1ParseError::ColumnOutOfRange(col));
462 }
463 Ok(column_to_letters(col - 1))
464}
465
466pub fn col_index_from_letters_1based(col: &str) -> Result<u32, A1ParseError> {
468 if col.is_empty() {
469 return Err(A1ParseError::MissingColumn);
470 }
471 for ch in col.chars() {
472 if !ch.is_ascii_alphabetic() {
473 return Err(A1ParseError::InvalidColumnChar(ch));
474 }
475 }
476 match letters_to_column_index(col) {
477 Some(zero_based) if zero_based <= COL_MAX => Ok(zero_based + 1),
478 Some(zero_based) => Err(A1ParseError::ColumnOutOfRange(zero_based + 1)),
479 None => Err(A1ParseError::ColumnOutOfRange(COL_MAX_1BASED + 1)),
480 }
481}
482
483fn parse_a1_components(input: &str) -> Result<(u32, u32, bool, bool), A1ParseError> {
484 if input.is_empty() {
485 return Err(A1ParseError::Empty);
486 }
487
488 let bytes = input.as_bytes();
489 let len = bytes.len();
490 let mut idx = 0usize;
491
492 let mut col_abs = false;
493 let mut row_abs = false;
494
495 if bytes[idx] == b'$' {
496 col_abs = true;
497 idx += 1;
498 if idx >= len {
499 return Err(A1ParseError::MissingColumn);
500 }
501 }
502
503 let col_start = idx;
504 while idx < len && bytes[idx].is_ascii_alphabetic() {
505 idx += 1;
506 }
507
508 if idx == col_start {
509 return Err(A1ParseError::MissingColumn);
510 }
511
512 let col_letters = &input[col_start..idx];
513
514 if idx < len && bytes[idx] == b'$' {
515 row_abs = true;
516 idx += 1;
517 }
518
519 if idx >= len {
520 return Err(A1ParseError::MissingRow);
521 }
522
523 let row_start = idx;
524 while idx < len && bytes[idx].is_ascii_digit() {
525 idx += 1;
526 }
527
528 if row_start == idx {
529 let invalid = input[row_start..].chars().next().unwrap_or('\0');
530 if invalid == '\0' {
531 return Err(A1ParseError::MissingRow);
532 }
533 return Err(A1ParseError::InvalidRowChar(invalid));
534 }
535
536 if idx != len {
537 return Err(A1ParseError::TrailingCharacters(input[idx..].to_string()));
538 }
539
540 let col = col_index_from_letters_1based(col_letters)?;
541 let row_str = &input[row_start..idx];
542 if !row_str.bytes().all(|b| b.is_ascii_digit()) {
543 let invalid = row_str.chars().find(|c| !c.is_ascii_digit()).unwrap();
544 return Err(A1ParseError::InvalidRowChar(invalid));
545 }
546 let row: u32 = row_str
547 .parse()
548 .map_err(|_| A1ParseError::RowOutOfRange(ROW_MAX_1BASED + 1))?;
549
550 if row == 0 || row > ROW_MAX_1BASED {
551 return Err(A1ParseError::RowOutOfRange(row));
552 }
553
554 Ok((row, col, row_abs, col_abs))
555}
556
557pub fn parse_a1_1based(input: &str) -> Result<(u32, u32, bool, bool), A1ParseError> {
559 parse_a1_components(input)
560}
561
562impl TryFrom<&str> for Coord {
563 type Error = A1ParseError;
564
565 fn try_from(value: &str) -> Result<Self, Self::Error> {
566 Coord::try_from_a1(value)
567 }
568}
569
570impl FromStr for Coord {
571 type Err = A1ParseError;
572
573 fn from_str(s: &str) -> Result<Self, Self::Err> {
574 Coord::try_from_a1(s)
575 }
576}
577
578impl FromStr for RelativeCoord {
579 type Err = A1ParseError;
580
581 fn from_str(s: &str) -> Result<Self, Self::Err> {
582 RelativeCoord::try_from_a1(s)
583 }
584}
585
586impl TryFrom<&str> for RelativeCoord {
587 type Error = A1ParseError;
588
589 fn try_from(value: &str) -> Result<Self, Self::Error> {
590 RelativeCoord::try_from_a1(value)
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 #[test]
599 fn absolute_roundtrip() {
600 let coord = Coord::new(1_048_575, 16_383);
601 assert_eq!(coord.row(), 1_048_575);
602 assert_eq!(coord.col(), 16_383);
603 let expected = (0xFFFFF_u64 << ROW_SHIFT) | (0x3FFF_u64 << COL_SHIFT);
604 assert_eq!(coord.as_u64(), expected);
605 }
606
607 #[test]
608 fn absolute_invalid_const() {
609 let invalid = Coord::INVALID;
610 assert!(!invalid.is_valid());
611 assert_eq!(invalid.as_u64(), u64::MAX);
612 }
613
614 #[test]
615 fn absolute_try_new() {
616 assert!(Coord::try_new(ROW_MAX, COL_MAX).is_ok());
617 assert_eq!(
618 Coord::try_new(ROW_MAX + 1, 0),
619 Err(CoordError::RowOverflow((ROW_MAX + 1) as i64))
620 );
621 assert_eq!(
622 Coord::try_new(0, COL_MAX + 1),
623 Err(CoordError::ColOverflow((COL_MAX + 1) as i64))
624 );
625 }
626
627 #[test]
628 fn relative_flags() {
629 let coord = RelativeCoord::new(0, 0, true, false);
630 assert!(coord.row_abs());
631 assert!(!coord.col_abs());
632 let toggled = coord.with_col_abs(true);
633 assert!(toggled.col_abs());
634 }
635
636 #[test]
637 fn relative_display() {
638 let coord = RelativeCoord::new(5, 27, true, false);
639 assert_eq!(coord.to_string(), "AB$6");
640 let coord = RelativeCoord::new(0, 0, false, false);
641 assert_eq!(coord.to_string(), "A1");
642 }
643
644 #[test]
645 fn rebase_behaviour() {
646 let origin = RelativeCoord::new(0, 0, false, false);
647 let target = RelativeCoord::new(1, 1, false, false);
648 let formula = RelativeCoord::new(2, 0, false, true);
649 let rebased = formula.rebase(origin, target);
650 assert_eq!(rebased, RelativeCoord::new(3, 0, false, true));
651 }
652
653 #[test]
654 fn column_letter_roundtrip() {
655 let letters = RelativeCoord::col_to_letters(27);
656 assert_eq!(letters, "AB");
657 let idx = RelativeCoord::letters_to_col(&letters).unwrap();
658 assert_eq!(idx, 27);
659 assert!(RelativeCoord::letters_to_col("a1").is_none());
660 }
661
662 #[test]
663 fn col_letters_from_1based_roundtrip() {
664 assert_eq!(col_letters_from_1based(1).unwrap(), "A");
665 assert_eq!(col_letters_from_1based(26).unwrap(), "Z");
666 assert_eq!(col_letters_from_1based(27).unwrap(), "AA");
667 assert_eq!(col_letters_from_1based(52).unwrap(), "AZ");
668 assert_eq!(col_letters_from_1based(53).unwrap(), "BA");
669 }
670
671 #[test]
672 fn col_index_from_letters_handles_lowercase() {
673 assert_eq!(col_index_from_letters_1based("a").unwrap(), 1);
674 assert_eq!(col_index_from_letters_1based("zz").unwrap(), 702);
675 assert_eq!(
676 col_index_from_letters_1based("XFD").unwrap(),
677 COL_MAX_1BASED
678 );
679 assert!(col_index_from_letters_1based("xfda").is_err());
680 assert!(col_index_from_letters_1based("!").is_err());
681 }
682
683 #[test]
684 fn parse_a1_components_basic() {
685 let (row, col, row_abs, col_abs) = parse_a1_1based("A1").unwrap();
686 assert_eq!((row, col, row_abs, col_abs), (1, 1, false, false));
687
688 let (row, col, row_abs, col_abs) = parse_a1_1based("$C$10").unwrap();
689 assert_eq!((row, col, row_abs, col_abs), (10, 3, true, true));
690
691 let (row, col, row_abs, col_abs) = parse_a1_1based("d$5").unwrap();
692 assert_eq!((row, col, row_abs, col_abs), (5, 4, true, false));
693 }
694
695 #[test]
696 fn parse_a1_components_errors() {
697 assert!(matches!(parse_a1_1based(""), Err(A1ParseError::Empty)));
698 assert!(matches!(
699 parse_a1_1based("$"),
700 Err(A1ParseError::MissingColumn)
701 ));
702 assert!(matches!(
703 parse_a1_1based("A"),
704 Err(A1ParseError::MissingRow)
705 ));
706 assert!(matches!(
707 parse_a1_1based("A0"),
708 Err(A1ParseError::RowOutOfRange(0))
709 ));
710 assert!(matches!(
711 parse_a1_1based("XFE1"),
712 Err(A1ParseError::ColumnOutOfRange(_))
713 ));
714 }
715
716 #[test]
717 fn coord_try_from_a1_matches_relative() {
718 let coord = Coord::try_from_a1("$B$2").unwrap();
719 assert_eq!((coord.row(), coord.col()), (1, 1));
720
721 let rel = RelativeCoord::try_from_a1("$B$2").unwrap();
722 assert!(rel.row_abs() && rel.col_abs());
723 assert_eq!((rel.row(), rel.col()), (1, 1));
724 }
725}