spreadsheet_formulas/
cellref.rs1use core::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct CellRef {
9 pub col: usize, pub row: usize, pub abs_col: bool, pub abs_row: bool, }
14
15impl CellRef {
16 pub fn parse(input: &str) -> Option<Self> {
18 let input = input.trim();
19 if input.is_empty() {
20 return None;
21 }
22
23 let mut chars = input.chars().peekable();
24 let mut abs_col = false;
25 let mut abs_row = false;
26
27 if chars.peek() == Some(&'$') {
29 abs_col = true;
30 chars.next();
31 }
32
33 let mut col_str = String::new();
35 while let Some(&c) = chars.peek() {
36 if c.is_ascii_alphabetic() {
37 col_str.push(chars.next().unwrap().to_ascii_uppercase());
38 } else {
39 break;
40 }
41 }
42 if col_str.is_empty() {
43 return None;
44 }
45
46 if chars.peek() == Some(&'$') {
48 abs_row = true;
49 chars.next();
50 }
51
52 let mut row_str = String::new();
54 while let Some(&c) = chars.peek() {
55 if c.is_ascii_digit() {
56 row_str.push(chars.next().unwrap());
57 } else {
58 break;
59 }
60 }
61
62 if row_str.is_empty() {
63 return None;
64 }
65
66 if chars.next().is_some() {
68 return None;
69 }
70
71 let col = col_letters_to_index(&col_str)?;
72 let row: usize = row_str.parse().ok()?;
73 if row == 0 {
74 return None;
75 }
76
77 Some(CellRef {
78 col,
79 row: row - 1, abs_col,
81 abs_row,
82 })
83 }
84
85 pub fn to_a1(&self) -> String {
87 format!(
88 "{}{}{}{}",
89 if self.abs_col { "$" } else { "" },
90 index_to_col_letters(self.col),
91 if self.abs_row { "$" } else { "" },
92 self.row + 1,
93 )
94 }
95}
96
97impl fmt::Display for CellRef {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(f, "{}", self.to_a1())
100 }
101}
102
103pub fn col_letters_to_index(letters: &str) -> Option<usize> {
105 let mut index: usize = 0;
106 for c in letters.chars() {
107 if !c.is_ascii_alphabetic() {
108 return None;
109 }
110 index = index * 26 + (c.to_ascii_uppercase() as usize - 'A' as usize + 1);
111 }
112 Some(index - 1)
113}
114
115pub fn index_to_col_letters(mut index: usize) -> String {
117 let mut result = String::new();
118 loop {
119 result.insert(0, (b'A' + (index % 26) as u8) as char);
120 if index < 26 {
121 break;
122 }
123 index = index / 26 - 1;
124 }
125 result
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn parse_simple() {
134 let c = CellRef::parse("A1").unwrap();
135 assert_eq!(c.col, 0);
136 assert_eq!(c.row, 0);
137 assert!(!c.abs_col);
138 assert!(!c.abs_row);
139 }
140
141 #[test]
142 fn parse_absolute_both() {
143 let c = CellRef::parse("$B$12").unwrap();
144 assert_eq!(c.col, 1);
145 assert_eq!(c.row, 11);
146 assert!(c.abs_col);
147 assert!(c.abs_row);
148 }
149
150 #[test]
151 fn parse_absolute_col() {
152 let c = CellRef::parse("$C5").unwrap();
153 assert_eq!(c.col, 2);
154 assert_eq!(c.row, 4);
155 assert!(c.abs_col);
156 assert!(!c.abs_row);
157 }
158
159 #[test]
160 fn parse_absolute_row() {
161 let c = CellRef::parse("D$7").unwrap();
162 assert_eq!(c.col, 3);
163 assert_eq!(c.row, 6);
164 assert!(!c.abs_col);
165 assert!(c.abs_row);
166 }
167
168 #[test]
169 fn parse_multi_letter_col() {
170 let c = CellRef::parse("AA1").unwrap();
171 assert_eq!(c.col, 26);
172 assert_eq!(c.row, 0);
173 }
174
175 #[test]
176 fn parse_invalid_no_row() {
177 assert!(CellRef::parse("A").is_none());
178 }
179
180 #[test]
181 fn parse_invalid_empty() {
182 assert!(CellRef::parse("").is_none());
183 }
184
185 #[test]
186 fn roundtrip() {
187 for input in &["A1", "$B$12", "C$5", "$D3", "AA100"] {
188 assert_eq!(CellRef::parse(input).unwrap().to_a1(), *input);
189 }
190 }
191
192 #[test]
193 fn col_index_roundtrip() {
194 for i in 0..702 {
195 assert_eq!(col_letters_to_index(&index_to_col_letters(i)), Some(i));
196 }
197 }
198}