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 /// Per-cell RNG key for deterministic draws: (seed, sheet_index, row, col).
16 /// Set by the workbook recalc loop; None for bare Engine::evaluate calls.
17 pub rng_cell: Option<(u64, u32, u32, u32)>,
18}
19
20impl Context {
21 /// Create a `Context` from a map of variable name → value.
22 /// Keys are normalised to uppercase on insertion.
23 pub fn new(vars: HashMap<String, Value>) -> Self {
24 let normalized = vars.into_iter()
25 .map(|(k, v)| (k.to_uppercase(), v))
26 .collect();
27 Self { vars: normalized, now_serial: None, rng_cell: None }
28 }
29
30 /// Create an empty `Context` with no variable bindings.
31 pub fn empty() -> Self {
32 Self { vars: HashMap::new(), now_serial: None, rng_cell: None }
33 }
34
35 /// Look up a variable by name (case-insensitive). Returns `Value::Empty` if not found.
36 pub fn get(&self, name: &str) -> Value {
37 self.vars
38 .get(&name.to_uppercase())
39 .cloned()
40 .unwrap_or(Value::Empty)
41 }
42
43 /// Look up a binding by name (case-insensitive), distinguishing an absent
44 /// binding (`None`) from one explicitly bound to [`Value::Empty`]
45 /// (`Some(Value::Empty)`). Used by the evaluator to decide whether a
46 /// reference should fall through to the resolver.
47 pub fn lookup(&self, name: &str) -> Option<Value> {
48 self.vars.get(&name.to_uppercase()).cloned()
49 }
50
51 /// Insert or overwrite a binding. Returns the previous value if one existed.
52 pub fn set(&mut self, name: String, value: Value) -> Option<Value> {
53 self.vars.insert(name.to_uppercase(), value)
54 }
55
56 /// Remove a binding. Used to restore context after lambda/let evaluation.
57 pub fn remove(&mut self, name: &str) {
58 self.vars.remove(&name.to_uppercase());
59 }
60
61 /// SplitMix64-based PRF: draw a value in [0,1) for the given draw index.
62 /// Uses the per-cell key when available; falls back to SystemTime for
63 /// bare Engine::evaluate calls (non-deterministic).
64 pub fn draw_rand(&self, draw_index: u32) -> f64 {
65 if let Some((seed, si, r, c)) = self.rng_cell {
66 let key = self.splitmix_chain(seed, si, r, c, draw_index);
67 // Use upper 32 bits for [0,1)
68 (key >> 32) as f64 / (u32::MAX as f64 + 1.0)
69 } else {
70 // Non-deterministic fallback for bare Engine::evaluate.
71 // Mix draw_index into the time seed so RANDARRAY cells differ.
72 let s = std::time::SystemTime::now()
73 .duration_since(std::time::UNIX_EPOCH)
74 .map(|d| d.as_nanos() as u64)
75 .unwrap_or(12345);
76 let key = Self::mix64(s ^ Self::mix64(draw_index as u64));
77 (key >> 32) as f64 / (u32::MAX as f64 + 1.0)
78 }
79 }
80
81 /// Draw `count` values in [0,1), each using its own draw_index.
82 pub fn draw_rand_n(&self, count: usize) -> Vec<f64> {
83 (0..count as u32).map(|i| self.draw_rand(i)).collect()
84 }
85
86 fn splitmix_chain(&self, seed: u64, si: u32, r: u32, c: u32, draw: u32) -> u64 {
87 let mut h = seed;
88 for part in [si as u64, r as u64, c as u64, draw as u64] {
89 h = Self::mix64(h ^ Self::mix64(part));
90 }
91 h
92 }
93
94 fn mix64(mut x: u64) -> u64 {
95 x = x.wrapping_add(0x9e3779b97f4a7c15);
96 x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
97 x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
98 x ^ (x >> 31)
99 }
100}
101
102#[cfg(test)]
103mod tests;