#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MergeRegion {
pub row: usize,
pub col: usize,
pub row_span: usize,
pub col_span: usize,
}
impl MergeRegion {
pub fn new(row: usize, col: usize, row_span: usize, col_span: usize) -> Result<Self, String> {
if row_span == 0 || col_span == 0 {
return Err("Span must be at least 1".to_string());
}
Ok(Self { row, col, row_span, col_span })
}
pub fn last_row(&self) -> usize {
self.row + self.row_span - 1
}
pub fn last_col(&self) -> usize {
self.col + self.col_span - 1
}
pub fn contains(&self, r: usize, c: usize) -> bool {
r >= self.row && r <= self.last_row() && c >= self.col && c <= self.last_col()
}
pub fn is_anchor(&self, r: usize, c: usize) -> bool {
r == self.row && c == self.col
}
pub fn overlaps(&self, other: &MergeRegion) -> bool {
self.row <= other.last_row()
&& other.row <= self.last_row()
&& self.col <= other.last_col()
&& other.col <= self.last_col()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CellMergeState {
Normal,
Anchor { row_span: usize, col_span: usize },
HMerge,
VMerge,
}
impl CellMergeState {
pub fn to_xml_attrs(&self) -> String {
match self {
CellMergeState::Normal => String::new(),
CellMergeState::Anchor { row_span, col_span } => {
let mut attrs = String::new();
if *col_span > 1 {
attrs.push_str(&format!(r#" gridSpan="{}""#, col_span));
}
if *row_span > 1 {
attrs.push_str(&format!(r#" rowSpan="{}""#, row_span));
}
attrs
}
CellMergeState::HMerge => r#" hMerge="1""#.to_string(),
CellMergeState::VMerge => r#" vMerge="1""#.to_string(),
}
}
pub fn is_merged_away(&self) -> bool {
matches!(self, CellMergeState::HMerge | CellMergeState::VMerge)
}
}
#[derive(Clone, Debug, Default)]
pub struct TableMergeMap {
regions: Vec<MergeRegion>,
rows: usize,
cols: usize,
}
impl TableMergeMap {
pub fn new(rows: usize, cols: usize) -> Self {
Self { regions: Vec::new(), rows, cols }
}
pub fn add_merge(&mut self, region: MergeRegion) -> Result<(), String> {
if region.last_row() >= self.rows {
return Err(format!(
"Merge region row {}-{} exceeds table rows {}",
region.row, region.last_row(), self.rows
));
}
if region.last_col() >= self.cols {
return Err(format!(
"Merge region col {}-{} exceeds table cols {}",
region.col, region.last_col(), self.cols
));
}
for existing in &self.regions {
if existing.overlaps(®ion) {
return Err(format!(
"Merge region ({},{}) {}x{} overlaps with ({},{}) {}x{}",
region.row, region.col, region.row_span, region.col_span,
existing.row, existing.col, existing.row_span, existing.col_span,
));
}
}
self.regions.push(region);
Ok(())
}
pub fn merge_cells(&mut self, row: usize, col: usize, row_span: usize, col_span: usize) -> Result<(), String> {
let region = MergeRegion::new(row, col, row_span, col_span)?;
self.add_merge(region)
}
pub fn cell_state(&self, r: usize, c: usize) -> CellMergeState {
for region in &self.regions {
if region.is_anchor(r, c) {
return CellMergeState::Anchor {
row_span: region.row_span,
col_span: region.col_span,
};
}
if region.contains(r, c) {
if r == region.row {
return CellMergeState::HMerge;
}
return CellMergeState::VMerge;
}
}
CellMergeState::Normal
}
pub fn regions(&self) -> &[MergeRegion] {
&self.regions
}
pub fn len(&self) -> usize {
self.regions.len()
}
pub fn is_empty(&self) -> bool {
self.regions.is_empty()
}
pub fn dimensions(&self) -> (usize, usize) {
(self.rows, self.cols)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_region_new() {
let r = MergeRegion::new(0, 0, 2, 3).unwrap();
assert_eq!(r.row, 0);
assert_eq!(r.col, 0);
assert_eq!(r.row_span, 2);
assert_eq!(r.col_span, 3);
}
#[test]
fn test_merge_region_zero_span() {
assert!(MergeRegion::new(0, 0, 0, 1).is_err());
assert!(MergeRegion::new(0, 0, 1, 0).is_err());
}
#[test]
fn test_merge_region_last() {
let r = MergeRegion::new(1, 2, 3, 4).unwrap();
assert_eq!(r.last_row(), 3);
assert_eq!(r.last_col(), 5);
}
#[test]
fn test_merge_region_contains() {
let r = MergeRegion::new(1, 1, 2, 2).unwrap();
assert!(r.contains(1, 1));
assert!(r.contains(2, 2));
assert!(!r.contains(0, 0));
assert!(!r.contains(3, 1));
}
#[test]
fn test_merge_region_is_anchor() {
let r = MergeRegion::new(1, 2, 2, 3).unwrap();
assert!(r.is_anchor(1, 2));
assert!(!r.is_anchor(1, 3));
assert!(!r.is_anchor(2, 2));
}
#[test]
fn test_merge_region_overlaps() {
let a = MergeRegion::new(0, 0, 2, 2).unwrap();
let b = MergeRegion::new(1, 1, 2, 2).unwrap();
assert!(a.overlaps(&b));
assert!(b.overlaps(&a));
let c = MergeRegion::new(2, 2, 1, 1).unwrap();
assert!(!a.overlaps(&c));
}
#[test]
fn test_merge_region_adjacent_no_overlap() {
let a = MergeRegion::new(0, 0, 2, 2).unwrap();
let b = MergeRegion::new(0, 2, 2, 2).unwrap();
assert!(!a.overlaps(&b));
}
#[test]
fn test_cell_merge_state_normal() {
let s = CellMergeState::Normal;
assert_eq!(s.to_xml_attrs(), "");
assert!(!s.is_merged_away());
}
#[test]
fn test_cell_merge_state_anchor() {
let s = CellMergeState::Anchor { row_span: 2, col_span: 3 };
let xml = s.to_xml_attrs();
assert!(xml.contains(r#"gridSpan="3""#));
assert!(xml.contains(r#"rowSpan="2""#));
assert!(!s.is_merged_away());
}
#[test]
fn test_cell_merge_state_anchor_col_only() {
let s = CellMergeState::Anchor { row_span: 1, col_span: 3 };
let xml = s.to_xml_attrs();
assert!(xml.contains(r#"gridSpan="3""#));
assert!(!xml.contains("rowSpan"));
}
#[test]
fn test_cell_merge_state_hmerge() {
let s = CellMergeState::HMerge;
assert!(s.to_xml_attrs().contains("hMerge"));
assert!(s.is_merged_away());
}
#[test]
fn test_cell_merge_state_vmerge() {
let s = CellMergeState::VMerge;
assert!(s.to_xml_attrs().contains("vMerge"));
assert!(s.is_merged_away());
}
#[test]
fn test_table_merge_map_new() {
let m = TableMergeMap::new(5, 4);
assert!(m.is_empty());
assert_eq!(m.dimensions(), (5, 4));
}
#[test]
fn test_table_merge_map_add() {
let mut m = TableMergeMap::new(5, 4);
assert!(m.merge_cells(0, 0, 2, 2).is_ok());
assert_eq!(m.len(), 1);
}
#[test]
fn test_table_merge_map_out_of_bounds() {
let mut m = TableMergeMap::new(3, 3);
assert!(m.merge_cells(2, 2, 2, 1).is_err()); assert!(m.merge_cells(0, 2, 1, 2).is_err()); }
#[test]
fn test_table_merge_map_overlap_detection() {
let mut m = TableMergeMap::new(5, 5);
m.merge_cells(0, 0, 2, 2).unwrap();
assert!(m.merge_cells(1, 1, 2, 2).is_err());
}
#[test]
fn test_table_merge_map_adjacent_ok() {
let mut m = TableMergeMap::new(5, 5);
m.merge_cells(0, 0, 2, 2).unwrap();
assert!(m.merge_cells(0, 2, 2, 2).is_ok());
assert_eq!(m.len(), 2);
}
#[test]
fn test_table_merge_map_cell_state() {
let mut m = TableMergeMap::new(4, 4);
m.merge_cells(0, 0, 2, 3).unwrap();
assert_eq!(
m.cell_state(0, 0),
CellMergeState::Anchor { row_span: 2, col_span: 3 }
);
assert_eq!(m.cell_state(0, 1), CellMergeState::HMerge);
assert_eq!(m.cell_state(0, 2), CellMergeState::HMerge);
assert_eq!(m.cell_state(1, 0), CellMergeState::VMerge);
assert_eq!(m.cell_state(1, 1), CellMergeState::VMerge);
assert_eq!(m.cell_state(1, 2), CellMergeState::VMerge);
assert_eq!(m.cell_state(0, 3), CellMergeState::Normal);
assert_eq!(m.cell_state(2, 0), CellMergeState::Normal);
}
#[test]
fn test_table_merge_map_multiple_regions() {
let mut m = TableMergeMap::new(4, 4);
m.merge_cells(0, 0, 1, 2).unwrap();
m.merge_cells(2, 2, 2, 2).unwrap();
assert_eq!(
m.cell_state(0, 0),
CellMergeState::Anchor { row_span: 1, col_span: 2 }
);
assert_eq!(m.cell_state(0, 1), CellMergeState::HMerge);
assert_eq!(
m.cell_state(2, 2),
CellMergeState::Anchor { row_span: 2, col_span: 2 }
);
assert_eq!(m.cell_state(3, 3), CellMergeState::VMerge);
assert_eq!(m.cell_state(1, 1), CellMergeState::Normal);
}
}