openprxl 0.1.0

A Rust spreadsheet library inspired by Python's openpyxl
Documentation
//! Cell styling support.
//!
//! Styles are immutable value objects. A [`Style`] can be registered with a
//! [`StyleManager`] to obtain a stable [`StyleId`] that can be assigned to cells.

use indexmap::IndexMap;
use std::sync::Arc;

mod alignment;
mod border;
mod fill;
mod font;
mod numfmt;

pub use alignment::{Alignment, HorizontalAlignment, VerticalAlignment};
pub use border::{Border, BorderStyle, DiagonalBorder, Side};
pub use fill::{Fill, PatternFill, PatternType};
pub use font::{Font, UnderlineStyle};
pub use numfmt::NumberFormat;

/// An opaque identifier for a registered style.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StyleId(pub usize);

impl StyleId {
    /// Return the raw index.
    pub fn index(&self) -> usize {
        self.0
    }
}

/// A complete cell style.
///
/// Use the builder-style methods to configure a style, then register it with
/// [`Workbook::register_style`](crate::workbook::Workbook::register_style) or
/// assign it directly to a cell.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct Style {
    pub(crate) font: Option<Font>,
    pub(crate) fill: Option<Fill>,
    pub(crate) border: Option<Border>,
    pub(crate) alignment: Option<Alignment>,
    pub(crate) number_format: Option<NumberFormat>,
    pub(crate) protection: Option<Protection>,
}

impl Style {
    /// Create an empty style.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the font.
    pub fn font(mut self, font: Font) -> Self {
        self.font = Some(font);
        self
    }

    /// Set the fill.
    pub fn fill(mut self, fill: Fill) -> Self {
        self.fill = Some(fill);
        self
    }

    /// Set the border.
    pub fn border(mut self, border: Border) -> Self {
        self.border = Some(border);
        self
    }

    /// Set the alignment.
    pub fn alignment(mut self, alignment: Alignment) -> Self {
        self.alignment = Some(alignment);
        self
    }

    /// Set the number format.
    pub fn number_format(mut self, number_format: NumberFormat) -> Self {
        self.number_format = Some(number_format);
        self
    }

    /// Set the protection flags.
    pub fn protection(mut self, protection: Protection) -> Self {
        self.protection = Some(protection);
        self
    }

    /// Set a solid color fill from an RGB hex string such as `FFFF00`.
    pub fn solid_fill<S: Into<String>>(mut self, rgb: S) -> Self {
        self.fill = Some(Fill::Pattern(PatternFill::solid(rgb)));
        self
    }

    /// Get the font, if set.
    pub fn get_font(&self) -> Option<&Font> {
        self.font.as_ref()
    }

    /// Get the fill, if set.
    pub fn get_fill(&self) -> Option<&Fill> {
        self.fill.as_ref()
    }

    /// Get the border, if set.
    pub fn get_border(&self) -> Option<&Border> {
        self.border.as_ref()
    }

    /// Get the alignment, if set.
    pub fn get_alignment(&self) -> Option<&Alignment> {
        self.alignment.as_ref()
    }

    /// Get the number format, if set.
    pub fn get_number_format(&self) -> Option<&NumberFormat> {
        self.number_format.as_ref()
    }

    /// Get the protection flags, if set.
    pub fn get_protection(&self) -> Option<&Protection> {
        self.protection.as_ref()
    }
}

/// Cell protection flags.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Protection {
    pub locked: bool,
    pub hidden: bool,
}

impl Protection {
    pub fn new(locked: bool, hidden: bool) -> Self {
        Self { locked, hidden }
    }
}

/// Manages deduplication and indexing of styles.
#[derive(Debug, Clone, Default)]
pub struct StyleManager {
    styles: IndexMap<Arc<Style>, StyleId>,
    fonts: IndexMap<Font, usize>,
    fills: IndexMap<Fill, usize>,
    borders: IndexMap<Border, usize>,
    number_formats: IndexMap<NumberFormat, usize>,
    cell_xfs: Vec<CellXf>,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct CellXf {
    pub font_id: Option<usize>,
    pub fill_id: Option<usize>,
    pub border_id: Option<usize>,
    pub num_fmt_id: Option<usize>,
    pub alignment: Option<Alignment>,
    pub protection: Option<Protection>,
}

impl StyleManager {
    /// Register a style and return its [`StyleId`].
    ///
    /// Equivalent styles return the same id.
    pub fn register(&mut self, style: Style) -> StyleId {
        if let Some(&id) = self.styles.get(&Arc::new(style.clone())) {
            return id;
        }

        let font_id = style.font.as_ref().map(|f| {
            if let Some(&idx) = self.fonts.get(f) {
                idx
            } else {
                let idx = self.fonts.len();
                self.fonts.insert(f.clone(), idx);
                idx
            }
        });

        let fill_id = style.fill.as_ref().map(|fi| {
            if let Some(&idx) = self.fills.get(fi) {
                idx
            } else {
                let idx = self.fills.len();
                self.fills.insert(fi.clone(), idx);
                idx
            }
        });

        let border_id = style.border.as_ref().map(|b| {
            if let Some(&idx) = self.borders.get(b) {
                idx
            } else {
                let idx = self.borders.len();
                self.borders.insert(b.clone(), idx);
                idx
            }
        });

        let num_fmt_id = style.number_format.as_ref().map(|n| {
            if let Some(&idx) = self.number_formats.get(n) {
                idx
            } else {
                let idx = self.number_formats.len() + 164;
                self.number_formats.insert(n.clone(), idx);
                idx
            }
        });

        let xf = CellXf {
            font_id,
            fill_id,
            border_id,
            num_fmt_id,
            alignment: style.alignment.clone(),
            protection: style.protection,
        };

        let _xf_id = self.cell_xfs.len();
        self.cell_xfs.push(xf);

        let style_id = StyleId(self.styles.len());
        self.styles.insert(Arc::new(style), style_id);
        style_id
    }

    /// Return the style for a given id, if registered.
    pub fn get(&self, id: StyleId) -> Option<&Style> {
        self.styles.get_index(id.0).map(|(k, _)| k.as_ref())
    }

    /// Iterate registered styles with their ids.
    pub fn iter(&self) -> impl Iterator<Item = (StyleId, &Style)> {
        self.styles
            .iter()
            .map(|(style, id)| (*id, style.as_ref()))
    }

    /// Number of registered styles.
    pub fn len(&self) -> usize {
        self.styles.len()
    }

    pub fn is_empty(&self) -> bool {
        self.styles.is_empty()
    }

    /// Iterate the unique fonts.
    pub fn fonts(&self) -> &IndexMap<Font, usize> {
        &self.fonts
    }

    /// Iterate the unique fills.
    pub fn fills(&self) -> &IndexMap<Fill, usize> {
        &self.fills
    }

    /// Iterate the unique borders.
    pub fn borders(&self) -> &IndexMap<Border, usize> {
        &self.borders
    }

    /// Iterate the unique number formats.
    pub fn number_formats(&self) -> &IndexMap<NumberFormat, usize> {
        &self.number_formats
    }

    /// Iterate the cell xfs in order of registration.
    pub(crate) fn cell_xfs(&self) -> &[CellXf] {
        &self.cell_xfs
    }
}