truecalc_core/eval/context/mod.rs
1use std::collections::HashMap;
2use crate::types::Value;
3
4/// Holds the named variable bindings for a formula evaluation.
5///
6/// All keys are stored and looked up in uppercase, so variable names are
7/// case-insensitive (`A1`, `a1`, and `A1` all refer to the same binding).
8pub struct Context {
9 pub vars: HashMap<String, Value>,
10 /// Pinned "now" for volatile date functions (`NOW`, `TODAY`), as a
11 /// local-time spreadsheet serial datetime (integer part = day serial,
12 /// fraction = time of day). `None` ⇒ the functions read the ambient
13 /// local clock. Set via [`crate::Engine::evaluate_at`] (P1.4, issue #526).
14 pub now_serial: Option<f64>,
15 /// Pinned "now" as an absolute UTC instant (nanoseconds since the Unix
16 /// epoch), for the zone-aware `TZNOW`. `None` ⇒ `TZNOW` reads the ambient
17 /// UTC clock. Set from the workbook's `timestamp_ms` during recalc; distinct
18 /// from `now_serial` (a tz-naive local serial for `NOW`/`TODAY`).
19 pub now_utc_nanos: Option<i64>,
20 /// Per-cell RNG key for deterministic draws: (seed, sheet_index, row, col).
21 /// Set by the workbook recalc loop; None for bare Engine::evaluate calls.
22 pub rng_cell: Option<(u64, u32, u32, u32)>,
23}
24
25impl Context {
26 /// Create a `Context` from a map of variable name → value.
27 /// Keys are normalised to uppercase on insertion.
28 pub fn new(vars: HashMap<String, Value>) -> Self {
29 let normalized = vars.into_iter()
30 .map(|(k, v)| (k.to_uppercase(), v))
31 .collect();
32 Self { vars: normalized, now_serial: None, now_utc_nanos: None, rng_cell: None }
33 }
34
35 /// Create an empty `Context` with no variable bindings.
36 pub fn empty() -> Self {
37 Self { vars: HashMap::new(), now_serial: None, now_utc_nanos: None, rng_cell: None }
38 }
39
40 /// Look up a variable by name (case-insensitive). Returns `Value::Empty` if not found.
41 pub fn get(&self, name: &str) -> Value {
42 self.vars
43 .get(&name.to_uppercase())
44 .cloned()
45 .unwrap_or(Value::Empty)
46 }
47
48 /// Look up a binding by name (case-insensitive), distinguishing an absent
49 /// binding (`None`) from one explicitly bound to [`Value::Empty`]
50 /// (`Some(Value::Empty)`). Used by the evaluator to decide whether a
51 /// reference should fall through to the resolver.
52 pub fn lookup(&self, name: &str) -> Option<Value> {
53 self.vars.get(&name.to_uppercase()).cloned()
54 }
55
56 /// Insert or overwrite a binding. Returns the previous value if one existed.
57 pub fn set(&mut self, name: String, value: Value) -> Option<Value> {
58 self.vars.insert(name.to_uppercase(), value)
59 }
60
61 /// Remove a binding. Used to restore context after lambda/let evaluation.
62 pub fn remove(&mut self, name: &str) {
63 self.vars.remove(&name.to_uppercase());
64 }
65
66 /// SplitMix64-based PRF: draw a value in [0,1) for the given draw index.
67 /// Uses the per-cell key when available; falls back to SystemTime for
68 /// bare Engine::evaluate calls (non-deterministic).
69 pub fn draw_rand(&self, draw_index: u32) -> f64 {
70 if let Some((seed, si, r, c)) = self.rng_cell {
71 let key = self.splitmix_chain(seed, si, r, c, draw_index);
72 // Use upper 32 bits for [0,1)
73 (key >> 32) as f64 / (u32::MAX as f64 + 1.0)
74 } else {
75 // Non-deterministic fallback for bare Engine::evaluate.
76 // Mix draw_index into the time seed so RANDARRAY cells differ.
77 let s = std::time::SystemTime::now()
78 .duration_since(std::time::UNIX_EPOCH)
79 .map(|d| d.as_nanos() as u64)
80 .unwrap_or(12345);
81 let key = Self::mix64(s ^ Self::mix64(draw_index as u64));
82 (key >> 32) as f64 / (u32::MAX as f64 + 1.0)
83 }
84 }
85
86 /// Draw `count` values in [0,1), each using its own draw_index.
87 pub fn draw_rand_n(&self, count: usize) -> Vec<f64> {
88 (0..count as u32).map(|i| self.draw_rand(i)).collect()
89 }
90
91 fn splitmix_chain(&self, seed: u64, si: u32, r: u32, c: u32, draw: u32) -> u64 {
92 let mut h = seed;
93 for part in [si as u64, r as u64, c as u64, draw as u64] {
94 h = Self::mix64(h ^ Self::mix64(part));
95 }
96 h
97 }
98
99 fn mix64(mut x: u64) -> u64 {
100 x = x.wrapping_add(0x9e3779b97f4a7c15);
101 x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
102 x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
103 x ^ (x >> 31)
104 }
105}
106
107#[cfg(test)]
108mod tests;