use std::collections::HashMap;
use truecalc_core::{Engine, ErrorKind, Value};
const PINNED_NOW: f64 = 46180.0 + (23.0 * 3600.0 + 50.0 * 60.0 + 56.808) / 86400.0;
fn eval(formula: &str) -> Value {
Engine::sheets().evaluate(formula, &HashMap::new())
}
fn eval_at(formula: &str, now_serial: f64) -> Value {
Engine::sheets().evaluate_at(formula, &HashMap::new(), now_serial)
}
#[test]
fn date_returns_a_date_typed_value() {
assert_eq!(eval("=DATE(2026,6,7)"), Value::Date(46180.0));
}
#[test]
fn n_of_date_is_a_plain_number() {
assert_eq!(eval("=N(DATE(2026,6,7))"), Value::Number(46180.0));
}
#[test]
fn date_plus_one_day_stays_date_typed() {
assert_eq!(eval("=DATE(2026,6,7)+1"), Value::Date(46181.0));
}
#[test]
fn date_arithmetic_across_month_boundary_stays_date_typed() {
assert_eq!(eval("=DATE(2026,6,30)+1"), Value::Date(46204.0));
}
#[test]
fn date_minus_date_is_a_plain_number() {
assert_eq!(eval("=DATE(2026,6,7)-DATE(2026,6,1)"), Value::Number(6.0));
}
#[test]
fn date_minus_number_stays_date_typed() {
assert_eq!(eval("=DATE(2026,6,7)-1"), Value::Date(46179.0));
assert_eq!(eval("=DATE(2026,6,7)-7"), Value::Date(46173.0));
}
#[test]
fn number_minus_date_is_a_plain_number() {
assert_eq!(eval("=100-DATE(2026,6,7)"), Value::Number(100.0 - 46180.0));
}
#[test]
fn date_times_or_divided_by_number_stays_a_plain_number() {
assert_eq!(eval("=DATE(2026,6,7)*1"), Value::Number(46180.0));
assert_eq!(eval("=DATE(2026,6,7)/1"), Value::Number(46180.0));
}
#[test]
fn isdate_observations() {
assert_eq!(eval("=ISDATE(DATE(2026,6,7))"), Value::Bool(true));
assert_eq!(eval("=ISDATE(DATE(2026,6,7)+1)"), Value::Bool(true));
assert_eq!(eval("=ISDATE(N(DATE(2026,6,7)))"), Value::Bool(false));
}
#[test]
fn plain_serial_literal_stays_number_typed() {
assert_eq!(eval("=45000"), Value::Number(45000.0));
}
#[test]
fn date_compares_equal_to_its_own_serial() {
assert_eq!(eval("=DATE(2026,6,7)=N(DATE(2026,6,7))"), Value::Bool(true));
}
#[test]
fn datevalue_is_number_typed() {
assert_eq!(eval("=DATEVALUE(\"2026-06-07\")"), Value::Number(46180.0));
assert_eq!(eval("=ISDATE(DATEVALUE(\"2026-06-07\"))"), Value::Bool(false));
}
#[test]
fn eomonth_is_date_typed() {
assert_eq!(eval("=ISDATE(EOMONTH(DATE(2026,6,7),0))"), Value::Bool(true));
}
#[test]
fn time_is_date_typed() {
assert_eq!(eval("=ISDATE(TIME(12,0,0))"), Value::Bool(true));
}
#[test]
fn time_24_0_0_wraps_to_zero_and_is_date_typed() {
assert_eq!(eval("=TIME(24,0,0)"), Value::Date(0.0));
assert_eq!(eval("=ISDATE(TIME(24,0,0))"), Value::Bool(true));
}
#[test]
fn workday_intl_is_date_typed() {
assert_eq!(
eval("=ISDATE(WORKDAY.INTL(DATE(2024,1,1),5,1))"),
Value::Bool(true)
);
}
#[test]
fn epochtodate_is_date_typed() {
assert_eq!(eval("=ISDATE(EPOCHTODATE(0))"), Value::Bool(true));
}
#[test]
fn couppcd_is_date_typed() {
assert_eq!(
eval("=ISDATE(COUPPCD(DATE(2011,1,25),DATE(2011,11,15),2,1))"),
Value::Bool(true)
);
}
#[test]
fn sheets_epoch_anchor_serials() {
assert_eq!(eval("=TEXT(0,\"yyyy-mm-dd\")"), Value::Text("1899-12-30".into()));
assert_eq!(eval("=TEXT(1,\"yyyy-mm-dd\")"), Value::Text("1899-12-31".into()));
}
#[test]
fn sheets_has_no_1900_leap_year_bug() {
assert_eq!(eval("=N(DATE(1900,1,1))"), Value::Number(2.0));
assert_eq!(eval("=N(DATE(1900,2,28))"), Value::Number(60.0));
assert_eq!(eval("=N(DATE(1900,3,1))"), Value::Number(61.0));
}
#[test]
fn nonexistent_1900_02_29_rolls_over_to_march_first() {
assert_eq!(eval("=N(DATE(1900,2,29))"), Value::Number(61.0));
assert_eq!(
eval("=TEXT(DATE(1900,2,29),\"yyyy-mm-dd\")"),
Value::Text("1900-03-01".into())
);
}
#[test]
fn plus_1900_year_rule_for_year_arguments_below_1900() {
assert_eq!(eval("=N(DATE(1899,12,31))"), Value::Number(693962.0));
assert_eq!(eval("=N(DATE(1899,12,30))"), Value::Number(693961.0));
assert_eq!(eval("=N(DATE(1899,12,25))"), Value::Number(693956.0));
assert_eq!(
eval("=TEXT(DATE(1899,12,25),\"yyyy-mm-dd\")"),
Value::Text("3799-12-25".into())
);
}
#[test]
fn pinned_today_matches_fixture_row() {
assert_eq!(eval_at("=TODAY()", PINNED_NOW), Value::Date(46180.0));
}
#[test]
fn pinned_today_is_date_typed() {
assert_eq!(eval_at("=ISDATE(TODAY())", PINNED_NOW), Value::Bool(true));
}
#[test]
fn today_minus_today_within_one_recalc_is_zero() {
assert_eq!(eval_at("=TODAY()-TODAY()", PINNED_NOW), Value::Number(0.0));
}
#[test]
fn pinned_now_returns_the_pinned_serial() {
assert_eq!(eval_at("=NOW()", PINNED_NOW), Value::Date(PINNED_NOW));
}
#[test]
fn evaluate_at_is_deterministic() {
assert_eq!(
eval_at("=TODAY()&\"|\"&NOW()", PINNED_NOW),
eval_at("=TODAY()&\"|\"&NOW()", PINNED_NOW)
);
}
#[test]
fn evaluate_at_rejects_non_finite_now() {
assert_eq!(
eval_at("=TODAY()", f64::NAN),
Value::Error(ErrorKind::Num)
);
assert_eq!(
eval_at("=TODAY()", f64::INFINITY),
Value::Error(ErrorKind::Num)
);
}
#[test]
fn array_literal_flows_through_unspilled() {
assert_eq!(
eval("={1,2,3}"),
Value::Array(vec![
Value::Number(1.0),
Value::Number(2.0),
Value::Number(3.0),
])
);
}
#[test]
fn array_through_function_result_is_unspilled() {
assert_eq!(
eval("=IF(TRUE,{1,2},0)"),
Value::Array(vec![Value::Number(1.0), Value::Number(2.0)])
);
}
#[test]
fn excel_evaluation_is_still_stubbed() {
assert!(matches!(
Engine::excel().evaluate("=DATE(2026,6,7)", &HashMap::new()),
Value::Error(_) | Value::ErrorMsg(_, _)
));
}