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