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 {
126 assert!(row <= ROW_MAX, "Row {row} exceeds 20 bits");
127 assert!(col <= COL_MAX, "Col {col} exceeds 14 bits");
128 Self(((row as u64) << ROW_SHIFT) | ((col as u64) << COL_SHIFT))
129 }
130
131 #[inline(always)]
133 pub fn from_excel(row: u32, col: u32) -> Self {
134 let row0 = row.saturating_sub(1);
135 let col0 = col.saturating_sub(1);
136 Self::new(row0, col0)
137 }
138
139 pub fn try_new(row: u32, col: u32) -> Result<Self, CoordError> {
141 if row > ROW_MAX {
142 return Err(CoordError::RowOverflow(row as i64));
143 }
144 if col > COL_MAX {
145 return Err(CoordError::ColOverflow(col as i64));
146 }
147 Ok(Self::new(row, col))
148 }
149
150 pub fn from_raw(raw: u64) -> Result<Self, CoordError> {
152 if raw == u64::MAX {
153 return Ok(Self::INVALID);
154 }
155 if raw & Self::RESERVED_MASK != 0 {
156 return Err(CoordError::ReservedBitsSet(raw & Self::RESERVED_MASK));
157 }
158 Ok(Self(raw))
159 }
160
161 #[inline(always)]
162 pub fn row(self) -> u32 {
163 ((self.0 & ROW_MASK) >> ROW_SHIFT) as u32
164 }
165
166 #[inline(always)]
167 pub fn col(self) -> u32 {
168 ((self.0 & COL_MASK) >> COL_SHIFT) as u32
169 }
170
171 #[inline(always)]
172 pub fn as_u64(self) -> u64 {
173 self.0
174 }
175
176 #[inline(always)]
177 pub fn is_valid(self) -> bool {
178 self.0 != u64::MAX
179 }
180
181 #[inline(always)]
183 pub fn normalize(self) -> Self {
184 Self(self.0 & !Self::RESERVED_MASK)
185 }
186
187 #[inline(always)]
189 pub fn into_relative(self) -> RelativeCoord {
190 RelativeCoord::new(self.row(), self.col(), true, true)
191 }
192
193 pub fn try_from_a1(input: &str) -> Result<Self, A1ParseError> {
195 let (row, col, _, _) = parse_a1_components(input)?;
196 let row0 = row.checked_sub(1).ok_or(A1ParseError::RowOutOfRange(0))?;
197 let col0 = col
198 .checked_sub(1)
199 .ok_or(A1ParseError::ColumnOutOfRange(0))?;
200 Coord::try_new(row0, col0).map_err(A1ParseError::from)
201 }
202}
203
204impl From<Coord> for (u32, u32) {
205 fn from(coord: Coord) -> Self {
206 (coord.row(), coord.col())
207 }
208}
209
210impl TryFrom<(u32, u32)> for Coord {
211 type Error = CoordError;
212
213 fn try_from(value: (u32, u32)) -> Result<Self, Self::Error> {
214 Self::try_new(value.0, value.1)
215 }
216}
217
218impl TryFrom<(i64, i64)> for Coord {
219 type Error = CoordError;
220
221 fn try_from(value: (i64, i64)) -> Result<Self, Self::Error> {
222 let (row, col) = value;
223 if row < 0 {
224 return Err(CoordError::NegativeRow(row));
225 }
226 if col < 0 {
227 return Err(CoordError::NegativeCol(col));
228 }
229 let row = row as u32;
230 let col = col as u32;
231 Self::try_new(row, col)
232 }
233}
234
235impl From<RelativeCoord> for Coord {
236 fn from(value: RelativeCoord) -> Self {
237 Self::new(value.row(), value.col())
238 }
239}
240
241#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
247pub struct RelativeCoord(u64);
248
249impl RelativeCoord {
250 const RESERVED_MASK: u64 = RESERVED_HIGH_MASK | RELATIVE_RESERVED_LOW_MASK;
251
252 pub fn new(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Self {
253 assert!(row <= ROW_MAX, "Row {row} exceeds 20 bits");
254 assert!(col <= COL_MAX, "Col {col} exceeds 14 bits");
255 let mut raw = ((row as u64) << ROW_SHIFT) | ((col as u64) << COL_SHIFT);
256 if row_abs {
257 raw |= ROW_ABS_BIT;
258 }
259 if col_abs {
260 raw |= COL_ABS_BIT;
261 }
262 Self(raw)
263 }
264
265 pub fn try_new(row: u32, col: u32, row_abs: bool, col_abs: bool) -> Result<Self, CoordError> {
266 if row > ROW_MAX {
267 return Err(CoordError::RowOverflow(row as i64));
268 }
269 if col > COL_MAX {
270 return Err(CoordError::ColOverflow(col as i64));
271 }
272 Ok(Self::new(row, col, row_abs, col_abs))
273 }
274
275 pub fn from_raw(raw: u64) -> Result<Self, CoordError> {
276 if raw & Self::RESERVED_MASK != 0 {
277 return Err(CoordError::ReservedBitsSet(raw & Self::RESERVED_MASK));
278 }
279 Ok(Self(raw))
280 }
281
282 #[inline(always)]
283 pub fn row(self) -> u32 {
284 ((self.0 & ROW_MASK) >> ROW_SHIFT) as u32
285 }
286
287 #[inline(always)]
288 pub fn col(self) -> u32 {
289 ((self.0 & COL_MASK) >> COL_SHIFT) as u32
290 }
291
292 #[inline(always)]
293 pub fn row_abs(self) -> bool {
294 self.0 & ROW_ABS_BIT != 0
295 }
296
297 #[inline(always)]
298 pub fn col_abs(self) -> bool {
299 self.0 & COL_ABS_BIT != 0
300 }
301
302 #[inline(always)]
303 pub fn with_row_abs(mut self, abs: bool) -> Self {
304 if abs {
305 self.0 |= ROW_ABS_BIT;
306 } else {
307 self.0 &= !ROW_ABS_BIT;
308 }
309 self
310 }
311
312 #[inline(always)]
313 pub fn with_col_abs(mut self, abs: bool) -> Self {
314 if abs {
315 self.0 |= COL_ABS_BIT;
316 } else {
317 self.0 &= !COL_ABS_BIT;
318 }
319 self
320 }
321
322 #[inline(always)]
324 pub fn offset(self, drow: i32, dcol: i32) -> Self {
325 let row = ((self.row() as i32) + drow) as u32;
326 let col = ((self.col() as i32) + dcol) as u32;
327 Self::new(row, col, self.row_abs(), self.col_abs())
328 }
329
330 #[inline(always)]
332 pub fn rebase(self, origin: RelativeCoord, target: RelativeCoord) -> Self {
333 let drow = target.row() as i32 - origin.row() as i32;
334 let dcol = target.col() as i32 - origin.col() as i32;
335 let new_row = if self.row_abs() {
336 self.row()
337 } else {
338 ((self.row() as i32) + drow) as u32
339 };
340 let new_col = if self.col_abs() {
341 self.col()
342 } else {
343 ((self.col() as i32) + dcol) as u32
344 };
345 Self::new(new_row, new_col, self.row_abs(), self.col_abs())
346 }
347
348 #[inline(always)]
349 pub fn into_absolute(self) -> Coord {
350 Coord::new(self.row(), self.col())
351 }
352
353 #[inline(always)]
354 pub fn as_u64(self) -> u64 {
355 self.0
356 }
357
358 pub fn col_to_letters(col: u32) -> String {
359 column_to_letters(col)
360 }
361
362 pub fn letters_to_col(s: &str) -> Option<u32> {
363 letters_to_column_index(s)
364 }
365
366 pub fn try_from_a1(input: &str) -> Result<Self, A1ParseError> {
368 let (row, col, row_abs, col_abs) = parse_a1_components(input)?;
369 let row0 = row.checked_sub(1).ok_or(A1ParseError::RowOutOfRange(0))?;
370 let col0 = col
371 .checked_sub(1)
372 .ok_or(A1ParseError::ColumnOutOfRange(0))?;
373 RelativeCoord::try_new(row0, col0, row_abs, col_abs).map_err(A1ParseError::from)
374 }
375}
376
377impl fmt::Display for RelativeCoord {
378 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 if self.col_abs() {
380 write!(f, "$")?;
381 }
382 write!(f, "{}", column_to_letters(self.col()))?;
383 if self.row_abs() {
384 write!(f, "$")?;
385 }
386 write!(f, "{}", self.row() + 1)
387 }
388}
389
390impl From<Coord> for RelativeCoord {
391 fn from(coord: Coord) -> Self {
392 Self::new(coord.row(), coord.col(), true, true)
393 }
394}
395
396impl TryFrom<(u32, u32, bool, bool)> for RelativeCoord {
397 type Error = CoordError;
398
399 fn try_from(value: (u32, u32, bool, bool)) -> Result<Self, Self::Error> {
400 Self::try_new(value.0, value.1, value.2, value.3)
401 }
402}
403
404fn column_to_letters(mut col: u32) -> String {
405 let mut buf = Vec::new();
406 loop {
407 let rem = (col % 26) as u8;
408 buf.push(b'A' + rem);
409 col /= 26;
410 if col == 0 {
411 break;
412 }
413 col -= 1;
414 }
415 buf.reverse();
416 String::from_utf8(buf).expect("only ASCII A-Z")
417}
418
419fn letters_to_column_index(s: &str) -> Option<u32> {
420 if s.is_empty() {
421 return None;
422 }
423 let mut col: u32 = 0;
424 for (idx, byte) in s.bytes().enumerate() {
425 let upper = byte.to_ascii_uppercase();
426 if !upper.is_ascii_uppercase() {
427 return None;
428 }
429 let val = (upper - b'A') as u32;
430 col = col.checked_mul(26)?;
431 col = col.checked_add(val)?;
432 if idx != s.len() - 1 {
433 col = col.checked_add(1)?;
434 }
435 }
436 Some(col)
437}
438
439pub fn col_letters_from_1based(col: u32) -> Result<String, A1ParseError> {
441 if col == 0 || col > COL_MAX_1BASED {
442 return Err(A1ParseError::ColumnOutOfRange(col));
443 }
444 Ok(column_to_letters(col - 1))
445}
446
447pub fn col_index_from_letters_1based(col: &str) -> Result<u32, A1ParseError> {
449 if col.is_empty() {
450 return Err(A1ParseError::MissingColumn);
451 }
452 for ch in col.chars() {
453 if !ch.is_ascii_alphabetic() {
454 return Err(A1ParseError::InvalidColumnChar(ch));
455 }
456 }
457 match letters_to_column_index(col) {
458 Some(zero_based) if zero_based <= COL_MAX => Ok(zero_based + 1),
459 Some(zero_based) => Err(A1ParseError::ColumnOutOfRange(zero_based + 1)),
460 None => Err(A1ParseError::ColumnOutOfRange(COL_MAX_1BASED + 1)),
461 }
462}
463
464fn parse_a1_components(input: &str) -> Result<(u32, u32, bool, bool), A1ParseError> {
465 if input.is_empty() {
466 return Err(A1ParseError::Empty);
467 }
468
469 let bytes = input.as_bytes();
470 let len = bytes.len();
471 let mut idx = 0usize;
472
473 let mut col_abs = false;
474 let mut row_abs = false;
475
476 if bytes[idx] == b'$' {
477 col_abs = true;
478 idx += 1;
479 if idx >= len {
480 return Err(A1ParseError::MissingColumn);
481 }
482 }
483
484 let col_start = idx;
485 while idx < len && bytes[idx].is_ascii_alphabetic() {
486 idx += 1;
487 }
488
489 if idx == col_start {
490 return Err(A1ParseError::MissingColumn);
491 }
492
493 let col_letters = &input[col_start..idx];
494
495 if idx < len && bytes[idx] == b'$' {
496 row_abs = true;
497 idx += 1;
498 }
499
500 if idx >= len {
501 return Err(A1ParseError::MissingRow);
502 }
503
504 let row_start = idx;
505 while idx < len && bytes[idx].is_ascii_digit() {
506 idx += 1;
507 }
508
509 if row_start == idx {
510 let invalid = input[row_start..].chars().next().unwrap_or('\0');
511 if invalid == '\0' {
512 return Err(A1ParseError::MissingRow);
513 }
514 return Err(A1ParseError::InvalidRowChar(invalid));
515 }
516
517 if idx != len {
518 return Err(A1ParseError::TrailingCharacters(input[idx..].to_string()));
519 }
520
521 let col = col_index_from_letters_1based(col_letters)?;
522 let row_str = &input[row_start..idx];
523 if !row_str.bytes().all(|b| b.is_ascii_digit()) {
524 let invalid = row_str.chars().find(|c| !c.is_ascii_digit()).unwrap();
525 return Err(A1ParseError::InvalidRowChar(invalid));
526 }
527 let row: u32 = row_str
528 .parse()
529 .map_err(|_| A1ParseError::RowOutOfRange(ROW_MAX_1BASED + 1))?;
530
531 if row == 0 || row > ROW_MAX_1BASED {
532 return Err(A1ParseError::RowOutOfRange(row));
533 }
534
535 Ok((row, col, row_abs, col_abs))
536}
537
538pub fn parse_a1_1based(input: &str) -> Result<(u32, u32, bool, bool), A1ParseError> {
540 parse_a1_components(input)
541}
542
543impl TryFrom<&str> for Coord {
544 type Error = A1ParseError;
545
546 fn try_from(value: &str) -> Result<Self, Self::Error> {
547 Coord::try_from_a1(value)
548 }
549}
550
551impl FromStr for Coord {
552 type Err = A1ParseError;
553
554 fn from_str(s: &str) -> Result<Self, Self::Err> {
555 Coord::try_from_a1(s)
556 }
557}
558
559impl FromStr for RelativeCoord {
560 type Err = A1ParseError;
561
562 fn from_str(s: &str) -> Result<Self, Self::Err> {
563 RelativeCoord::try_from_a1(s)
564 }
565}
566
567impl TryFrom<&str> for RelativeCoord {
568 type Error = A1ParseError;
569
570 fn try_from(value: &str) -> Result<Self, Self::Error> {
571 RelativeCoord::try_from_a1(value)
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn absolute_roundtrip() {
581 let coord = Coord::new(1_048_575, 16_383);
582 assert_eq!(coord.row(), 1_048_575);
583 assert_eq!(coord.col(), 16_383);
584 let expected = (0xFFFFF_u64 << ROW_SHIFT) | (0x3FFF_u64 << COL_SHIFT);
585 assert_eq!(coord.as_u64(), expected);
586 }
587
588 #[test]
589 fn absolute_invalid_const() {
590 let invalid = Coord::INVALID;
591 assert!(!invalid.is_valid());
592 assert_eq!(invalid.as_u64(), u64::MAX);
593 }
594
595 #[test]
596 fn absolute_try_new() {
597 assert!(Coord::try_new(ROW_MAX, COL_MAX).is_ok());
598 assert_eq!(
599 Coord::try_new(ROW_MAX + 1, 0),
600 Err(CoordError::RowOverflow((ROW_MAX + 1) as i64))
601 );
602 assert_eq!(
603 Coord::try_new(0, COL_MAX + 1),
604 Err(CoordError::ColOverflow((COL_MAX + 1) as i64))
605 );
606 }
607
608 #[test]
609 fn relative_flags() {
610 let coord = RelativeCoord::new(0, 0, true, false);
611 assert!(coord.row_abs());
612 assert!(!coord.col_abs());
613 let toggled = coord.with_col_abs(true);
614 assert!(toggled.col_abs());
615 }
616
617 #[test]
618 fn relative_display() {
619 let coord = RelativeCoord::new(5, 27, true, false);
620 assert_eq!(coord.to_string(), "AB$6");
621 let coord = RelativeCoord::new(0, 0, false, false);
622 assert_eq!(coord.to_string(), "A1");
623 }
624
625 #[test]
626 fn rebase_behaviour() {
627 let origin = RelativeCoord::new(0, 0, false, false);
628 let target = RelativeCoord::new(1, 1, false, false);
629 let formula = RelativeCoord::new(2, 0, false, true);
630 let rebased = formula.rebase(origin, target);
631 assert_eq!(rebased, RelativeCoord::new(3, 0, false, true));
632 }
633
634 #[test]
635 fn column_letter_roundtrip() {
636 let letters = RelativeCoord::col_to_letters(27);
637 assert_eq!(letters, "AB");
638 let idx = RelativeCoord::letters_to_col(&letters).unwrap();
639 assert_eq!(idx, 27);
640 assert!(RelativeCoord::letters_to_col("a1").is_none());
641 }
642
643 #[test]
644 fn col_letters_from_1based_roundtrip() {
645 assert_eq!(col_letters_from_1based(1).unwrap(), "A");
646 assert_eq!(col_letters_from_1based(26).unwrap(), "Z");
647 assert_eq!(col_letters_from_1based(27).unwrap(), "AA");
648 assert_eq!(col_letters_from_1based(52).unwrap(), "AZ");
649 assert_eq!(col_letters_from_1based(53).unwrap(), "BA");
650 }
651
652 #[test]
653 fn col_index_from_letters_handles_lowercase() {
654 assert_eq!(col_index_from_letters_1based("a").unwrap(), 1);
655 assert_eq!(col_index_from_letters_1based("zz").unwrap(), 702);
656 assert_eq!(
657 col_index_from_letters_1based("XFD").unwrap(),
658 COL_MAX_1BASED
659 );
660 assert!(col_index_from_letters_1based("xfda").is_err());
661 assert!(col_index_from_letters_1based("!").is_err());
662 }
663
664 #[test]
665 fn parse_a1_components_basic() {
666 let (row, col, row_abs, col_abs) = parse_a1_1based("A1").unwrap();
667 assert_eq!((row, col, row_abs, col_abs), (1, 1, false, false));
668
669 let (row, col, row_abs, col_abs) = parse_a1_1based("$C$10").unwrap();
670 assert_eq!((row, col, row_abs, col_abs), (10, 3, true, true));
671
672 let (row, col, row_abs, col_abs) = parse_a1_1based("d$5").unwrap();
673 assert_eq!((row, col, row_abs, col_abs), (5, 4, true, false));
674 }
675
676 #[test]
677 fn parse_a1_components_errors() {
678 assert!(matches!(parse_a1_1based(""), Err(A1ParseError::Empty)));
679 assert!(matches!(
680 parse_a1_1based("$"),
681 Err(A1ParseError::MissingColumn)
682 ));
683 assert!(matches!(
684 parse_a1_1based("A"),
685 Err(A1ParseError::MissingRow)
686 ));
687 assert!(matches!(
688 parse_a1_1based("A0"),
689 Err(A1ParseError::RowOutOfRange(0))
690 ));
691 assert!(matches!(
692 parse_a1_1based("XFE1"),
693 Err(A1ParseError::ColumnOutOfRange(_))
694 ));
695 }
696
697 #[test]
698 fn coord_try_from_a1_matches_relative() {
699 let coord = Coord::try_from_a1("$B$2").unwrap();
700 assert_eq!((coord.row(), coord.col()), (1, 1));
701
702 let rel = RelativeCoord::try_from_a1("$B$2").unwrap();
703 assert!(rel.row_abs() && rel.col_abs());
704 assert_eq!((rel.row(), rel.col()), (1, 1));
705 }
706}