cellrune 0.1.18

Bounded XLSX/XLSM reading, deterministic calculation, editing, and writing for Rust
Documentation
use std::collections::BTreeMap;

use crate::{
    CalculationCellId, CalculationCellResult, CalculationSnapshot, CellValue,
    MaterializedResultOrigin,
};

use super::{RecalculationWritePolicy, WriteLimits, XlsxWriteError, XlsxWriteErrorCode};

const DETAIL_INCOMPLETE_CALCULATION: &str =
    "one or more materialized cells do not have a current calculation value";
const DETAIL_MATERIALIZATION_COUNT: &str = "max_materialized_formula_cells";
const DETAIL_FOLLOWER_COUNT: &str = "max_materialized_spill_cells";

#[derive(Debug, Clone, PartialEq)]
pub(crate) enum MaterializationAction {
    Set(CellValue),
    Invalidate,
}

#[derive(Debug, Clone)]
pub(crate) struct PlannedMaterialization {
    pub(crate) origin: MaterializedResultOrigin,
    pub(crate) action: MaterializationAction,
}

pub(crate) struct MaterializationPlan {
    cells: BTreeMap<CalculationCellId, PlannedMaterialization>,
    invalidated_cells: Vec<CalculationCellId>,
    materialized_count: usize,
}

impl MaterializationPlan {
    pub(crate) fn new(
        calculation: &CalculationSnapshot,
        policy: RecalculationWritePolicy,
        limits: WriteLimits,
    ) -> Result<Self, XlsxWriteError> {
        enforce_count(
            DETAIL_MATERIALIZATION_COUNT,
            calculation.len(),
            limits.max_materialized_formula_cells(),
        )?;
        let follower_count = calculation
            .materialized_cells()
            .len()
            .saturating_sub(calculation.len());
        enforce_count(
            DETAIL_FOLLOWER_COUNT,
            follower_count,
            limits.max_materialized_spill_cells(),
        )?;

        let mut cells = BTreeMap::new();
        let mut invalidated_cells = Vec::new();
        let mut materialized_count = 0_usize;
        for (id, materialized) in calculation.materialized_cells() {
            let action = match materialized.result() {
                CalculationCellResult::Value(value) => {
                    materialized_count = materialized_count.saturating_add(1);
                    MaterializationAction::Set(value.clone())
                }
                CalculationCellResult::Unavailable(_) => match policy {
                    RecalculationWritePolicy::RequireComplete => {
                        return Err(
                            XlsxWriteError::new(XlsxWriteErrorCode::IncompleteCalculation)
                                .with_detail(DETAIL_INCOMPLETE_CALCULATION),
                        );
                    }
                    RecalculationWritePolicy::InvalidateUnavailable => {
                        invalidated_cells.push(id);
                        MaterializationAction::Invalidate
                    }
                },
            };
            cells.insert(
                id,
                PlannedMaterialization {
                    origin: materialized.origin(),
                    action,
                },
            );
        }
        Ok(Self {
            cells,
            invalidated_cells,
            materialized_count,
        })
    }

    pub(crate) const fn cells(&self) -> &BTreeMap<CalculationCellId, PlannedMaterialization> {
        &self.cells
    }

    pub(crate) fn invalidated_cells(&self) -> &[CalculationCellId] {
        &self.invalidated_cells
    }

    pub(crate) const fn materialized_count(&self) -> usize {
        self.materialized_count
    }

    pub(crate) const fn is_complete(&self) -> bool {
        self.invalidated_cells.is_empty()
    }
}

fn enforce_count(name: &'static str, actual: usize, maximum: u64) -> Result<(), XlsxWriteError> {
    if actual as u64 > maximum {
        return Err(
            XlsxWriteError::new(XlsxWriteErrorCode::ResourceLimitExceeded)
                .with_detail(format!("{name}: {actual} > {maximum}")),
        );
    }
    Ok(())
}