use chrono::{Duration as ChronoDuration, NaiveDate, NaiveDateTime, NaiveTime};
use formualizer_common::{
RangeAddress,
error::{ExcelError, ExcelErrorKind},
};
#[cfg(feature = "json")]
use formualizer_workbook::JsonAdapter;
#[cfg(feature = "json")]
use formualizer_workbook::backends::json::JsonReadOptions;
use formualizer_workbook::{CellData, LiteralValue, SpreadsheetReader, SpreadsheetWriter};
#[cfg(feature = "json")]
#[test]
fn json_loader_declares_sources_before_formulas() {
use formualizer_workbook::{LoadStrategy, Workbook, WorkbookConfig};
let bytes = br#"{
"version": 1,
"sources": [
{ "type": "scalar", "name": "Foo", "version": 1 }
],
"sheets": {
"S": {
"cells": [
{ "row": 1, "col": 1, "formula": "Foo" }
]
}
}
}"#
.to_vec();
let adapter = JsonAdapter::open_bytes(bytes).unwrap();
let cfg = WorkbookConfig::interactive();
let mut wb = Workbook::from_reader(adapter, LoadStrategy::EagerAll, cfg).unwrap();
let v = wb.evaluate_cell("S", 1, 1).unwrap();
match v {
LiteralValue::Error(e) => assert_eq!(e.kind, ExcelErrorKind::Ref),
other => panic!("expected #REF!, got {other:?}"),
}
}
#[cfg(feature = "json")]
#[test]
fn json_loader_declares_sources_before_formula_ingest_eager_mode() {
use formualizer_eval::engine::ingest::EngineLoadStream;
use formualizer_eval::engine::{Engine, EvalConfig};
use formualizer_eval::traits::{
EvaluationContext, FunctionProvider, NamedRangeResolver, Range, RangeResolver,
ReferenceResolver, Resolver, SourceResolver, Table, TableResolver,
};
use formualizer_parse::parser::TableReference;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
#[derive(Clone)]
struct Ctx {
v: Arc<RwLock<LiteralValue>>,
calls: Arc<AtomicUsize>,
}
impl Ctx {
fn new(v: LiteralValue) -> Self {
Self {
v: Arc::new(RwLock::new(v)),
calls: Arc::new(AtomicUsize::new(0)),
}
}
fn set(&self, v: LiteralValue) {
*self.v.write().unwrap() = v;
}
fn calls(&self) -> usize {
self.calls.load(Ordering::Relaxed)
}
}
impl ReferenceResolver for Ctx {
fn resolve_cell_reference(
&self,
_sheet: Option<&str>,
_row: u32,
_col: u32,
) -> Result<LiteralValue, formualizer_common::error::ExcelError> {
Err(formualizer_common::error::ExcelError::new(
ExcelErrorKind::NImpl,
))
}
}
impl RangeResolver for Ctx {
fn resolve_range_reference(
&self,
_sheet: Option<&str>,
_sr: Option<u32>,
_sc: Option<u32>,
_er: Option<u32>,
_ec: Option<u32>,
) -> Result<Box<dyn Range>, formualizer_common::error::ExcelError> {
Err(formualizer_common::error::ExcelError::new(
ExcelErrorKind::NImpl,
))
}
}
impl NamedRangeResolver for Ctx {
fn resolve_named_range_reference(
&self,
_name: &str,
) -> Result<Vec<Vec<LiteralValue>>, formualizer_common::error::ExcelError> {
Err(formualizer_common::error::ExcelError::new(
ExcelErrorKind::Name,
))
}
}
impl TableResolver for Ctx {
fn resolve_table_reference(
&self,
_tref: &TableReference,
) -> Result<Box<dyn Table>, formualizer_common::error::ExcelError> {
Err(formualizer_common::error::ExcelError::new(
ExcelErrorKind::NImpl,
))
}
}
impl SourceResolver for Ctx {
fn resolve_source_scalar(
&self,
_name: &str,
) -> Result<LiteralValue, formualizer_common::error::ExcelError> {
self.calls.fetch_add(1, Ordering::Relaxed);
Ok(self.v.read().unwrap().clone())
}
}
impl FunctionProvider for Ctx {
fn get_function(
&self,
ns: &str,
name: &str,
) -> Option<std::sync::Arc<dyn formualizer_eval::function::Function>> {
formualizer_eval::function_registry::get(ns, name)
}
}
impl Resolver for Ctx {}
impl EvaluationContext for Ctx {}
let ctx = Ctx::new(LiteralValue::Number(1.0));
let bytes = br#"{
"version": 1,
"sources": [
{ "type": "scalar", "name": "Foo", "version": 1 }
],
"sheets": {
"S": {
"cells": [
{ "row": 1, "col": 1, "formula": "Foo" }
]
}
}
}"#
.to_vec();
let mut adapter = JsonAdapter::open_bytes(bytes).unwrap();
let cfg = EvalConfig {
defer_graph_building: false,
..Default::default()
};
let mut engine: Engine<_> = Engine::new(ctx.clone(), cfg);
adapter.stream_into_engine(&mut engine).unwrap();
let v1 = engine
.evaluate_cell("S", 1, 1)
.unwrap()
.expect("computed value");
assert_eq!(v1, LiteralValue::Number(1.0));
ctx.set(LiteralValue::Number(2.0));
engine.invalidate_source("Foo").unwrap();
let v2 = engine
.evaluate_cell("S", 1, 1)
.unwrap()
.expect("computed value");
assert_eq!(v2, LiteralValue::Number(2.0));
assert!(ctx.calls() >= 2);
}
#[cfg(feature = "json")]
#[test]
fn json_roundtrip_in_memory_bytes() {
let mut adapter = JsonAdapter::new();
adapter.create_sheet("Sheet1").unwrap();
adapter
.write_cell("Sheet1", 1, 1, CellData::from_value(42.0))
.unwrap();
adapter
.write_cell("Sheet1", 2, 1, CellData::from_formula("=A1*2"))
.unwrap();
let bytes = adapter.save_to_bytes().unwrap();
let mut adapter2 = JsonAdapter::open_bytes(bytes).unwrap();
let data = adapter2.read_sheet("Sheet1").unwrap();
assert_eq!(
data.cells.get(&(1, 1)).unwrap().value,
Some(LiteralValue::Number(42.0))
);
assert_eq!(
data.cells.get(&(2, 1)).unwrap().formula.as_deref(),
Some("=A1*2")
);
}
#[cfg(feature = "json")]
#[test]
fn json_strict_dates_reject_invalid_date_strings() {
let bytes = br#"{
"version": 1,
"sheets": {
"S": {
"cells": [
{ "row": 1, "col": 1, "value": { "type": "Date", "value": "not-a-date" } }
]
}
}
}"#
.to_vec();
let mut adapter = JsonAdapter::open_bytes(bytes).unwrap();
let err = adapter
.read_sheet("S")
.expect_err("expected strict parse error");
match err {
formualizer_workbook::IoError::Backend { backend, message } => {
assert_eq!(backend, "json");
assert!(message.contains("Invalid date"));
}
other => panic!("expected backend error, got {other:?}"),
}
}
#[cfg(feature = "json")]
#[test]
fn json_non_strict_dates_preserve_invalid_as_text() {
let bytes = br#"{
"version": 1,
"sheets": {
"S": {
"cells": [
{ "row": 1, "col": 1, "value": { "type": "Date", "value": "not-a-date" } }
]
}
}
}"#
.to_vec();
let mut adapter = JsonAdapter::open_bytes(bytes).unwrap();
adapter.set_read_options(JsonReadOptions {
strict_dates: false,
});
let sheet = adapter.read_sheet("S").unwrap();
let v = sheet
.cells
.get(&(1, 1))
.and_then(|c| c.value.clone())
.unwrap();
assert_eq!(v, LiteralValue::Text("not-a-date".to_string()));
}
#[cfg(feature = "json")]
#[test]
fn json_defined_names_roundtrip() {
use formualizer_workbook::backends::JsonAdapter;
use formualizer_workbook::traits::{DefinedName, DefinedNameDefinition, DefinedNameScope};
let mut adapter = JsonAdapter::new();
adapter.create_sheet("Sheet1").unwrap();
adapter.create_sheet("Sheet2").unwrap();
let global_range = DefinedName {
name: "GlobalRange".to_string(),
scope: DefinedNameScope::Workbook,
scope_sheet: None,
definition: DefinedNameDefinition::Range {
address: RangeAddress::new("Sheet2", 1, 1, 1, 2).unwrap(),
},
};
let sheet_literal = DefinedName {
name: "LocalLit".to_string(),
scope: DefinedNameScope::Sheet,
scope_sheet: Some("Sheet1".to_string()),
definition: DefinedNameDefinition::Literal {
value: LiteralValue::Number(3.5),
},
};
let scoped_other_sheet_range = DefinedName {
name: "ScopedOther".to_string(),
scope: DefinedNameScope::Sheet,
scope_sheet: Some("Sheet1".to_string()),
definition: DefinedNameDefinition::Range {
address: RangeAddress::new("Sheet2", 2, 1, 2, 1).unwrap(),
},
};
adapter.set_defined_names(vec![
global_range.clone(),
sheet_literal.clone(),
scoped_other_sheet_range.clone(),
]);
let bytes = adapter.save_to_bytes().unwrap();
let mut adapter2 = JsonAdapter::open_bytes(bytes).unwrap();
let mut got = adapter2.defined_names().unwrap();
got.sort_by(|a, b| {
(a.name.clone(), a.scope_sheet.clone()).cmp(&(b.name.clone(), b.scope_sheet.clone()))
});
let mut expected = vec![global_range, sheet_literal, scoped_other_sheet_range];
expected.sort_by(|a, b| {
(a.name.clone(), a.scope_sheet.clone()).cmp(&(b.name.clone(), b.scope_sheet.clone()))
});
assert_eq!(got, expected);
}
#[cfg(feature = "json")]
#[test]
fn json_schema_shape() {
let mut adapter = JsonAdapter::new();
adapter.create_sheet("Sheet1").unwrap();
adapter
.write_cell("Sheet1", 1, 1, CellData::from_value(42.0))
.unwrap();
let s = adapter.to_json_string().unwrap();
let v: serde_json::Value = serde_json::from_str(&s).unwrap();
assert!(v["sheets"].is_object());
assert!(v["sheets"]["Sheet1"].is_object());
assert!(v["sheets"]["Sheet1"]["cells"].is_array());
assert_eq!(v["sheets"]["Sheet1"]["cells"][0]["row"], 1);
assert_eq!(v["sheets"]["Sheet1"]["cells"][0]["col"], 1);
assert_eq!(v["sheets"]["Sheet1"]["cells"][0]["value"]["type"], "Number");
assert_eq!(v["sheets"]["Sheet1"]["cells"][0]["value"]["value"], 42.0);
}
#[cfg(feature = "json")]
#[test]
fn json_transaction_commit_and_rollback() {
use formualizer_workbook::WriteTransaction;
let mut adapter = JsonAdapter::new();
adapter.create_sheet("Sheet1").unwrap();
{
let mut tx = WriteTransaction::new(&mut adapter);
tx.write_cell("Sheet1", 1, 1, CellData::from_value(1.0));
}
let sheet = adapter.read_sheet("Sheet1").unwrap();
assert!(sheet.cells.is_empty());
{
let mut tx = WriteTransaction::new(&mut adapter);
tx.write_cell("Sheet1", 1, 1, CellData::from_value(10.0));
tx.commit().unwrap();
}
let sheet = adapter.read_sheet("Sheet1").unwrap();
assert_eq!(
sheet.cells.get(&(1, 1)).unwrap().value,
Some(LiteralValue::Number(10.0))
);
}
#[cfg(feature = "json")]
#[test]
fn json_value_variants_roundtrip() {
let mut adapter = JsonAdapter::new();
adapter.create_sheet("Sheet1").unwrap();
let d = NaiveDate::from_ymd_opt(2020, 1, 2).unwrap();
let dt = NaiveDateTime::new(d, NaiveTime::from_hms_opt(3, 4, 5).unwrap());
let t = NaiveTime::from_hms_opt(12, 34, 56).unwrap();
let dur = ChronoDuration::seconds(3600);
adapter
.write_cell("Sheet1", 1, 1, CellData::from_value(LiteralValue::Date(d)))
.unwrap();
adapter
.write_cell(
"Sheet1",
2,
1,
CellData::from_value(LiteralValue::DateTime(dt)),
)
.unwrap();
adapter
.write_cell("Sheet1", 3, 1, CellData::from_value(LiteralValue::Time(t)))
.unwrap();
adapter
.write_cell(
"Sheet1",
4,
1,
CellData::from_value(LiteralValue::Duration(dur)),
)
.unwrap();
adapter
.write_cell(
"Sheet1",
5,
1,
CellData::from_value(LiteralValue::Array(vec![
vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
])),
)
.unwrap();
adapter
.write_cell(
"Sheet1",
6,
1,
CellData::from_value(LiteralValue::Error(ExcelError::from_error_string(
"#DIV/0!",
))),
)
.unwrap();
adapter
.write_cell("Sheet1", 7, 1, CellData::from_value(LiteralValue::Pending))
.unwrap();
let bytes = adapter.save_to_bytes().unwrap();
let mut adapter2 = JsonAdapter::open_bytes(bytes).unwrap();
let sheet = adapter2.read_sheet("Sheet1").unwrap();
assert!(
matches!(sheet.cells.get(&(1,1)).unwrap().value, Some(LiteralValue::Date(d2)) if d2==d)
);
assert!(
matches!(sheet.cells.get(&(2,1)).unwrap().value, Some(LiteralValue::DateTime(dt2)) if dt2==dt)
);
assert!(
matches!(sheet.cells.get(&(3,1)).unwrap().value, Some(LiteralValue::Time(t2)) if t2==t)
);
assert!(
matches!(sheet.cells.get(&(4,1)).unwrap().value, Some(LiteralValue::Duration(d2)) if d2==dur)
);
assert!(
matches!(sheet.cells.get(&(6,1)).unwrap().value, Some(LiteralValue::Error(ref e)) if e == "#DIV/0!")
);
assert!(matches!(
sheet.cells.get(&(7, 1)).unwrap().value,
Some(LiteralValue::Pending)
));
if let Some(LiteralValue::Array(arr)) = &sheet.cells.get(&(5, 1)).unwrap().value {
assert_eq!(arr.len(), 2);
assert!(matches!(arr[0][0], LiteralValue::Number(n) if (n-1.0).abs()<1e-9));
assert!(matches!(arr[1][1], LiteralValue::Number(n) if (n-4.0).abs()<1e-9));
} else {
panic!("Array not roundtripped");
}
}
#[cfg(feature = "json")]
#[test]
fn json_metadata_roundtrip() {
use formualizer_workbook::traits::NamedRangeScope;
use formualizer_workbook::{MergedRange, NamedRange, TableDefinition};
let mut adapter = JsonAdapter::new();
adapter.create_sheet("Meta").unwrap();
adapter.set_dimensions("Meta", Some((100, 50)));
adapter.set_date_system_1904("Meta", true);
adapter.set_merged_cells(
"Meta",
vec![MergedRange {
start_row: 1,
start_col: 1,
end_row: 2,
end_col: 3,
}],
);
adapter.set_tables(
"Meta",
vec![TableDefinition {
name: "T1".into(),
range: (1, 1, 10, 3),
header_row: true,
headers: vec!["A".into(), "B".into(), "C".into()],
totals_row: false,
}],
);
adapter.set_named_ranges(
"Meta",
vec![NamedRange {
name: "R1".into(),
scope: NamedRangeScope::Workbook,
address: RangeAddress::new("Meta", 1, 1, 2, 2).unwrap(),
}],
);
let bytes = adapter.save_to_bytes().unwrap();
let mut adapter2 = JsonAdapter::open_bytes(bytes).unwrap();
let sheet = adapter2.read_sheet("Meta").unwrap();
assert_eq!(sheet.dimensions, Some((100, 50)));
assert!(sheet.date_system_1904);
assert_eq!(sheet.merged_cells.len(), 1);
assert_eq!(sheet.tables.len(), 1);
assert!(sheet.tables[0].header_row);
assert_eq!(sheet.named_ranges.len(), 1);
}