Skip to main content

formualizer_common/
range.rs

1use 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    /// Convert into the richer [`SheetRangeRef`] representation.
48    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}