use std::fmt;
use crate::{RangeAddress, SheetAddressError, format_a1_sheet_name};
const MAX_ROW: u32 = 1_048_576;
const MAX_COLUMN: u32 = 16_384;
fn validate_row(row: u32) -> Result<(), SheetAddressError> {
match row {
0 => Err(SheetAddressError::ZeroIndex),
1..=MAX_ROW => Ok(()),
_ => Err(SheetAddressError::RowOutOfBounds),
}
}
fn validate_column(column: u32) -> Result<(), SheetAddressError> {
match column {
0 => Err(SheetAddressError::ZeroIndex),
1..=MAX_COLUMN => Ok(()),
_ => Err(SheetAddressError::ColumnOutOfBounds),
}
}
fn unbounded_column_letters(mut column: u32) -> String {
let mut reversed = Vec::new();
while column > 0 {
column -= 1;
reversed.push(char::from(b'A' + (column % 26) as u8));
column /= 26;
}
reversed.into_iter().rev().collect()
}
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct CellAddress {
pub sheet: String,
pub row: u32,
pub column: u32,
}
impl CellAddress {
pub fn new(sheet: impl Into<String>, row: u32, column: u32) -> Result<Self, SheetAddressError> {
validate_row(row)?;
validate_column(column)?;
Ok(Self {
sheet: sheet.into(),
row,
column,
})
}
pub fn to_finite(&self) -> RangeAddress {
RangeAddress {
sheet: self.sheet.clone(),
start_row: self.row,
start_col: self.column,
end_row: self.row,
end_col: self.column,
}
}
pub fn from_finite(range: &RangeAddress) -> Option<Self> {
(range.start_row == range.end_row && range.start_col == range.end_col).then(|| Self {
sheet: range.sheet.clone(),
row: range.start_row,
column: range.start_col,
})
}
}
impl fmt::Display for CellAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sheet = format_a1_sheet_name(&self.sheet);
if self.row == 0 || self.column == 0 {
return write!(f, "{sheet}!#REF!");
}
write!(
f,
"{sheet}!{}{}",
unbounded_column_letters(self.column),
self.row
)
}
}
impl From<CellAddress> for RangeAddress {
fn from(value: CellAddress) -> Self {
Self {
sheet: value.sheet,
start_row: value.row,
start_col: value.column,
end_row: value.row,
end_col: value.column,
}
}
}
impl TryFrom<RangeAddress> for CellAddress {
type Error = SheetAddressError;
fn try_from(value: RangeAddress) -> Result<Self, Self::Error> {
if value.start_row != value.end_row || value.start_col != value.end_col {
return Err(SheetAddressError::NonSingleCellRange);
}
Self::new(value.sheet, value.start_row, value.start_col)
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct RangeArea {
pub sheet: String,
pub start_row: Option<u32>,
pub start_column: Option<u32>,
pub end_row: Option<u32>,
pub end_column: Option<u32>,
}
impl RangeArea {
pub fn new(
sheet: impl Into<String>,
start_row: Option<u32>,
start_column: Option<u32>,
end_row: Option<u32>,
end_column: Option<u32>,
) -> Result<Self, SheetAddressError> {
for row in [start_row, end_row].into_iter().flatten() {
validate_row(row)?;
}
for column in [start_column, end_column].into_iter().flatten() {
validate_column(column)?;
}
if start_row
.zip(end_row)
.is_some_and(|(start, end)| start > end)
|| start_column
.zip(end_column)
.is_some_and(|(start, end)| start > end)
{
return Err(SheetAddressError::RangeOrder);
}
Ok(Self {
sheet: sheet.into(),
start_row,
start_column,
end_row,
end_column,
})
}
pub fn from_finite(range: &RangeAddress) -> Self {
Self {
sheet: range.sheet.clone(),
start_row: Some(range.start_row),
start_column: Some(range.start_col),
end_row: Some(range.end_row),
end_column: Some(range.end_col),
}
}
pub fn to_finite(&self) -> Option<RangeAddress> {
Some(RangeAddress {
sheet: self.sheet.clone(),
start_row: self.start_row?,
start_col: self.start_column?,
end_row: self.end_row?,
end_col: self.end_column?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cell_display_uses_canonical_sheet_quoting() {
assert_eq!(
CellAddress::new("Data", 12, 28).unwrap().to_string(),
"Data!AB12"
);
assert_eq!(
CellAddress::new("My Sheet", 1, 1).unwrap().to_string(),
"'My Sheet'!A1"
);
assert_eq!(
CellAddress::new("O'Brien", 2, 3).unwrap().to_string(),
"'O''Brien'!C2"
);
assert_eq!(
CellAddress::new("TRUE", 3, 2).unwrap().to_string(),
"'TRUE'!B3"
);
}
#[test]
fn display_is_total_for_checked_and_unchecked_coordinates() {
let display = |row, column| {
CellAddress {
sheet: "Sheet".to_string(),
row,
column,
}
.to_string()
};
assert_eq!(display(1, 16_384), "Sheet!XFD1");
assert_eq!(display(1, 16_385), "Sheet!XFE1");
assert_eq!(display(1, u32::MAX), "Sheet!MWLQKWU1");
assert_eq!(display(1, 0), "Sheet!#REF!");
assert_eq!(display(0, 1), "Sheet!#REF!");
assert_eq!(display(0, 0), "Sheet!#REF!");
}
#[test]
fn constructors_validate_grid_bounds_and_order() {
assert_eq!(
CellAddress::new("S", 0, 1).unwrap_err(),
SheetAddressError::ZeroIndex
);
assert_eq!(
CellAddress::new("S", MAX_ROW + 1, 1).unwrap_err(),
SheetAddressError::RowOutOfBounds
);
assert_eq!(
CellAddress::new("S", 1, MAX_COLUMN + 1).unwrap_err(),
SheetAddressError::ColumnOutOfBounds
);
assert!(CellAddress::new("S", MAX_ROW, MAX_COLUMN).is_ok());
assert_eq!(
RangeArea::new("S", Some(4), Some(1), Some(3), Some(2)).unwrap_err(),
SheetAddressError::RangeOrder
);
assert_eq!(
RangeArea::new("S", None, Some(0), None, Some(2)).unwrap_err(),
SheetAddressError::ZeroIndex
);
assert_eq!(
RangeArea::new("S", Some(MAX_ROW + 1), None, None, None).unwrap_err(),
SheetAddressError::RowOutOfBounds
);
assert_eq!(
RangeArea::new("S", None, None, None, Some(MAX_COLUMN + 1)).unwrap_err(),
SheetAddressError::ColumnOutOfBounds
);
assert!(RangeArea::new("S", None, Some(2), Some(9), None).is_ok());
}
#[test]
fn finite_conversions_round_trip() {
let finite = RangeAddress::new("S", 2, 3, 5, 7).unwrap();
assert_eq!(RangeArea::from_finite(&finite).to_finite(), Some(finite));
let cell = CellAddress::new("S", 8, 9).unwrap();
assert_eq!(
CellAddress::from_finite(&cell.to_finite()),
Some(cell.clone())
);
assert_eq!(
CellAddress::try_from(RangeAddress::from(cell.clone())),
Ok(cell)
);
assert_eq!(
CellAddress::try_from(RangeAddress::new("S", 1, 1, 2, 1).unwrap()),
Err(SheetAddressError::NonSingleCellRange)
);
}
}