#[derive(Debug, Clone, PartialEq)]
pub(crate) enum CellValue {
Number(f64),
String(String),
Bool(bool),
Error(String),
Empty,
}
impl CellValue {
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
matches!(self, CellValue::Empty)
}
#[allow(dead_code)]
pub fn as_raw_string(&self) -> String {
match self {
CellValue::Number(n) => n.to_string(),
CellValue::String(s) => s.clone(),
CellValue::Bool(b) => b.to_string(),
CellValue::Error(e) => e.clone(),
CellValue::Empty => String::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct CellCoord {
pub row: u32,
pub col: u32,
}
impl CellCoord {
pub fn new(row: u32, col: u32) -> Self {
Self { row, col }
}
#[allow(dead_code, clippy::wrong_self_convention)]
pub fn to_a1_notation(&self) -> String {
let col_str = Self::col_index_to_letter(self.col);
format!("{}{}", col_str, self.row + 1)
}
#[allow(dead_code)]
fn col_index_to_letter(mut col: u32) -> String {
let mut result = String::new();
loop {
let remainder = col % 26;
result.insert(0, (b'A' + remainder as u8) as char);
if col < 26 {
break;
}
col = col / 26 - 1;
}
result
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CellRange {
pub start: CellCoord,
pub end: CellCoord,
}
impl CellRange {
pub fn new(start: CellCoord, end: CellCoord) -> Self {
Self { start, end }
}
#[allow(dead_code)]
pub fn contains(&self, coord: CellCoord) -> bool {
coord.row >= self.start.row
&& coord.row <= self.end.row
&& coord.col >= self.start.col
&& coord.col <= self.end.col
}
#[allow(dead_code)]
pub fn size(&self) -> (u32, u32) {
let rows = self.end.row - self.start.row + 1;
let cols = self.end.col - self.start.col + 1;
(rows, cols)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct MergedRegion {
pub range: CellRange,
pub parent: CellCoord,
}
impl MergedRegion {
pub fn new(range: CellRange) -> Self {
Self {
parent: range.start,
range,
}
}
#[allow(dead_code)]
pub fn contains(&self, coord: CellCoord) -> bool {
self.range.contains(coord)
}
pub fn row_span(&self) -> u32 {
self.range.end.row - self.range.start.row + 1
}
pub fn col_span(&self) -> u32 {
self.range.end.col - self.range.start.col + 1
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RichTextFormat {
pub bold: bool,
pub italic: bool,
}
impl RichTextFormat {
pub fn new() -> Self {
Self {
bold: false,
italic: false,
}
}
}
impl Default for RichTextFormat {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct RichTextSegment {
pub text: String,
pub format: RichTextFormat,
}
impl RichTextSegment {
pub fn new(text: String, format: RichTextFormat) -> Self {
Self { text, format }
}
pub fn plain(text: String) -> Self {
Self {
text,
format: RichTextFormat::new(),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct RawCellData {
pub coord: CellCoord,
pub value: CellValue,
pub format_id: Option<u16>,
pub format_string: Option<String>,
pub formula: Option<String>,
pub hyperlink: Option<String>,
pub rich_text: Option<Vec<RichTextSegment>>,
}
#[derive(Debug, Clone)]
pub(crate) struct SheetMetadata {
#[allow(dead_code)]
pub name: String,
#[allow(dead_code)]
pub index: usize,
#[allow(dead_code)]
pub hidden: bool,
pub merged_regions: Vec<MergedRegion>,
pub hidden_rows: Vec<u32>,
pub hidden_cols: Vec<u32>,
pub is_1904: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cell_value_is_empty() {
assert!(CellValue::Empty.is_empty());
assert!(!CellValue::Number(42.0).is_empty());
assert!(!CellValue::String("test".to_string()).is_empty());
assert!(!CellValue::Bool(true).is_empty());
assert!(!CellValue::Error("#DIV/0!".to_string()).is_empty());
}
#[test]
fn test_cell_value_as_raw_string() {
assert_eq!(CellValue::Empty.as_raw_string(), "");
assert_eq!(CellValue::Number(42.5).as_raw_string(), "42.5");
assert_eq!(
CellValue::String("hello".to_string()).as_raw_string(),
"hello"
);
assert_eq!(CellValue::Bool(true).as_raw_string(), "true");
assert_eq!(
CellValue::Error("#DIV/0!".to_string()).as_raw_string(),
"#DIV/0!"
);
}
#[test]
fn test_cell_coord_new() {
let coord = CellCoord::new(0, 0);
assert_eq!(coord.row, 0);
assert_eq!(coord.col, 0);
}
#[test]
fn test_cell_coord_to_a1_notation() {
assert_eq!(CellCoord::new(0, 0).to_a1_notation(), "A1");
assert_eq!(CellCoord::new(0, 25).to_a1_notation(), "Z1");
assert_eq!(CellCoord::new(0, 26).to_a1_notation(), "AA1");
assert_eq!(CellCoord::new(99, 701).to_a1_notation(), "ZZ100");
assert_eq!(CellCoord::new(0, 51).to_a1_notation(), "AZ1");
assert_eq!(CellCoord::new(0, 52).to_a1_notation(), "BA1");
assert_eq!(CellCoord::new(0, 701).to_a1_notation(), "ZZ1");
}
#[test]
fn test_cell_coord_col_index_to_letter() {
assert_eq!(CellCoord::new(0, 0).to_a1_notation(), "A1");
assert_eq!(CellCoord::new(0, 25).to_a1_notation(), "Z1");
assert_eq!(CellCoord::new(0, 26).to_a1_notation(), "AA1");
}
#[test]
fn test_cell_range_new() {
let start = CellCoord::new(0, 0);
let end = CellCoord::new(10, 5);
let range = CellRange::new(start, end);
assert_eq!(range.start, start);
assert_eq!(range.end, end);
}
#[test]
fn test_cell_range_contains() {
let range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(10, 5));
assert!(range.contains(CellCoord::new(0, 0)));
assert!(range.contains(CellCoord::new(5, 3)));
assert!(range.contains(CellCoord::new(10, 5)));
assert!(!range.contains(CellCoord::new(11, 5)));
assert!(!range.contains(CellCoord::new(5, 6)));
assert!(!range.contains(CellCoord::new(0, 6)));
}
#[test]
fn test_cell_range_size() {
let range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(10, 5));
assert_eq!(range.size(), (11, 6));
let range2 = CellRange::new(CellCoord::new(5, 3), CellCoord::new(7, 4));
assert_eq!(range2.size(), (3, 2));
let range3 = CellRange::new(CellCoord::new(0, 0), CellCoord::new(0, 0));
assert_eq!(range3.size(), (1, 1));
}
#[test]
fn test_merged_region_new() {
let range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(2, 3));
let merged = MergedRegion::new(range);
assert_eq!(merged.range, range);
assert_eq!(merged.parent, CellCoord::new(0, 0));
}
#[test]
fn test_merged_region_contains() {
let range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(2, 3));
let merged = MergedRegion::new(range);
assert!(merged.contains(CellCoord::new(0, 0)));
assert!(merged.contains(CellCoord::new(1, 2)));
assert!(merged.contains(CellCoord::new(2, 3)));
assert!(!merged.contains(CellCoord::new(3, 3)));
assert!(!merged.contains(CellCoord::new(1, 4)));
}
#[test]
fn test_merged_region_row_span() {
let range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(2, 3));
let merged = MergedRegion::new(range);
assert_eq!(merged.row_span(), 3);
let range2 = CellRange::new(CellCoord::new(5, 1), CellCoord::new(5, 1));
let merged2 = MergedRegion::new(range2);
assert_eq!(merged2.row_span(), 1);
}
#[test]
fn test_merged_region_col_span() {
let range = CellRange::new(CellCoord::new(0, 0), CellCoord::new(2, 3));
let merged = MergedRegion::new(range);
assert_eq!(merged.col_span(), 4);
let range2 = CellRange::new(CellCoord::new(5, 1), CellCoord::new(5, 1));
let merged2 = MergedRegion::new(range2);
assert_eq!(merged2.col_span(), 1);
}
#[test]
fn test_raw_cell_data() {
let coord = CellCoord::new(0, 0);
let value = CellValue::Number(42.0);
let cell_data = RawCellData {
coord,
value: value.clone(),
format_id: Some(1),
format_string: Some("0.00".to_string()),
formula: None,
hyperlink: None,
rich_text: None,
};
assert_eq!(cell_data.coord, coord);
assert_eq!(cell_data.value, value);
assert_eq!(cell_data.format_id, Some(1));
assert_eq!(cell_data.format_string, Some("0.00".to_string()));
assert_eq!(cell_data.formula, None);
}
#[test]
fn test_raw_cell_data_with_formula() {
let coord = CellCoord::new(1, 1);
let value = CellValue::Number(100.0);
let cell_data = RawCellData {
coord,
value: value.clone(),
format_id: None,
format_string: None,
formula: Some("=A1*2".to_string()),
hyperlink: None,
rich_text: None,
};
assert_eq!(cell_data.formula, Some("=A1*2".to_string()));
}
#[test]
fn test_sheet_metadata_phase_i() {
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![],
hidden_rows: vec![], hidden_cols: vec![], is_1904: false, };
assert_eq!(metadata.name, "Sheet1");
assert_eq!(metadata.index, 0);
assert!(!metadata.hidden);
assert!(metadata.merged_regions.is_empty());
assert!(metadata.hidden_rows.is_empty());
assert!(metadata.hidden_cols.is_empty());
assert!(!metadata.is_1904);
}
#[test]
fn test_sheet_metadata_with_merged_regions() {
let range1 = CellRange::new(CellCoord::new(0, 0), CellCoord::new(0, 2));
let range2 = CellRange::new(CellCoord::new(2, 0), CellCoord::new(3, 1));
let merged1 = MergedRegion::new(range1);
let merged2 = MergedRegion::new(range2);
let metadata = SheetMetadata {
name: "Sheet1".to_string(),
index: 0,
hidden: false,
merged_regions: vec![merged1.clone(), merged2.clone()],
hidden_rows: vec![],
hidden_cols: vec![],
is_1904: false,
};
assert_eq!(metadata.merged_regions.len(), 2);
assert_eq!(metadata.merged_regions[0], merged1);
assert_eq!(metadata.merged_regions[1], merged2);
}
#[allow(unused_doc_comments)]
mod property_tests {
use super::*;
use proptest::prelude::*;
#[allow(unused_doc_comments)]
proptest! {
#[test]
fn test_a1_notation_round_trip(row in 0u32..10000, col in 0u32..10000) {
let coord = CellCoord::new(row, col);
let a1 = coord.to_a1_notation();
prop_assert!(a1.chars().next().unwrap().is_ascii_uppercase());
prop_assert!(a1.chars().last().unwrap().is_ascii_digit());
let mut found_digit = false;
for (i, ch) in a1.chars().enumerate() {
if ch.is_ascii_digit() {
found_digit = true;
prop_assert!(i > 0, "A1 notation should have at least one letter");
} else {
prop_assert!(!found_digit, "A1 notation should not have letters after digits");
}
}
prop_assert!(!a1.is_empty());
let row_part: String = a1.chars().filter(|c| c.is_ascii_digit()).collect();
let row_num: u32 = row_part.parse().unwrap();
prop_assert!(row_num >= 1);
prop_assert_eq!(row_num, row + 1);
}
}
}
}