1use std::fmt;
6
7#[cfg(feature = "serde")]
8use serde::Serialize;
9
10pub const MAX_ROW: u32 = 1_048_576;
16pub const MAX_COL: u32 = 16_384;
18pub const MAX_COL_LABEL: &str = "XFD";
20
21#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
32#[cfg_attr(feature = "serde", derive(Serialize))]
33pub struct CellAddress {
34 pub row: u32,
35 pub col: u32,
36 pub a1: String,
37}
38
39impl CellAddress {
40 pub fn new(row: u32, col: u32) -> Option<Self> {
44 if row == 0 || row > MAX_ROW || col == 0 || col > MAX_COL {
45 return None;
46 }
47 let a1 = format!("{}{}", col_to_label(col), row);
48 Some(Self { row, col, a1 })
49 }
50
51 #[inline]
53 pub(crate) fn new_unchecked(row: u32, col: u32) -> Self {
54 Self {
55 a1: format!("{}{}", col_to_label(col), row),
56 row,
57 col,
58 }
59 }
60}
61
62impl fmt::Display for CellAddress {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.write_str(&self.a1)
65 }
66}
67
68#[derive(Clone, Debug)]
76#[cfg_attr(feature = "serde", derive(Serialize))]
77#[derive(PartialEq)]
78pub struct ComparedRange {
79 pub start: Option<(u32, u32)>,
81 pub end: Option<(u32, u32)>,
83}
84
85impl ComparedRange {
86 pub fn empty() -> Self {
87 Self { start: None, end: None }
88 }
89
90 pub(crate) fn union(
92 old_start: Option<(u32, u32)>,
93 old_end: Option<(u32, u32)>,
94 new_start: Option<(u32, u32)>,
95 new_end: Option<(u32, u32)>,
96 ) -> Self {
97 let start = match (old_start, new_start) {
98 (None, None) => None,
99 (Some(a), None) | (None, Some(a)) => Some(a),
100 (Some((ar, ac)), Some((br, bc))) => Some((ar.min(br), ac.min(bc))),
101 };
102 let end = match (old_end, new_end) {
103 (None, None) => None,
104 (Some(a), None) | (None, Some(a)) => Some(a),
105 (Some((ar, ac)), Some((br, bc))) => Some((ar.max(br), ac.max(bc))),
106 };
107 Self { start, end }
108 }
109}
110
111pub fn col_to_label(mut col: u32) -> String {
120 debug_assert!(col > 0, "col must be 1-based");
121 let mut bytes = Vec::with_capacity(3);
122 while col > 0 {
123 let rem = (col - 1) % 26;
124 bytes.push(b'A' + rem as u8);
125 col = (col - 1) / 26;
126 }
127 bytes.reverse();
128 unsafe { String::from_utf8_unchecked(bytes) }
130}
131
132pub fn cell_pos_to_a1(row: u32, col: u32) -> String {
134 format!("{}{}", col_to_label(col), row)
135}
136
137#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn col_label_single_letters() {
147 assert_eq!(col_to_label(1), "A");
148 assert_eq!(col_to_label(26), "Z");
149 }
150
151 #[test]
152 fn col_label_double_letters() {
153 assert_eq!(col_to_label(27), "AA");
154 assert_eq!(col_to_label(52), "AZ");
155 assert_eq!(col_to_label(53), "BA");
156 assert_eq!(col_to_label(702), "ZZ");
157 }
158
159 #[test]
160 fn col_label_triple_letters() {
161 assert_eq!(col_to_label(703), "AAA");
162 assert_eq!(col_to_label(16_384), MAX_COL_LABEL);
163 }
164
165 #[test]
166 fn cell_address_new_valid() {
167 let addr = CellAddress::new(1, 1).unwrap();
168 assert_eq!(addr.a1, "A1");
169 assert_eq!(addr.row, 1);
170 assert_eq!(addr.col, 1);
171
172 let last = CellAddress::new(MAX_ROW, MAX_COL).unwrap();
173 assert_eq!(last.a1, "XFD1048576");
174 }
175
176 #[test]
177 fn cell_address_new_out_of_bounds() {
178 assert!(CellAddress::new(0, 1).is_none());
179 assert!(CellAddress::new(1, 0).is_none());
180 assert!(CellAddress::new(MAX_ROW + 1, 1).is_none());
181 assert!(CellAddress::new(1, MAX_COL + 1).is_none());
182 }
183
184 #[test]
185 fn sort_order_is_row_col_not_a1_lexicographic() {
186 let a2 = CellAddress::new(2, 1).unwrap();
187 let a10 = CellAddress::new(10, 1).unwrap();
188 assert!(a2 < a10, "A10 must sort after A2 (numeric row, not lex)");
189 }
190}