formualizer_common/
range.rs1use crate::address::{AxisBound, SheetAddressError, SheetLocator, SheetRangeRef};
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
8pub struct RangeAddress {
9 pub sheet: String,
10 pub start_row: u32,
11 pub start_col: u32,
12 pub end_row: u32,
13 pub end_col: u32,
14}
15
16impl RangeAddress {
17 pub fn new(
18 sheet: impl Into<String>,
19 start_row: u32,
20 start_col: u32,
21 end_row: u32,
22 end_col: u32,
23 ) -> Result<Self, &'static str> {
24 if start_row == 0 || start_col == 0 || end_row == 0 || end_col == 0 {
25 return Err("Row and column indices must be 1-based");
26 }
27 if start_row > end_row || start_col > end_col {
28 return Err("Range must be ordered: start <= end");
29 }
30 Ok(Self {
31 sheet: sheet.into(),
32 start_row,
33 start_col,
34 end_row,
35 end_col,
36 })
37 }
38
39 pub fn width(&self) -> u32 {
40 self.end_col - self.start_col + 1
41 }
42
43 pub fn height(&self) -> u32 {
44 self.end_row - self.start_row + 1
45 }
46
47 pub fn to_sheet_range(&self) -> SheetRangeRef<'_> {
49 let sheet = SheetLocator::from_name(self.sheet.as_str());
50 let start_row = Some(AxisBound::new(self.start_row - 1, true));
51 let start_col = Some(AxisBound::new(self.start_col - 1, true));
52 let end_row = Some(AxisBound::new(self.end_row - 1, true));
53 let end_col = Some(AxisBound::new(self.end_col - 1, true));
54 SheetRangeRef::new(sheet, start_row, start_col, end_row, end_col)
55 }
56}
57
58impl<'a> TryFrom<SheetRangeRef<'a>> for RangeAddress {
59 type Error = SheetAddressError;
60
61 fn try_from(value: SheetRangeRef<'a>) -> Result<Self, Self::Error> {
62 let sheet = value
63 .sheet
64 .name()
65 .ok_or(SheetAddressError::MissingSheetName)?;
66 let (sr, sc, er, ec) = match (
67 value.start_row,
68 value.start_col,
69 value.end_row,
70 value.end_col,
71 ) {
72 (Some(sr), Some(sc), Some(er), Some(ec)) => (sr, sc, er, ec),
73 _ => return Err(SheetAddressError::UnboundedRange),
74 };
75 if sr.index > er.index || sc.index > ec.index {
76 return Err(SheetAddressError::RangeOrder);
77 }
78 Ok(RangeAddress {
79 sheet: sheet.to_owned(),
80 start_row: sr.to_excel_1based(),
81 start_col: sc.to_excel_1based(),
82 end_row: er.to_excel_1based(),
83 end_col: ec.to_excel_1based(),
84 })
85 }
86}
87
88impl<'a> From<&'a RangeAddress> for SheetRangeRef<'a> {
89 fn from(value: &'a RangeAddress) -> Self {
90 value.to_sheet_range()
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn convert_to_sheet_range() {
100 let range = RangeAddress::new("Sheet1", 1, 1, 3, 4).unwrap();
101 let sheet_range = range.to_sheet_range();
102 assert_eq!(sheet_range.start_col.unwrap().index, 0);
103 assert_eq!(sheet_range.end_col.unwrap().index, 3);
104 assert_eq!(sheet_range.start_row.unwrap().index, 0);
105 assert_eq!(sheet_range.end_row.unwrap().index, 2);
106 assert_eq!(sheet_range.sheet.name(), Some("Sheet1"));
107 assert!(sheet_range.start_row.unwrap().abs);
108 assert!(sheet_range.start_col.unwrap().abs);
109 }
110
111 #[test]
112 fn convert_from_sheet_range_requires_name() {
113 let owned = RangeAddress::new("Sheet1", 2, 2, 2, 5).unwrap();
114 let sheet_range = owned.to_sheet_range();
115 let reconstructed = RangeAddress::try_from(sheet_range.clone()).unwrap();
116 assert_eq!(owned, reconstructed);
117
118 let without_name = SheetRangeRef::new(
119 SheetLocator::from_id(3),
120 sheet_range.start_row,
121 sheet_range.start_col,
122 sheet_range.end_row,
123 sheet_range.end_col,
124 );
125 let err = RangeAddress::try_from(without_name).unwrap_err();
126 assert_eq!(err, SheetAddressError::MissingSheetName);
127 }
128}