1use std::fmt;
19
20use crate::{RangeAddress, SheetAddressError, format_a1_sheet_name};
21
22const MAX_ROW: u32 = 1_048_576;
23const MAX_COLUMN: u32 = 16_384;
24
25fn validate_row(row: u32) -> Result<(), SheetAddressError> {
26 match row {
27 0 => Err(SheetAddressError::ZeroIndex),
28 1..=MAX_ROW => Ok(()),
29 _ => Err(SheetAddressError::RowOutOfBounds),
30 }
31}
32
33fn validate_column(column: u32) -> Result<(), SheetAddressError> {
34 match column {
35 0 => Err(SheetAddressError::ZeroIndex),
36 1..=MAX_COLUMN => Ok(()),
37 _ => Err(SheetAddressError::ColumnOutOfBounds),
38 }
39}
40
41fn unbounded_column_letters(mut column: u32) -> String {
42 let mut reversed = Vec::new();
43 while column > 0 {
44 column -= 1;
45 reversed.push(char::from(b'A' + (column % 26) as u8));
46 column /= 26;
47 }
48 reversed.into_iter().rev().collect()
49}
50
51#[cfg(feature = "serde")]
52use serde::{Deserialize, Serialize};
53
54#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
56#[derive(Clone, Debug, Eq, PartialEq, Hash)]
57pub struct CellAddress {
58 pub sheet: String,
59 pub row: u32,
60 pub column: u32,
61}
62
63impl CellAddress {
64 pub fn new(sheet: impl Into<String>, row: u32, column: u32) -> Result<Self, SheetAddressError> {
66 validate_row(row)?;
67 validate_column(column)?;
68 Ok(Self {
69 sheet: sheet.into(),
70 row,
71 column,
72 })
73 }
74
75 pub fn to_finite(&self) -> RangeAddress {
77 RangeAddress {
78 sheet: self.sheet.clone(),
79 start_row: self.row,
80 start_col: self.column,
81 end_row: self.row,
82 end_col: self.column,
83 }
84 }
85
86 pub fn from_finite(range: &RangeAddress) -> Option<Self> {
88 (range.start_row == range.end_row && range.start_col == range.end_col).then(|| Self {
89 sheet: range.sheet.clone(),
90 row: range.start_row,
91 column: range.start_col,
92 })
93 }
94}
95
96impl fmt::Display for CellAddress {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 let sheet = format_a1_sheet_name(&self.sheet);
104 if self.row == 0 || self.column == 0 {
105 return write!(f, "{sheet}!#REF!");
106 }
107 write!(
108 f,
109 "{sheet}!{}{}",
110 unbounded_column_letters(self.column),
111 self.row
112 )
113 }
114}
115
116impl From<CellAddress> for RangeAddress {
117 fn from(value: CellAddress) -> Self {
118 Self {
119 sheet: value.sheet,
120 start_row: value.row,
121 start_col: value.column,
122 end_row: value.row,
123 end_col: value.column,
124 }
125 }
126}
127
128impl TryFrom<RangeAddress> for CellAddress {
129 type Error = SheetAddressError;
130
131 fn try_from(value: RangeAddress) -> Result<Self, Self::Error> {
132 if value.start_row != value.end_row || value.start_col != value.end_col {
133 return Err(SheetAddressError::NonSingleCellRange);
134 }
135 Self::new(value.sheet, value.start_row, value.start_col)
136 }
137}
138
139#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
143#[derive(Clone, Debug, Eq, PartialEq, Hash)]
144pub struct RangeArea {
145 pub sheet: String,
146 pub start_row: Option<u32>,
147 pub start_column: Option<u32>,
148 pub end_row: Option<u32>,
149 pub end_column: Option<u32>,
150}
151
152impl RangeArea {
153 pub fn new(
155 sheet: impl Into<String>,
156 start_row: Option<u32>,
157 start_column: Option<u32>,
158 end_row: Option<u32>,
159 end_column: Option<u32>,
160 ) -> Result<Self, SheetAddressError> {
161 for row in [start_row, end_row].into_iter().flatten() {
162 validate_row(row)?;
163 }
164 for column in [start_column, end_column].into_iter().flatten() {
165 validate_column(column)?;
166 }
167 if start_row
168 .zip(end_row)
169 .is_some_and(|(start, end)| start > end)
170 || start_column
171 .zip(end_column)
172 .is_some_and(|(start, end)| start > end)
173 {
174 return Err(SheetAddressError::RangeOrder);
175 }
176 Ok(Self {
177 sheet: sheet.into(),
178 start_row,
179 start_column,
180 end_row,
181 end_column,
182 })
183 }
184
185 pub fn from_finite(range: &RangeAddress) -> Self {
187 Self {
188 sheet: range.sheet.clone(),
189 start_row: Some(range.start_row),
190 start_column: Some(range.start_col),
191 end_row: Some(range.end_row),
192 end_column: Some(range.end_col),
193 }
194 }
195
196 pub fn to_finite(&self) -> Option<RangeAddress> {
198 Some(RangeAddress {
199 sheet: self.sheet.clone(),
200 start_row: self.start_row?,
201 start_col: self.start_column?,
202 end_row: self.end_row?,
203 end_col: self.end_column?,
204 })
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn cell_display_uses_canonical_sheet_quoting() {
214 assert_eq!(
215 CellAddress::new("Data", 12, 28).unwrap().to_string(),
216 "Data!AB12"
217 );
218 assert_eq!(
219 CellAddress::new("My Sheet", 1, 1).unwrap().to_string(),
220 "'My Sheet'!A1"
221 );
222 assert_eq!(
223 CellAddress::new("O'Brien", 2, 3).unwrap().to_string(),
224 "'O''Brien'!C2"
225 );
226 assert_eq!(
227 CellAddress::new("TRUE", 3, 2).unwrap().to_string(),
228 "'TRUE'!B3"
229 );
230 }
231
232 #[test]
233 fn display_is_total_for_checked_and_unchecked_coordinates() {
234 let display = |row, column| {
235 CellAddress {
236 sheet: "Sheet".to_string(),
237 row,
238 column,
239 }
240 .to_string()
241 };
242 assert_eq!(display(1, 16_384), "Sheet!XFD1");
243 assert_eq!(display(1, 16_385), "Sheet!XFE1");
244 assert_eq!(display(1, u32::MAX), "Sheet!MWLQKWU1");
245 assert_eq!(display(1, 0), "Sheet!#REF!");
246 assert_eq!(display(0, 1), "Sheet!#REF!");
247 assert_eq!(display(0, 0), "Sheet!#REF!");
248 }
249
250 #[test]
251 fn constructors_validate_grid_bounds_and_order() {
252 assert_eq!(
253 CellAddress::new("S", 0, 1).unwrap_err(),
254 SheetAddressError::ZeroIndex
255 );
256 assert_eq!(
257 CellAddress::new("S", MAX_ROW + 1, 1).unwrap_err(),
258 SheetAddressError::RowOutOfBounds
259 );
260 assert_eq!(
261 CellAddress::new("S", 1, MAX_COLUMN + 1).unwrap_err(),
262 SheetAddressError::ColumnOutOfBounds
263 );
264 assert!(CellAddress::new("S", MAX_ROW, MAX_COLUMN).is_ok());
265 assert_eq!(
266 RangeArea::new("S", Some(4), Some(1), Some(3), Some(2)).unwrap_err(),
267 SheetAddressError::RangeOrder
268 );
269 assert_eq!(
270 RangeArea::new("S", None, Some(0), None, Some(2)).unwrap_err(),
271 SheetAddressError::ZeroIndex
272 );
273 assert_eq!(
274 RangeArea::new("S", Some(MAX_ROW + 1), None, None, None).unwrap_err(),
275 SheetAddressError::RowOutOfBounds
276 );
277 assert_eq!(
278 RangeArea::new("S", None, None, None, Some(MAX_COLUMN + 1)).unwrap_err(),
279 SheetAddressError::ColumnOutOfBounds
280 );
281 assert!(RangeArea::new("S", None, Some(2), Some(9), None).is_ok());
282 }
283
284 #[test]
285 fn finite_conversions_round_trip() {
286 let finite = RangeAddress::new("S", 2, 3, 5, 7).unwrap();
287 assert_eq!(RangeArea::from_finite(&finite).to_finite(), Some(finite));
288
289 let cell = CellAddress::new("S", 8, 9).unwrap();
290 assert_eq!(
291 CellAddress::from_finite(&cell.to_finite()),
292 Some(cell.clone())
293 );
294 assert_eq!(
295 CellAddress::try_from(RangeAddress::from(cell.clone())),
296 Ok(cell)
297 );
298 assert_eq!(
299 CellAddress::try_from(RangeAddress::new("S", 1, 1, 2, 1).unwrap()),
300 Err(SheetAddressError::NonSingleCellRange)
301 );
302 }
303}