rulemorph 0.3.4

YAML-based declarative data transformation engine for CSV/JSON to JSON
Documentation
use calamine::{Data, ExcelDateTime, Range};
use serde_json::{Number as JsonNumber, Value as JsonValue};

use crate::model::ExcelDatePolicy;

use super::*;

pub(super) fn excel_cell_to_json(
    cell: Option<&Data>,
    formula: Option<&String>,
    excel: &ExcelInput,
    options: &NormalizationOptions,
) -> Result<Option<JsonValue>, TransformError> {
    let cell = cell.unwrap_or(&Data::Empty);
    if let Some(formula) = formula.filter(|formula| !formula.trim().is_empty()) {
        match excel.formula {
            ExcelFormulaPolicy::Error => {
                return Err(invalid("Excel formula cell is not supported"));
            }
            ExcelFormulaPolicy::Formula => {
                return checked_string(formula.clone(), options).map(Some);
            }
            ExcelFormulaPolicy::Cached => {
                if matches!(cell, Data::Empty) {
                    return Err(invalid("Excel formula cell is missing a cached value"));
                }
            }
        }
    }
    match cell {
        Data::Empty => Ok(None),
        Data::String(value) => checked_string(value.clone(), options).map(Some),
        Data::Float(value) => excel_float_to_json(*value).map(Some),
        Data::Int(value) => Ok(Some(JsonValue::Number((*value).into()))),
        Data::Bool(value) => Ok(Some(JsonValue::Bool(*value))),
        Data::DateTime(value) => excel_datetime_to_json(value, excel.date),
        Data::DateTimeIso(value) | Data::DurationIso(value) => {
            checked_string(value.clone(), options).map(Some)
        }
        Data::Error(_) => Err(invalid("Excel error cell is not supported")),
    }
}

fn excel_datetime_to_json(
    value: &ExcelDateTime,
    policy: ExcelDatePolicy,
) -> Result<Option<JsonValue>, TransformError> {
    match policy {
        ExcelDatePolicy::Serial => JsonNumber::from_f64(value.as_f64())
            .map(JsonValue::Number)
            .map(Some)
            .ok_or_else(|| invalid("Excel datetime serial is not JSON-compatible")),
        ExcelDatePolicy::Iso8601 | ExcelDatePolicy::String => {
            let datetime = value
                .as_datetime()
                .ok_or_else(|| invalid("Excel datetime is invalid"))?;
            let text = datetime.format("%Y-%m-%dT%H:%M:%S").to_string();
            Ok(Some(JsonValue::String(text)))
        }
    }
}

fn checked_string(
    value: String,
    options: &NormalizationOptions,
) -> Result<JsonValue, TransformError> {
    if value.len() > options.max_text_bytes {
        return Err(invalid("input exceeds max_text_bytes"));
    }
    Ok(JsonValue::String(value))
}

fn excel_float_to_json(value: f64) -> Result<JsonValue, TransformError> {
    if !value.is_finite() {
        return Err(invalid("Excel float is not JSON-compatible"));
    }
    if value.fract() == 0.0 && value >= i64::MIN as f64 && value <= i64::MAX as f64 {
        return Ok(JsonValue::Number((value as i64).into()));
    }
    JsonNumber::from_f64(value)
        .map(JsonValue::Number)
        .ok_or_else(|| invalid("Excel float is not JSON-compatible"))
}

pub(super) fn cell_at<'a>(rows: &'a [&'a [Data]], row: usize, col: usize) -> Option<&'a Data> {
    rows.get(row).and_then(|row| row.get(col))
}

pub(super) fn formula_at(
    formulas: Option<&Range<String>>,
    row: usize,
    col: usize,
) -> Option<&String> {
    let row = u32::try_from(row).ok()?;
    let col = u32::try_from(col).ok()?;
    formulas.and_then(|formulas| formulas.get_value((row, col)))
}