Skip to main content

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;