use crate::engine::{CycleConfig, Engine, EvalConfig};
use crate::test_workbook::TestWorkbook;
use crate::timezone::{ClockProvider, TimeZoneSpec};
use chrono::{Duration, NaiveDateTime, TimeZone};
use formualizer_common::LiteralValue;
use formualizer_parse::parser::parse;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug)]
struct TickingClock {
start: NaiveDateTime,
step: Duration,
calls: AtomicU64,
timezone: TimeZoneSpec,
}
impl TickingClock {
fn new(start: NaiveDateTime, step: Duration) -> Self {
Self {
start,
step,
calls: AtomicU64::new(0),
timezone: TimeZoneSpec::Utc,
}
}
}
impl ClockProvider for TickingClock {
fn timezone(&self) -> &TimeZoneSpec {
&self.timezone
}
fn now(&self) -> NaiveDateTime {
let n = self.calls.fetch_add(1, Ordering::SeqCst) as i32;
self.start + self.step * n
}
}
fn start_instant() -> NaiveDateTime {
chrono::Utc
.with_ymd_and_hms(2025, 6, 1, 12, 0, 0)
.single()
.expect("valid timestamp")
.naive_utc()
}
fn set_formula(engine: &mut Engine<TestWorkbook>, sheet: &str, row: u32, col: u32, f: &str) {
engine
.set_cell_formula(sheet, row, col, parse(f).expect("parse"))
.expect("set formula");
}
fn num(engine: &Engine<TestWorkbook>, sheet: &str, row: u32, col: u32) -> f64 {
match engine.get_cell_value(sheet, row, col) {
Some(LiteralValue::Number(n)) => n,
other => panic!("expected number at {sheet} r{row}c{col}, got {other:?}"),
}
}
#[test]
fn now_agrees_across_cells_within_one_recalc_and_advances_across_recalcs() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
engine.set_clock(Arc::new(TickingClock::new(
start_instant(),
Duration::days(1),
)));
set_formula(&mut engine, "Sheet1", 1, 1, "=NOW()");
set_formula(&mut engine, "Sheet1", 2, 1, "=NOW()");
engine.evaluate_all().unwrap();
let a1_first = num(&engine, "Sheet1", 1, 1);
let a2_first = num(&engine, "Sheet1", 2, 1);
assert_eq!(
a1_first, a2_first,
"two NOW() cells in one recalc must observe the same clock sample"
);
engine.evaluate_all().unwrap();
let a1_second = num(&engine, "Sheet1", 1, 1);
assert!(
a1_second > a1_first,
"NOW() must advance across recalcs (got {a1_first} then {a1_second})"
);
assert_eq!(num(&engine, "Sheet1", 1, 1), num(&engine, "Sheet1", 2, 1));
}
#[test]
fn now_is_stable_across_iteration_passes_within_one_recalc() {
let mut engine = Engine::new(
TestWorkbook::new(),
EvalConfig::default().with_cycle(CycleConfig::iterate(100, 0.001)),
);
engine.set_clock(Arc::new(TickingClock::new(
start_instant(),
Duration::days(1),
)));
set_formula(&mut engine, "Sheet1", 1, 1, "=A1*0+NOW()");
engine.evaluate_all().unwrap();
let t = engine.last_cycle_telemetry();
assert_eq!(
t.iterated_sccs, 1,
"self-referent NOW() cell must enter iterative calculation"
);
assert_eq!(
t.converged_sccs, 1,
"NOW() must be frozen within the recalc so the SCC converges \
(telemetry: {t:?})"
);
assert_eq!(t.capped_sccs, 0, "no pass cap expected (telemetry: {t:?})");
assert_eq!(
t.max_abs_delta_at_stop, 0.0,
"both passes must observe the identical clock sample"
);
let first = num(&engine, "Sheet1", 1, 1);
engine.evaluate_all().unwrap();
let t = engine.last_cycle_telemetry();
assert_eq!(
(t.iterated_sccs, t.converged_sccs, t.capped_sccs),
(1, 1, 0)
);
let second = num(&engine, "Sheet1", 1, 1);
assert!(
second > first,
"NOW() must advance across recalcs (got {first} then {second})"
);
}
#[test]
fn today_and_now_share_one_sample_per_recalc() {
let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
let start = chrono::Utc
.with_ymd_and_hms(2025, 6, 1, 23, 59, 59)
.single()
.unwrap()
.naive_utc();
engine.set_clock(Arc::new(TickingClock::new(start, Duration::hours(1))));
set_formula(&mut engine, "Sheet1", 1, 1, "=TODAY()");
set_formula(&mut engine, "Sheet1", 2, 1, "=INT(NOW())");
engine.evaluate_all().unwrap();
assert_eq!(
num(&engine, "Sheet1", 1, 1),
num(&engine, "Sheet1", 2, 1),
"TODAY() and INT(NOW()) must agree within one recalc"
);
}