openprxl 0.1.0

A Rust spreadsheet library inspired by Python's openpyxl
Documentation
//! Worksheet API.

use crate::cell::{Cell, Value};
use crate::error::Result;
use crate::reference::{CellRef, RangeRef};
use crate::style::StyleId;
use std::collections::{BTreeMap, HashSet};

/// A single worksheet inside a workbook.
#[derive(Debug, Clone)]
pub struct Worksheet {
    pub(crate) name: String,
    pub(crate) sheet_id: u32,
    pub(crate) cells: BTreeMap<(u32, u32), Cell>,
    pub(crate) row_heights: BTreeMap<u32, f64>,
    pub(crate) col_widths: BTreeMap<u32, f64>,
    pub(crate) hidden_rows: HashSet<u32>,
    pub(crate) hidden_cols: HashSet<u32>,
    pub(crate) merged_cells: Vec<RangeRef>,
    pub(crate) tab_color: Option<String>,
}

impl Worksheet {
    /// Create a new worksheet with the given name and internal sheet id.
    pub(crate) fn new<S: Into<String>>(name: S, sheet_id: u32) -> Self {
        Self {
            name: name.into(),
            sheet_id,
            cells: BTreeMap::new(),
            row_heights: BTreeMap::new(),
            col_widths: BTreeMap::new(),
            hidden_rows: HashSet::new(),
            hidden_cols: HashSet::new(),
            merged_cells: Vec::new(),
            tab_color: None,
        }
    }

    /// Get the worksheet name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Rename the worksheet.
    pub fn set_name<S: Into<String>>(&mut self, name: S) {
        self.name = name.into();
    }

    /// Get the internal sheet id.
    pub fn sheet_id(&self) -> u32 {
        self.sheet_id
    }

    /// Get a reference to a cell by A1-style reference.
    ///
    /// Returns an empty cell if it does not exist.
    pub fn cell(&self, reference: &str) -> &Cell {
        static EMPTY: Cell = Cell::empty();
        match CellRef::parse(reference) {
            Ok(r) => self.cells.get(&(r.row, r.col)).unwrap_or(&EMPTY),
            Err(_) => &EMPTY,
        }
    }

    /// Get a mutable reference to a cell by A1-style reference.
    ///
    /// Creates an empty cell if it does not exist.
    pub fn cell_mut(&mut self, reference: &str) -> Result<&mut Cell> {
        let r = CellRef::parse(reference)?;
        Ok(self.cells.entry((r.row, r.col)).or_default())
    }

    /// Get a cell by row and column (1-indexed).
    pub fn cell_at(&self, row: u32, col: u32) -> &Cell {
        static EMPTY: Cell = Cell::empty();
        self.cells.get(&(row, col)).unwrap_or(&EMPTY)
    }

    /// Get a mutable cell by row and column (1-indexed).
    pub fn cell_at_mut(&mut self, row: u32, col: u32) -> &mut Cell {
        self.cells.entry((row, col)).or_default()
    }

    /// Set a cell value by A1-style reference.
    pub fn set_cell<V: Into<Value>>(&mut self, reference: &str, value: V) -> Result<()> {
        let r = CellRef::parse(reference)?;
        self.cells.insert((r.row, r.col), Cell::new(value));
        Ok(())
    }

    /// Set a cell value by row and column (1-indexed).
    pub fn set_cell_at<V: Into<Value>>(&mut self, row: u32, col: u32, value: V) {
        self.cells.insert((row, col), Cell::new(value));
    }

    /// Merge a range of cells given by an A1-style range.
    pub fn merge_cells(&mut self, range: &str) -> Result<()> {
        let range = RangeRef::parse(range)?;
        self.merged_cells.push(range.normalized());
        Ok(())
    }

    /// Return all merged cell ranges.
    pub fn merged_cells(&self) -> &[RangeRef] {
        &self.merged_cells
    }

    /// Set the height of a row in points.
    pub fn set_row_height(&mut self, row: u32, height: f64) {
        if row == 0 {
            return;
        }
        self.row_heights.insert(row, height);
    }

    /// Get the height of a row, if explicitly set.
    pub fn row_height(&self, row: u32) -> Option<f64> {
        self.row_heights.get(&row).copied()
    }

    /// Hide a row.
    pub fn hide_row(&mut self, row: u32) {
        if row > 0 {
            self.hidden_rows.insert(row);
        }
    }

    /// Show a previously hidden row.
    pub fn show_row(&mut self, row: u32) {
        self.hidden_rows.remove(&row);
    }

    /// Returns `true` if the row is hidden.
    pub fn is_row_hidden(&self, row: u32) -> bool {
        self.hidden_rows.contains(&row)
    }

    /// Set the width of a column in characters.
    pub fn set_column_width(&mut self, col: &str, width: f64) -> Result<()> {
        let col_idx = crate::reference::letters_to_col(col)?;
        self.col_widths.insert(col_idx, width);
        Ok(())
    }

    /// Get the width of a column, if explicitly set.
    pub fn column_width(&self, col: u32) -> Option<f64> {
        self.col_widths.get(&col).copied()
    }

    /// Hide a column.
    pub fn hide_column(&mut self, col: u32) {
        if col > 0 {
            self.hidden_cols.insert(col);
        }
    }

    /// Show a previously hidden column.
    pub fn show_column(&mut self, col: u32) {
        self.hidden_cols.remove(&col);
    }

    /// Returns `true` if the column is hidden.
    pub fn is_column_hidden(&self, col: u32) -> bool {
        self.hidden_cols.contains(&col)
    }

    /// Set the tab color as an RGB hex string.
    pub fn set_tab_color<S: Into<String>>(&mut self, color: S) {
        self.tab_color = Some(color.into());
    }

    /// Get the tab color, if set.
    pub fn tab_color(&self) -> Option<&str> {
        self.tab_color.as_deref()
    }

    /// Compute the used range of the worksheet.
    pub fn dimension(&self) -> Option<RangeRef> {
        if self.cells.is_empty() {
            return None;
        }
        let mut min_row = u32::MAX;
        let mut min_col = u32::MAX;
        let mut max_row = 0;
        let mut max_col = 0;
        for (row, col) in self.cells.keys() {
            min_row = min_row.min(*row);
            min_col = min_col.min(*col);
            max_row = max_row.max(*row);
            max_col = max_col.max(*col);
        }
        Some(
            RangeRef::new(
                CellRef::new(min_row, min_col).ok()?,
                CellRef::new(max_row, max_col).ok()?,
            )
            .normalized(),
        )
    }

    /// Iterate over all non-empty cells.
    pub fn iter_cells(&self) -> impl Iterator<Item = (u32, u32, &Cell)> {
        self.cells
            .iter()
            .map(|((row, col), cell)| (*row, *col, cell))
    }

    /// Clear all cells.
    pub fn clear(&mut self) {
        self.cells.clear();
    }

    /// Delete a cell by A1-style reference.
    pub fn delete_cell(&mut self, reference: &str) -> Result<()> {
        let r = CellRef::parse(reference)?;
        self.cells.remove(&(r.row, r.col));
        Ok(())
    }

    /// Apply a style to a cell by A1-style reference.
    pub fn set_cell_style(&mut self, reference: &str, style_id: StyleId) -> Result<()> {
        let r = CellRef::parse(reference)?;
        self.cells
            .entry((r.row, r.col))
            .or_default()
            .set_style_id(style_id);
        Ok(())
    }
}

impl std::ops::Index<&str> for Worksheet {
    type Output = Cell;

    fn index(&self, reference: &str) -> &Self::Output {
        self.cell(reference)
    }
}

impl std::ops::IndexMut<&str> for Worksheet {
    fn index_mut(&mut self, reference: &str) -> &mut Self::Output {
        let r = CellRef::parse(reference).expect("invalid cell reference");
        self.cells.entry((r.row, r.col)).or_default()
    }
}