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    /// 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;