cellrune 0.1.18

Bounded XLSX/XLSM reading, deterministic calculation, editing, and writing for Rust
Documentation
use super::super::ast::Expr;
use super::super::coerce::{to_logical, to_number, to_text};
use super::super::eval::{Engine, EvalContext};
use super::super::runtime::Array;
use super::super::value::{ErrorKind, Value};
use super::array_common::{poll_cancellation, validate_array_input};
use super::util::required_text;

struct Delimiter {
    text: String,
    folded: String,
    characters: usize,
}

struct DelimiterSearch<'a> {
    text: &'a str,
    boundaries: &'a [usize],
    delimiters: &'a [Delimiter],
    comparison_work: u64,
    case_insensitive: bool,
}

pub(super) fn call_scalar(engine: &Engine<'_>, context: EvalContext<'_>, args: &[Expr]) -> Value {
    match call_array(engine, context, args) {
        Ok(array) => array
            .data
            .into_iter()
            .next()
            .unwrap_or(Value::Error(ErrorKind::Calc)),
        Err(kind) => Value::Error(kind),
    }
}

pub(super) fn call_array(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    args: &[Expr],
) -> Result<Array, ErrorKind> {
    if args.len() < 2 || args.len() > 6 {
        return Err(ErrorKind::Value);
    }
    let text = required_text(engine, context, &args[0])?;
    engine.ensure_text_bytes(text.len())?;
    engine.charge_function_iterations(context, text.len() as u64)?;

    let column_delimiters = delimiters(engine, context, args.get(1))?;
    let row_delimiters = delimiters(engine, context, args.get(2))?;
    if column_delimiters.is_empty() && row_delimiters.is_empty() {
        return Err(ErrorKind::Value);
    }
    let ignore_empty = args.get(3).map_or(Ok(false), |expression| {
        if matches!(expression, Expr::Missing) {
            Ok(false)
        } else {
            to_logical(&engine.eval_scalar(context, expression))
        }
    })?;
    let case_insensitive = args.get(4).map_or(Ok(false), |expression| {
        if matches!(expression, Expr::Missing) {
            Ok(false)
        } else {
            match to_number(&engine.eval_scalar(context, expression))?.trunc() {
                0.0 => Ok(false),
                1.0 => Ok(true),
                _ => Err(ErrorKind::Value),
            }
        }
    })?;
    let padding = match args.get(5) {
        None | Some(Expr::Missing) => Value::Error(ErrorKind::NA),
        Some(expression) => engine.eval_scalar(context, expression),
    };
    if let Value::Error(kind) = padding
        && kind.is_engine_issue()
    {
        return Err(kind);
    }
    if let Value::Text(text) = &padding {
        engine.ensure_text_bytes(text.len())?;
    }

    let rows = split(
        engine,
        context,
        &text,
        &row_delimiters,
        ignore_empty,
        case_insensitive,
    )?;
    let mut split_rows = Vec::with_capacity(rows.len());
    let mut columns = 0_usize;
    for row in rows {
        poll_cancellation(context)?;
        let cells = split(
            engine,
            context,
            &row,
            &column_delimiters,
            ignore_empty,
            case_insensitive,
        )?;
        columns = columns.max(cells.len());
        split_rows.push(cells);
    }
    if split_rows.is_empty() || columns == 0 {
        return Err(ErrorKind::Calc);
    }
    let row_count = u32::try_from(split_rows.len()).map_err(|_| ErrorKind::Num)?;
    let column_count = u32::try_from(columns).map_err(|_| ErrorKind::Num)?;
    let cells = u64::from(row_count)
        .checked_mul(u64::from(column_count))
        .ok_or(ErrorKind::Num)?;
    engine.ensure_array_cells(cells)?;
    engine.charge_function_iterations(context, cells)?;
    let populated_cells = split_rows.iter().try_fold(0_u64, |count, row| {
        count
            .checked_add(u64::try_from(row.len()).map_err(|_| ErrorKind::Num)?)
            .ok_or(ErrorKind::Num)
    })?;
    let padding_cells = cells.checked_sub(populated_cells).ok_or(ErrorKind::Num)?;
    if let Value::Text(text) = &padding {
        let padding_bytes = padding_cells
            .checked_mul(u64::try_from(text.len()).map_err(|_| ErrorKind::Num)?)
            .ok_or(ErrorKind::Num)?;
        engine.charge_function_iterations(context, padding_bytes)?;
    }

    let mut data = Vec::with_capacity(usize::try_from(cells).map_err(|_| ErrorKind::Num)?);
    for row in split_rows {
        let row_length = row.len();
        data.extend(row.into_iter().map(Value::Text));
        data.resize(data.len() + columns - row_length, padding.clone());
    }
    Ok(Array {
        rows: row_count,
        cols: column_count,
        data,
    })
}

fn delimiters(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    expression: Option<&Expr>,
) -> Result<Vec<Delimiter>, ErrorKind> {
    let Some(expression) = expression else {
        return Ok(Vec::new());
    };
    if matches!(expression, Expr::Missing) {
        return Ok(Vec::new());
    }
    let array = engine.eval_array(context, expression)?;
    validate_array_input(engine, context, &array)?;
    array
        .data
        .into_iter()
        .map(|value| {
            let text = to_text(&value)?;
            if text.is_empty() {
                return Err(ErrorKind::Value);
            }
            engine.ensure_text_bytes(text.len())?;
            let preprocessing_work = u64::try_from(text.len())
                .map_err(|_| ErrorKind::Num)?
                .checked_mul(2)
                .ok_or(ErrorKind::Num)?;
            engine.charge_function_iterations(context, preprocessing_work)?;
            poll_cancellation(context)?;
            Ok(Delimiter {
                folded: text.to_lowercase(),
                characters: text.chars().count(),
                text,
            })
        })
        .collect()
}

fn split(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    text: &str,
    delimiters: &[Delimiter],
    ignore_empty: bool,
    case_insensitive: bool,
) -> Result<Vec<String>, ErrorKind> {
    if delimiters.is_empty() {
        return Ok(vec![text.to_owned()]);
    }
    let boundaries: Vec<usize> = text
        .char_indices()
        .map(|(index, _)| index)
        .chain(std::iter::once(text.len()))
        .collect();
    let comparison_work = delimiters.iter().try_fold(0_u64, |work, delimiter| {
        work.checked_add(delimiter.characters.max(1) as u64)
            .ok_or(ErrorKind::Num)
    })?;
    let mut output = Vec::new();
    let mut cursor = 0;
    let mut boundary_index = 0;
    let search = DelimiterSearch {
        text,
        boundaries: &boundaries,
        delimiters,
        comparison_work,
        case_insensitive,
    };
    while let Some((start, end, next_boundary)) =
        next_match(engine, context, &search, boundary_index)?
    {
        poll_cancellation(context)?;
        let piece = text.get(cursor..start).ok_or(ErrorKind::Value)?;
        if !ignore_empty || !piece.is_empty() {
            output.push(piece.to_owned());
        }
        cursor = end;
        boundary_index = next_boundary;
    }
    let tail = text.get(cursor..).ok_or(ErrorKind::Value)?;
    if !ignore_empty || !tail.is_empty() {
        output.push(tail.to_owned());
    }
    Ok(output)
}

fn next_match(
    engine: &Engine<'_>,
    context: EvalContext<'_>,
    search: &DelimiterSearch<'_>,
    boundary_index: usize,
) -> Result<Option<(usize, usize, usize)>, ErrorKind> {
    for index in boundary_index..search.boundaries.len().saturating_sub(1) {
        poll_cancellation(context)?;
        engine.charge_function_iterations(context, search.comparison_work)?;
        let start = search.boundaries[index];
        let mut matched_end = None;
        for delimiter in search.delimiters {
            let Some(end_index) = index.checked_add(delimiter.characters) else {
                continue;
            };
            let Some(&end) = search.boundaries.get(end_index) else {
                continue;
            };
            let candidate = search.text.get(start..end).ok_or(ErrorKind::Value)?;
            let matched = if search.case_insensitive {
                candidate.to_lowercase() == delimiter.folded
            } else {
                candidate == delimiter.text
            };
            if matched && matched_end.is_none_or(|current| end > current) {
                matched_end = Some(end);
            }
        }
        if let Some(end) = matched_end {
            let next_boundary = search
                .boundaries
                .binary_search(&end)
                .map_err(|_| ErrorKind::Value)?;
            return Ok(Some((start, end, next_boundary)));
        }
    }
    Ok(None)
}