use serde::Serialize;
use crate::range_ref::{cell_key, RangeRef};
pub const MAX_RANGE_CELLS: usize = 10_000;
fn strip_anchors(addr: &str) -> String {
addr.replace('$', "")
}
pub fn split_ref(reference: &str, current_sheet: &str) -> (String, String) {
match reference.rsplit_once('!') {
Some((sheet, addr)) => (sheet.trim_matches('\'').to_string(), strip_anchors(addr)),
None => (current_sheet.to_string(), strip_anchors(reference)),
}
}
pub fn parse_a1(addr: &str) -> Option<(String, u32)> {
let split = addr.find(|c: char| c.is_ascii_digit())?;
if split == 0 {
return None; }
let (col, row) = addr.split_at(split);
if !col.bytes().all(|b| b.is_ascii_alphabetic()) {
return None;
}
let row: u32 = row.parse().ok()?;
if row == 0 {
return None;
}
Some((col.to_ascii_uppercase(), row))
}
fn col_to_index(col: &str) -> Option<u32> {
if col.is_empty() {
return None;
}
let mut idx: u32 = 0;
for b in col.bytes() {
if !b.is_ascii_alphabetic() {
return None;
}
let v = (b.to_ascii_uppercase() - b'A') as u32 + 1;
idx = idx.checked_mul(26)?.checked_add(v)?;
}
Some(idx)
}
pub fn a1_to_zero_indexed_row_col(addr: &str) -> Option<(u32, u16)> {
let (col_letters, row) = parse_a1(addr)?;
let col_1based = col_to_index(&col_letters)?;
let col_zero = u16::try_from(col_1based.checked_sub(1)?).ok()?;
Some((row - 1, col_zero))
}
fn index_to_col(mut idx: u32) -> String {
let mut chars = Vec::new();
while idx > 0 {
let rem = ((idx - 1) % 26) as u8;
chars.push(char::from(b'A' + rem));
idx = (idx - 1) / 26;
}
chars.iter().rev().collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub struct RangeShape {
pub rows: u32,
pub cols: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, schemars::JsonSchema)]
pub enum ResolveError {
MalformedRange {
start: String,
end: String,
},
RangeTooLarge {
cells: u64,
cap: usize,
},
}
pub fn expand_range(
range: &RangeRef,
current_sheet: &str,
) -> Result<(Vec<String>, RangeShape), ResolveError> {
let sheet = if range.sheet.is_empty() {
current_sheet.to_string()
} else {
range.sheet.trim_matches('\'').to_string()
};
let start = strip_anchors(&range.start);
let end = strip_anchors(&range.end);
let malformed = || ResolveError::MalformedRange {
start: start.clone(),
end: end.clone(),
};
let (Some((sc, sr)), Some((ec, er))) = (parse_a1(&start), parse_a1(&end)) else {
return Err(malformed());
};
let (Some(sci), Some(eci)) = (col_to_index(&sc), col_to_index(&ec)) else {
return Err(malformed());
};
let (col_lo, col_hi) = (sci.min(eci), sci.max(eci));
let (row_lo, row_hi) = (sr.min(er), sr.max(er));
let cols = col_hi - col_lo + 1;
let rows = row_hi - row_lo + 1;
let n_cells = u64::from(cols) * u64::from(rows);
if n_cells > MAX_RANGE_CELLS as u64 {
return Err(ResolveError::RangeTooLarge {
cells: n_cells,
cap: MAX_RANGE_CELLS,
});
}
let mut keys = Vec::with_capacity(n_cells as usize);
for col in col_lo..=col_hi {
let col_letters = index_to_col(col);
for row in row_lo..=row_hi {
keys.push(cell_key(&sheet, &format!("{col_letters}{row}")));
}
}
Ok((keys, RangeShape { rows, cols }))
}
#[cfg(test)]
mod tests {
use super::*;
fn rr(sheet: &str, start: &str, end: &str) -> RangeRef {
RangeRef {
sheet: sheet.to_string(),
start: start.to_string(),
end: end.to_string(),
}
}
#[test]
fn public_expand_range_single_column_returns_keys_and_shape() {
let (keys, shape) = expand_range(&rr("S", "B2", "B4"), "S").expect("valid range");
assert_eq!(
keys,
vec!["S!B2".to_string(), "S!B3".to_string(), "S!B4".to_string()]
);
assert_eq!(shape, RangeShape { rows: 3, cols: 1 });
}
#[test]
fn public_expand_range_2x2_is_column_major_with_2x2_shape() {
let (keys, shape) = expand_range(&rr("S", "A1", "B2"), "S").expect("valid range");
assert_eq!(
keys,
vec![
"S!A1".to_string(),
"S!A2".to_string(),
"S!B1".to_string(),
"S!B2".to_string(),
]
);
assert_eq!(shape, RangeShape { rows: 2, cols: 2 });
}
#[test]
fn public_expand_range_defaults_empty_sheet_to_current() {
let (keys, _shape) = expand_range(&rr("", "C1", "C2"), "5_Quantities").expect("valid");
assert_eq!(
keys,
vec!["5_Quantities!C1".to_string(), "5_Quantities!C2".to_string()]
);
}
#[test]
fn public_expand_range_over_cap_is_err() {
let err = expand_range(&rr("S", "A1", "XFD1048576"), "S")
.expect_err("an over-cap range must be Err");
assert!(matches!(
err,
ResolveError::RangeTooLarge { cap, cells } if cap == MAX_RANGE_CELLS && cells > MAX_RANGE_CELLS as u64
));
}
#[test]
fn public_expand_range_malformed_endpoint_is_err() {
let err =
expand_range(&rr("S", "1A", "B2"), "S").expect_err("a malformed endpoint must be Err");
assert!(matches!(err, ResolveError::MalformedRange { .. }));
}
#[test]
fn public_parse_a1_parses_and_rejects() {
assert_eq!(parse_a1("C16"), Some(("C".to_string(), 16)));
assert_eq!(parse_a1("$C$16"), None); assert_eq!(parse_a1("16"), None); assert_eq!(parse_a1("C0"), None); assert_eq!(parse_a1("CC"), None); }
#[test]
fn public_split_ref_strips_anchors_and_defaults_sheet() {
assert_eq!(
split_ref("2_Constants!$C$17", "5_Quantities"),
("2_Constants".to_string(), "C17".to_string())
);
assert_eq!(
split_ref("$C$16", "5_Quantities"),
("5_Quantities".to_string(), "C16".to_string())
);
}
#[test]
fn a1_to_zero_indexed_row_col_converts_and_rejects() {
assert_eq!(a1_to_zero_indexed_row_col("C16"), Some((15, 2)));
assert_eq!(a1_to_zero_indexed_row_col("A1"), Some((0, 0)));
assert_eq!(a1_to_zero_indexed_row_col("AA1"), Some((0, 26)));
assert_eq!(a1_to_zero_indexed_row_col("1A"), None);
assert_eq!(a1_to_zero_indexed_row_col("$C$16"), None); assert_eq!(a1_to_zero_indexed_row_col(""), None);
}
#[test]
fn col_index_round_trips() {
for (col, idx) in [("A", 1u32), ("Z", 26), ("AA", 27), ("XFD", 16384)] {
assert_eq!(col_to_index(col), Some(idx));
assert_eq!(index_to_col(idx), col);
}
}
}