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 {
88 start: None,
89 end: None,
90 }
91 }
92
93 pub fn union(
95 old_start: Option<(u32, u32)>,
96 old_end: Option<(u32, u32)>,
97 new_start: Option<(u32, u32)>,
98 new_end: Option<(u32, u32)>,
99 ) -> Self {
100 let start = match (old_start, new_start) {
101 (None, None) => None,
102 (Some(a), None) | (None, Some(a)) => Some(a),
103 (Some((ar, ac)), Some((br, bc))) => Some((ar.min(br), ac.min(bc))),
104 };
105 let end = match (old_end, new_end) {
106 (None, None) => None,
107 (Some(a), None) | (None, Some(a)) => Some(a),
108 (Some((ar, ac)), Some((br, bc))) => Some((ar.max(br), ac.max(bc))),
109 };
110 Self { start, end }
111 }
112}
113
114pub fn col_to_label(mut col: u32) -> String {
123 debug_assert!(col > 0, "col must be 1-based");
124 let mut bytes = Vec::with_capacity(3);
125 while col > 0 {
126 let rem = (col - 1) % 26;
127 bytes.push(b'A' + rem as u8);
128 col = (col - 1) / 26;
129 }
130 bytes.reverse();
131 String::from_utf8(bytes).expect("col_to_label only pushes ASCII uppercase bytes")
134}
135
136pub fn cell_pos_to_a1(row: u32, col: u32) -> String {
138 format!("{}{}", col_to_label(col), row)
139}
140
141#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn col_label_single_letters() {
151 assert_eq!(col_to_label(1), "A");
152 assert_eq!(col_to_label(26), "Z");
153 }
154
155 #[test]
156 fn col_label_double_letters() {
157 assert_eq!(col_to_label(27), "AA");
158 assert_eq!(col_to_label(52), "AZ");
159 assert_eq!(col_to_label(53), "BA");
160 assert_eq!(col_to_label(702), "ZZ");
161 }
162
163 #[test]
164 fn col_label_triple_letters() {
165 assert_eq!(col_to_label(703), "AAA");
166 assert_eq!(col_to_label(16_384), MAX_COL_LABEL);
167 }
168
169 #[test]
170 fn cell_address_new_valid() {
171 let addr = CellAddress::new(1, 1).unwrap();
172 assert_eq!(addr.a1, "A1");
173 assert_eq!(addr.row, 1);
174 assert_eq!(addr.col, 1);
175
176 let last = CellAddress::new(MAX_ROW, MAX_COL).unwrap();
177 assert_eq!(last.a1, "XFD1048576");
178 }
179
180 #[test]
181 fn cell_address_new_out_of_bounds() {
182 assert!(CellAddress::new(0, 1).is_none());
183 assert!(CellAddress::new(1, 0).is_none());
184 assert!(CellAddress::new(MAX_ROW + 1, 1).is_none());
185 assert!(CellAddress::new(1, MAX_COL + 1).is_none());
186 }
187
188 #[test]
189 fn sort_order_is_row_col_not_a1_lexicographic() {
190 let a2 = CellAddress::new(2, 1).unwrap();
191 let a10 = CellAddress::new(10, 1).unwrap();
192 assert!(a2 < a10, "A10 must sort after A2 (numeric row, not lex)");
193 }
194}