rink_core/loader/
context.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use super::Registry;
6use crate::ast::{DatePattern, Expr, Query};
7use crate::output::{ConversionReply, Digits, NotFoundError, NumberParts, QueryError, QueryReply};
8use crate::types::{BaseUnit, BigInt, Dimensionality, Number, Numeric};
9use crate::{commands, Value};
10use chrono::{DateTime, Local, TimeZone};
11use std::collections::BTreeMap;
12
13/// The evaluation context that contains unit definitions.
14#[derive(Debug)]
15pub struct Context {
16    /// Contains all the information about units.
17    pub registry: Registry,
18    /// Used only during initialization.
19    pub(crate) temporaries: BTreeMap<String, Number>,
20    /// The current time, as set by the caller.
21    ///
22    /// This is used instead of directly asking the OS for the time
23    /// since it allows determinism in unit tests, and prevents edge
24    /// cases like `now - now` being non-zero.
25    pub now: DateTime<Local>,
26    /// Enables the use of chrono-humanize. It can be disabled for unit
27    /// tests, as well as in wasm builds where the time API panics.
28    pub use_humanize: bool,
29    /// Whether to save the previous query result and make it available
30    /// as the `ans` variable.
31    pub save_previous_result: bool,
32    /// The previous query result.
33    pub previous_result: Option<Number>,
34}
35
36impl Default for Context {
37    /// Equivalent to Context::new()
38    fn default() -> Self {
39        Context::new()
40    }
41}
42
43impl Context {
44    /// Creates a new, empty context
45    pub fn new() -> Context {
46        Context {
47            registry: Registry::default(),
48            temporaries: BTreeMap::new(),
49            now: Local.timestamp_opt(0, 0).unwrap(),
50            use_humanize: true,
51            save_previous_result: false,
52            previous_result: None,
53        }
54    }
55
56    pub fn set_time(&mut self, time: DateTime<Local>) {
57        self.now = time;
58    }
59
60    pub fn update_time(&mut self) {
61        self.now = Local::now();
62    }
63
64    pub fn load_dates(&mut self, mut dates: Vec<Vec<DatePattern>>) {
65        self.registry.datepatterns.append(&mut dates)
66    }
67
68    /// Given a unit name, returns its value if it exists. Supports SI
69    /// prefixes, plurals, bare dimensions like length, and quantities.
70    pub fn lookup(&self, name: &str) -> Option<Number> {
71        if name == "ans" || name == "ANS" || name == "_" {
72            return self.previous_result.clone();
73        }
74        if let Some(v) = self.temporaries.get(name).cloned() {
75            return Some(v);
76        }
77
78        self.registry.lookup(name)
79    }
80
81    /// Given a unit name, try to return a canonical name (expanding aliases and such)
82    pub fn canonicalize(&self, name: &str) -> Option<String> {
83        self.registry.canonicalize(name)
84    }
85
86    /// Describes a value's unit, gives true if the unit is reciprocal
87    /// (e.g. you should prefix "1.0 / " or replace "multiply" with
88    /// "divide" when rendering it).
89    pub fn describe_unit(&self, value: &Number) -> (bool, String) {
90        use std::io::Write;
91
92        let mut buf = vec![];
93        let mut recip = false;
94        let square = Number {
95            value: Numeric::one(),
96            unit: value.unit.clone(),
97        }
98        .root(2)
99        .ok();
100        let inverse = (&Number::one()
101            / &Number {
102                value: Numeric::one(),
103                unit: value.unit.clone(),
104            })
105            .unwrap();
106        if let Some(name) = self.registry.quantities.get(&value.unit) {
107            write!(buf, "{}", name).unwrap();
108        } else if let Some(name) =
109            square.and_then(|square| self.registry.quantities.get(&square.unit))
110        {
111            write!(buf, "{}^2", name).unwrap();
112        } else if let Some(name) = self.registry.quantities.get(&inverse.unit) {
113            recip = true;
114            write!(buf, "{}", name).unwrap();
115        } else {
116            let helper = |dim: &BaseUnit, pow: i64, buf: &mut Vec<u8>| {
117                let unit = Dimensionality::new_dim(dim.clone(), pow);
118                if let Some(name) = self.registry.quantities.get(&unit) {
119                    write!(buf, " {}", name).unwrap();
120                } else {
121                    let unit = Dimensionality::base_unit(dim.clone());
122                    if let Some(name) = self.registry.quantities.get(&unit) {
123                        write!(buf, " {}", name).unwrap();
124                    } else {
125                        write!(buf, " '{}'", dim).unwrap();
126                    }
127                    if pow != 1 {
128                        write!(buf, "^{}", pow).unwrap();
129                    }
130                }
131            };
132
133            let mut frac = vec![];
134            let mut found = false;
135            for (dim, &pow) in value.unit.iter() {
136                if pow < 0 {
137                    frac.push((dim, -pow));
138                } else {
139                    found = true;
140                    helper(dim, pow, &mut buf);
141                }
142            }
143            if !frac.is_empty() {
144                if !found {
145                    recip = true;
146                } else {
147                    write!(buf, " /").unwrap();
148                }
149                for (dim, pow) in frac {
150                    let unit = Dimensionality::new_dim(dim.clone(), pow);
151                    if let Some(name) = self.registry.quantities.get(&unit) {
152                        write!(buf, " {}", name).unwrap();
153                    } else {
154                        helper(dim, pow, &mut buf);
155                    }
156                }
157            }
158            buf.remove(0);
159        }
160
161        (recip, String::from_utf8(buf).unwrap())
162    }
163
164    pub fn typo_dym<'a>(&'a self, what: &str) -> Option<&'a str> {
165        commands::search_internal(self, what, 1).into_iter().next()
166    }
167
168    pub fn unknown_unit_err(&self, name: &str) -> NotFoundError {
169        NotFoundError {
170            got: name.to_owned(),
171            suggestion: self.typo_dym(name).map(|x| x.to_owned()),
172        }
173    }
174
175    pub fn humanize<Tz: chrono::TimeZone>(&self, date: chrono::DateTime<Tz>) -> Option<String> {
176        if self.use_humanize {
177            crate::parsing::datetime::humanize(self.now, date)
178        } else {
179            None
180        }
181    }
182
183    /// Takes a parsed definitions.units from
184    /// `gnu_units::parse()`. Returns a list of errors, if there were any.
185    pub fn load(&mut self, defs: crate::ast::Defs) -> Result<(), String> {
186        let errors = crate::loader::load_defs(self, defs);
187
188        if errors.is_empty() {
189            Ok(())
190        } else {
191            let mut lines = vec![format!("Multiple errors encountered while loading:")];
192            for error in errors {
193                lines.push(format!("  {error}"));
194            }
195            Err(lines.join("\n"))
196        }
197    }
198
199    /// Evaluates an expression to compute its value, *excluding* `->`
200    /// conversions.
201    pub fn eval(&self, expr: &Expr) -> Result<Value, QueryError> {
202        crate::runtime::eval_expr(self, expr)
203    }
204
205    #[deprecated(since = "0.7.0", note = "renamed to eval_query()")]
206    pub fn eval_outer(&self, query: &Query) -> Result<QueryReply, QueryError> {
207        self.eval_query(query)
208    }
209
210    /// Evaluates an expression, include `->` conversions.
211    pub fn eval_query(&self, query: &Query) -> Result<QueryReply, QueryError> {
212        crate::runtime::eval_query(self, query)
213    }
214
215    pub fn show(
216        &self,
217        raw: &Number,
218        bottom: &Number,
219        bottom_name: BTreeMap<String, isize>,
220        bottom_const: Numeric,
221        base: u8,
222        digits: Digits,
223    ) -> ConversionReply {
224        let (exact, approx) = raw.numeric_value(base, digits);
225        let bottom_name = bottom_name
226            .into_iter()
227            .map(|(a, b)| (BaseUnit::new(&*a), b as i64))
228            .collect();
229        let (num, den) = bottom_const.to_rational();
230        ConversionReply {
231            value: NumberParts {
232                raw_value: Some(raw.clone()),
233                exact_value: exact,
234                approx_value: approx,
235                factor: if num != BigInt::one() {
236                    Some(num.to_string())
237                } else {
238                    None
239                },
240                divfactor: if den != BigInt::one() {
241                    Some(den.to_string())
242                } else {
243                    None
244                },
245                unit: Some(Number::unit_to_string(&bottom_name)),
246                raw_unit: Some(bottom_name),
247                ..bottom.to_parts(self)
248            },
249        }
250    }
251}