1use std::borrow::Cow;
4use std::error::Error;
5use std::fmt;
6
7use crate::coord::{A1ParseError, CoordError, RelativeCoord};
8
9pub type SheetId = u16;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum SheetAddressError {
15 ZeroIndex,
17 RangeOrder,
19 MismatchedSheets,
21 MissingSheetName,
23 UnboundedRange,
25 Coord(CoordError),
27 Parse(A1ParseError),
29}
30
31impl fmt::Display for SheetAddressError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 SheetAddressError::ZeroIndex => {
35 write!(f, "row and column indices must be 1-based (>= 1)")
36 }
37 SheetAddressError::RangeOrder => {
38 write!(
39 f,
40 "range must be ordered so the start is above/left of the end"
41 )
42 }
43 SheetAddressError::MismatchedSheets => {
44 write!(f, "range bounds refer to different sheets")
45 }
46 SheetAddressError::MissingSheetName => {
47 write!(f, "sheet name required to materialise textual address")
48 }
49 SheetAddressError::UnboundedRange => {
50 write!(f, "range requires explicit bounds")
51 }
52 SheetAddressError::Coord(err) => err.fmt(f),
53 SheetAddressError::Parse(err) => err.fmt(f),
54 }
55 }
56}
57
58impl Error for SheetAddressError {}
59
60impl From<CoordError> for SheetAddressError {
61 fn from(value: CoordError) -> Self {
62 SheetAddressError::Coord(value)
63 }
64}
65
66impl From<A1ParseError> for SheetAddressError {
67 fn from(value: A1ParseError) -> Self {
68 SheetAddressError::Parse(value)
69 }
70}
71
72#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
74pub enum SheetLocator<'a> {
75 #[default]
77 Current,
78 Id(SheetId),
80 Name(Cow<'a, str>),
82}
83
84impl<'a> SheetLocator<'a> {
85 pub const fn current() -> Self {
87 SheetLocator::Current
88 }
89
90 pub const fn from_id(id: SheetId) -> Self {
92 SheetLocator::Id(id)
93 }
94
95 pub fn from_name(name: impl Into<Cow<'a, str>>) -> Self {
97 SheetLocator::Name(name.into())
98 }
99
100 pub const fn id(&self) -> Option<SheetId> {
102 match self {
103 SheetLocator::Id(id) => Some(*id),
104 SheetLocator::Current | SheetLocator::Name(_) => None,
105 }
106 }
107
108 pub fn name(&self) -> Option<&str> {
110 match self {
111 SheetLocator::Name(name) => Some(name.as_ref()),
112 SheetLocator::Current | SheetLocator::Id(_) => None,
113 }
114 }
115
116 pub const fn is_current(&self) -> bool {
118 matches!(self, SheetLocator::Current)
119 }
120
121 pub fn as_ref(&self) -> SheetLocator<'_> {
123 match self {
124 SheetLocator::Current => SheetLocator::Current,
125 SheetLocator::Id(id) => SheetLocator::Id(*id),
126 SheetLocator::Name(name) => SheetLocator::Name(Cow::Borrowed(name.as_ref())),
127 }
128 }
129
130 pub fn into_owned(self) -> SheetLocator<'static> {
132 match self {
133 SheetLocator::Current => SheetLocator::Current,
134 SheetLocator::Id(id) => SheetLocator::Id(id),
135 SheetLocator::Name(name) => SheetLocator::Name(Cow::Owned(name.into_owned())),
136 }
137 }
138}
139
140impl<'a> From<SheetId> for SheetLocator<'a> {
141 fn from(value: SheetId) -> Self {
142 SheetLocator::from_id(value)
143 }
144}
145
146impl<'a> From<&'a str> for SheetLocator<'a> {
147 fn from(value: &'a str) -> Self {
148 SheetLocator::from_name(value)
149 }
150}
151
152impl<'a> From<String> for SheetLocator<'a> {
153 fn from(value: String) -> Self {
154 SheetLocator::from_name(value)
155 }
156}
157
158#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
160pub struct AxisBound {
161 pub index: u32,
163 pub abs: bool,
165}
166
167impl AxisBound {
168 pub const fn new(index: u32, abs: bool) -> Self {
169 AxisBound { index, abs }
170 }
171
172 pub fn from_excel_1based(index: u32, abs: bool) -> Result<Self, SheetAddressError> {
174 let index0 = index.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
175 Ok(AxisBound::new(index0, abs))
176 }
177
178 pub const fn to_excel_1based(self) -> u32 {
180 self.index + 1
181 }
182}
183
184#[derive(Clone, Debug, Eq, PartialEq, Hash)]
186pub struct SheetCellRef<'a> {
187 pub sheet: SheetLocator<'a>,
188 pub coord: RelativeCoord,
189}
190
191impl<'a> SheetCellRef<'a> {
192 pub const fn new(sheet: SheetLocator<'a>, coord: RelativeCoord) -> Self {
193 SheetCellRef { sheet, coord }
194 }
195
196 pub fn from_excel(
198 sheet: SheetLocator<'a>,
199 row: u32,
200 col: u32,
201 row_abs: bool,
202 col_abs: bool,
203 ) -> Result<Self, SheetAddressError> {
204 let row0 = row.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
205 let col0 = col.checked_sub(1).ok_or(SheetAddressError::ZeroIndex)?;
206 let coord = RelativeCoord::try_new(row0, col0, row_abs, col_abs)?;
207 Ok(SheetCellRef::new(sheet, coord))
208 }
209
210 pub fn try_from_a1(
212 sheet: SheetLocator<'a>,
213 reference: &str,
214 ) -> Result<Self, SheetAddressError> {
215 let coord = RelativeCoord::try_from_a1(reference)?;
216 Ok(SheetCellRef::new(sheet, coord))
217 }
218
219 pub fn as_ref(&self) -> SheetCellRef<'_> {
221 SheetCellRef {
222 sheet: self.sheet.as_ref(),
223 coord: self.coord,
224 }
225 }
226
227 pub fn into_owned(self) -> SheetCellRef<'static> {
229 SheetCellRef {
230 sheet: self.sheet.into_owned(),
231 coord: self.coord,
232 }
233 }
234}
235
236#[derive(Clone, Debug, Eq, PartialEq, Hash)]
238pub struct SheetRangeRef<'a> {
239 pub sheet: SheetLocator<'a>,
240 pub start_row: Option<AxisBound>,
241 pub start_col: Option<AxisBound>,
242 pub end_row: Option<AxisBound>,
243 pub end_col: Option<AxisBound>,
244}
245
246impl<'a> SheetRangeRef<'a> {
247 pub const fn new(
248 sheet: SheetLocator<'a>,
249 start_row: Option<AxisBound>,
250 start_col: Option<AxisBound>,
251 end_row: Option<AxisBound>,
252 end_col: Option<AxisBound>,
253 ) -> Self {
254 SheetRangeRef {
255 sheet,
256 start_row,
257 start_col,
258 end_row,
259 end_col,
260 }
261 }
262
263 pub fn from_cells(
265 start: SheetCellRef<'a>,
266 end: SheetCellRef<'a>,
267 ) -> Result<Self, SheetAddressError> {
268 if start.sheet != end.sheet {
269 return Err(SheetAddressError::MismatchedSheets);
270 }
271 let sr = AxisBound::new(start.coord.row(), start.coord.row_abs());
272 let sc = AxisBound::new(start.coord.col(), start.coord.col_abs());
273 let er = AxisBound::new(end.coord.row(), end.coord.row_abs());
274 let ec = AxisBound::new(end.coord.col(), end.coord.col_abs());
275 SheetRangeRef::from_parts(start.sheet, Some(sr), Some(sc), Some(er), Some(ec))
276 }
277
278 #[allow(clippy::too_many_arguments)]
280 pub fn from_excel_rect(
281 sheet: SheetLocator<'a>,
282 start_row: u32,
283 start_col: u32,
284 end_row: u32,
285 end_col: u32,
286 start_row_abs: bool,
287 start_col_abs: bool,
288 end_row_abs: bool,
289 end_col_abs: bool,
290 ) -> Result<Self, SheetAddressError> {
291 let sr = AxisBound::from_excel_1based(start_row, start_row_abs)?;
292 let sc = AxisBound::from_excel_1based(start_col, start_col_abs)?;
293 let er = AxisBound::from_excel_1based(end_row, end_row_abs)?;
294 let ec = AxisBound::from_excel_1based(end_col, end_col_abs)?;
295 SheetRangeRef::from_parts(sheet, Some(sr), Some(sc), Some(er), Some(ec))
296 }
297
298 pub fn from_parts(
300 sheet: SheetLocator<'a>,
301 start_row: Option<AxisBound>,
302 start_col: Option<AxisBound>,
303 end_row: Option<AxisBound>,
304 end_col: Option<AxisBound>,
305 ) -> Result<Self, SheetAddressError> {
306 if let (Some(sr), Some(er)) = (start_row, end_row)
307 && sr.index > er.index
308 {
309 return Err(SheetAddressError::RangeOrder);
310 }
311 if let (Some(sc), Some(ec)) = (start_col, end_col)
312 && sc.index > ec.index
313 {
314 return Err(SheetAddressError::RangeOrder);
315 }
316 Ok(SheetRangeRef::new(
317 sheet, start_row, start_col, end_row, end_col,
318 ))
319 }
320
321 pub fn as_ref(&self) -> SheetRangeRef<'_> {
323 SheetRangeRef {
324 sheet: self.sheet.as_ref(),
325 start_row: self.start_row,
326 start_col: self.start_col,
327 end_row: self.end_row,
328 end_col: self.end_col,
329 }
330 }
331
332 pub fn into_owned(self) -> SheetRangeRef<'static> {
334 SheetRangeRef {
335 sheet: self.sheet.into_owned(),
336 start_row: self.start_row,
337 start_col: self.start_col,
338 end_row: self.end_row,
339 end_col: self.end_col,
340 }
341 }
342}
343
344#[derive(Clone, Debug, Eq, PartialEq, Hash)]
346pub enum SheetRef<'a> {
347 Cell(SheetCellRef<'a>),
348 Range(SheetRangeRef<'a>),
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354
355 #[test]
356 fn sheet_locator_roundtrip() {
357 let loc = SheetLocator::from_id(7);
358 assert_eq!(loc.id(), Some(7));
359 assert_eq!(loc.name(), None);
360 assert_eq!(loc.as_ref(), SheetLocator::Id(7));
361
362 let name = SheetLocator::from_name("Data");
363 assert_eq!(name.id(), None);
364 assert_eq!(name.name(), Some("Data"));
365 let owned = name.clone().into_owned();
366 assert_eq!(owned.name(), Some("Data"));
367 assert_eq!(name, owned.as_ref());
368
369 let current = SheetLocator::current();
370 assert!(current.is_current());
371 assert_eq!(current.id(), None);
372 }
373
374 #[test]
375 fn cell_from_excel_preserves_flags() {
376 let a1 = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 1, 1, false, false)
377 .expect("valid cell");
378 assert_eq!(a1.coord.row(), 0);
379 assert_eq!(a1.coord.col(), 0);
380 assert!(!a1.coord.row_abs());
381 assert!(!a1.coord.col_abs());
382
383 let abs = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 3, 2, true, false)
384 .expect("valid absolute cell");
385 assert_eq!(abs.coord.row(), 2);
386 assert!(abs.coord.row_abs());
387 assert!(!abs.coord.col_abs());
388 }
389
390 #[test]
391 fn cell_from_excel_rejects_zero() {
392 let err = SheetCellRef::from_excel(SheetLocator::from_name("Sheet1"), 0, 1, false, false)
393 .unwrap_err();
394 assert_eq!(err, SheetAddressError::ZeroIndex);
395 }
396
397 #[test]
398 fn range_from_cells_validates_sheet_and_order() {
399 let sheet = SheetLocator::from_name("Sheet1");
400 let start = SheetCellRef::try_from_a1(sheet.as_ref(), "A1").unwrap();
401 let end = SheetCellRef::try_from_a1(sheet.as_ref(), "$B$3").unwrap();
402 let range = SheetRangeRef::from_cells(start.clone(), end.clone()).unwrap();
403 assert_eq!(range.start_row.unwrap().index, 0);
404 assert_eq!(range.end_row.unwrap().index, 2);
405
406 let other_sheet =
407 SheetCellRef::try_from_a1(SheetLocator::from_name("Other"), "C2").unwrap();
408 assert_eq!(
409 SheetRangeRef::from_cells(start, other_sheet).unwrap_err(),
410 SheetAddressError::MismatchedSheets
411 );
412
413 let inverted = SheetRangeRef::from_parts(
414 SheetLocator::from_name("Sheet1"),
415 Some(AxisBound::new(end.coord.row(), end.coord.row_abs())),
416 Some(AxisBound::new(end.coord.col(), end.coord.col_abs())),
417 Some(AxisBound::new(0, false)),
418 Some(AxisBound::new(0, false)),
419 );
420 assert_eq!(inverted.unwrap_err(), SheetAddressError::RangeOrder);
421 }
422}