Skip to main content

geop_core_program/
numeric.rs

1//! [`NumericInput`]: a number an operation is given — a length, an angle, a
2//! count — typed into a dialog's number field as a plain value or as a
3//! formula of the program's parameters, which it follows ([`evaluate`]).
4//! An operation's arguments take one wherever they take a number. (How a
5//! dialog shows one, the editor says.)
6//!
7//! A step resolves it where it builds ([`NumericInput::resolve`]), reading each
8//! parameter the formula names — a target the program defines
9//! ([`parameter`]): the read is recorded, so a change to one of them
10//! rebuilds the step, a change to any other does not.
11
12use std::borrow::Cow;
13
14use geop_core_math::geop_error::{GeopError, GeopResult};
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    element::{ElementReference, HasElements, Kind, whole},
19    reader::Reader,
20    target::{Target, TargetReference},
21};
22
23/// What a parameter of the program comes to, as its target holds it
24/// ([`parameter`]): a number, or a text — the row of a table a part family
25/// is looked up in, by name.
26#[derive(Clone, Debug, PartialEq)]
27pub enum ParameterValue {
28    Number(f64),
29    Text(String),
30}
31
32/// Defined once, by the step that defines the parameter.
33impl Target for ParameterValue {}
34
35/// Read whole.
36impl Kind for ParameterValue {
37    const NAME: &'static str = "parameter";
38}
39
40impl HasElements<ParameterValue> for ParameterValue {
41    type Element<'a> = &'a ParameterValue;
42
43    fn element(&self, name: &str) -> GeopResult<&ParameterValue> {
44        whole(self, name)
45    }
46}
47
48/// The target of the parameter `name`: `width`, or `size.d` for the column
49/// `d` of the row a table looks up by the parameter `size`.
50pub fn parameter(name: &str) -> TargetReference<ParameterValue> {
51    TargetReference::new(format!("parameter:{name}"))
52}
53
54/// Every parameter of the part, by name, in the order the steps defining
55/// them define them, as each comes to: what lists or designates the part
56/// reads. A parameter that failed is left out.
57#[derive(Clone, Debug, Default, PartialEq)]
58pub struct ParameterValues(pub Vec<(String, ParameterValue)>);
59
60/// Each step defining parameters adds its own.
61impl Target for ParameterValues {
62    const CHAIN: bool = true;
63}
64
65/// Read whole.
66impl Kind for ParameterValues {
67    const NAME: &'static str = "parameter_values";
68}
69
70impl HasElements<ParameterValues> for ParameterValues {
71    type Element<'a> = &'a ParameterValues;
72    const SINGLETON: Option<&'static str> = Some("parameter_values");
73
74    fn element(&self, name: &str) -> GeopResult<&ParameterValues> {
75        whole(self, name)
76    }
77}
78
79/// The target of every parameter of the part, as they come to.
80pub fn parameter_values() -> TargetReference<ParameterValues> {
81    TargetReference::new("parameter_values")
82}
83
84/// Every parameter of the part `reader` reads, as they come to: none, for
85/// a part whose program defines none.
86pub fn read_parameter_values<'a>(reader: &Reader<'a>) -> GeopResult<Cow<'a, ParameterValues>> {
87    if !reader.defines(&parameter_values()) {
88        return Ok(Cow::Owned(ParameterValues::default()));
89    }
90    Ok(Cow::Borrowed(reader.retrieve_element(
91        &ElementReference::whole(parameter_values()),
92    )?))
93}
94
95/// The number `name` stands for in a formula, as `reader` reads it, the
96/// read recorded: a number parameter's value — `width`, or `size.d`, a
97/// column of the row a table looked up. None where no step defines that
98/// parameter, or it failed, or it is no number.
99pub fn read_parameter(reader: &Reader, name: &str) -> Option<f64> {
100    let reference = parameter(name);
101    if !reader.defines(&reference) {
102        return None;
103    }
104    match reader.retrieve_element(&ElementReference::whole(reference)) {
105        Ok(ParameterValue::Number(value)) => Some(*value),
106        _ => None,
107    }
108}
109
110/// A number an operation is given: a plain value, or a formula of the
111/// program's parameters that it follows. Saved as a number when plain, and
112/// as `{"formula": "width / 2"}` otherwise.
113#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
114#[serde(untagged)]
115pub enum NumericInput {
116    Plain(f64),
117    Formula { formula: String },
118}
119
120/// Zero, plain.
121impl Default for NumericInput {
122    fn default() -> Self {
123        NumericInput::Plain(0.0)
124    }
125}
126
127impl From<f64> for NumericInput {
128    fn from(value: f64) -> Self {
129        NumericInput::Plain(value)
130    }
131}
132
133impl From<&str> for NumericInput {
134    /// `text` as typed: plain if it is a number, a formula otherwise.
135    fn from(text: &str) -> Self {
136        let text = text.trim();
137        match text.parse() {
138            Ok(value) => NumericInput::Plain(value),
139            Err(_) => NumericInput::Formula {
140                formula: text.to_string(),
141            },
142        }
143    }
144}
145
146impl From<String> for NumericInput {
147    fn from(text: String) -> Self {
148        NumericInput::from(text.as_str())
149    }
150}
151
152impl std::fmt::Display for NumericInput {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self {
155            NumericInput::Plain(value) => write!(f, "{value}"),
156            NumericInput::Formula { formula } => f.write_str(formula),
157        }
158    }
159}
160
161impl NumericInput {
162    /// Its value if it is plain; `None` for a formula.
163    pub fn plain(&self) -> Option<f64> {
164        match self {
165            NumericInput::Plain(value) => Some(*value),
166            NumericInput::Formula { .. } => None,
167        }
168    }
169
170    /// Its value, each parameter it names read through `reader` (see
171    /// [`read_parameter`]): in a target's generator, every one of them
172    /// recorded read.
173    pub fn resolve(&self, reader: &Reader) -> GeopResult<f64> {
174        match self {
175            NumericInput::Plain(value) => Ok(*value),
176            NumericInput::Formula { formula } => {
177                evaluate(formula, |name| read_parameter(reader, name))
178            }
179        }
180    }
181}
182
183/// How long the number `text` starts with is: digits and points, then
184/// perhaps an exponent, `1e-3`.
185fn number_len(text: &str) -> usize {
186    let mut len = text
187        .find(|c: char| !(c.is_ascii_digit() || c == '.'))
188        .unwrap_or(text.len());
189    let rest = text.get(len..).unwrap_or("");
190    if let Some(exponent) = rest.strip_prefix(['e', 'E']) {
191        let digits = exponent.trim_start_matches(['+', '-']);
192        if digits.starts_with(|c: char| c.is_ascii_digit()) {
193            len += rest.len() - digits.len();
194            len += digits
195                .find(|c: char| !c.is_ascii_digit())
196                .unwrap_or(digits.len());
197        }
198    }
199    len
200}
201
202/// How long the name `text` starts with is: letters, digits, `_` and `.`.
203fn name_len(text: &str) -> usize {
204    text.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.'))
205        .unwrap_or(text.len())
206}
207
208/// The names of parameters `expression` reads, in order, each once.
209pub fn names_in(expression: &str) -> Vec<String> {
210    let mut names = Vec::new();
211    let _ = evaluate(expression, |name| {
212        if !names.iter().any(|n| n == name) {
213            names.push(name.to_string());
214        }
215        Some(1.0)
216    });
217    names
218}
219
220/// A function a formula can call: of one argument or of two.
221#[derive(Clone, Copy)]
222enum Function {
223    One(fn(f64) -> f64),
224    Two(fn(f64, f64) -> f64),
225}
226
227/// The functions a formula can call, by name. Angles are in degrees, as
228/// everywhere a designer types one.
229pub const FUNCTION_NAMES: [&str; 13] = [
230    "sqrt", "abs", "sin", "cos", "tan", "asin", "acos", "atan", "round", "floor", "ceil", "min",
231    "max",
232];
233
234fn function(name: &str) -> Option<Function> {
235    use Function::{One, Two};
236    Some(match name {
237        "sqrt" => One(f64::sqrt),
238        "abs" => One(f64::abs),
239        "sin" => One(|x| x.to_radians().sin()),
240        "cos" => One(|x| x.to_radians().cos()),
241        "tan" => One(|x| x.to_radians().tan()),
242        "asin" => One(|x| x.asin().to_degrees()),
243        "acos" => One(|x| x.acos().to_degrees()),
244        "atan" => One(|x| x.atan().to_degrees()),
245        "round" => One(f64::round),
246        "floor" => One(f64::floor),
247        "ceil" => One(f64::ceil),
248        "min" => Two(f64::min),
249        "max" => Two(f64::max),
250        _ => return None,
251    })
252}
253
254/// The value of the formula `expression`, with `lookup` the value of each
255/// parameter it names: numbers, `+ - * / ^`, parentheses, `pi`, and the
256/// functions of [`FUNCTION_NAMES`], with angles in degrees — and, last, one
257/// comparison of two of those, `< <= > >= == !=`, 1 where it holds and 0
258/// where not: what says whether a step runs, `length > 40`
259/// ([`crate::program::ProgramStep::active`]). Fails for anything else, for
260/// a name `lookup` does not know, and for a value that is no finite number.
261pub fn evaluate(expression: &str, lookup: impl FnMut(&str) -> Option<f64>) -> GeopResult<f64> {
262    let mut parser = Parser {
263        rest: expression,
264        lookup,
265    };
266    let ctx = |e: GeopError| e.with_context(format!("evaluating {expression:?}"));
267    let value = parser.comparison().map_err(ctx)?;
268    parser.skip_space();
269    if !parser.rest.is_empty() {
270        return Err(ctx(GeopError::new(format!("unexpected {:?}", parser.rest))));
271    }
272    if !value.is_finite() {
273        return Err(ctx(GeopError::new(format!("{value} is no finite number"))));
274    }
275    Ok(value)
276}
277
278/// A comparison a formula may end in: how it is spelled, and whether it
279/// holds of two numbers.
280type Comparison = (&'static str, fn(f64, f64) -> bool);
281
282/// A recursive-descent reader of a formula: what is left of it to read.
283struct Parser<'t, F> {
284    rest: &'t str,
285    lookup: F,
286}
287
288impl<'t, F: FnMut(&str) -> Option<f64>> Parser<'t, F> {
289    fn skip_space(&mut self) {
290        self.rest = self.rest.trim_start();
291    }
292
293    /// Takes the first `len` bytes of what is left.
294    fn take(&mut self, len: usize) -> &'t str {
295        let (taken, rest) = self.rest.split_at(len.min(self.rest.len()));
296        self.rest = rest;
297        taken
298    }
299
300    /// Takes `c` if it comes next.
301    fn eat(&mut self, c: char) -> bool {
302        self.skip_space();
303        match self.rest.strip_prefix(c) {
304            Some(rest) => {
305                self.rest = rest;
306                true
307            }
308            None => false,
309        }
310    }
311
312    /// A sum, or two compared: 1 where the comparison holds, 0 where not.
313    fn comparison(&mut self) -> GeopResult<f64> {
314        let a = self.sum()?;
315        self.skip_space();
316        let comparisons: [Comparison; 6] = [
317            ("<=", |a, b| a <= b),
318            (">=", |a, b| a >= b),
319            ("==", |a, b| a == b),
320            ("!=", |a, b| a != b),
321            ("<", |a, b| a < b),
322            (">", |a, b| a > b),
323        ];
324        let Some((op, holds)) = (comparisons.into_iter()).find(|(op, _)| self.rest.starts_with(op))
325        else {
326            return Ok(a);
327        };
328        self.take(op.len());
329        let b = self.sum()?;
330        Ok(f64::from(u8::from(holds(a, b))))
331    }
332
333    fn sum(&mut self) -> GeopResult<f64> {
334        let mut v = self.product()?;
335        loop {
336            if self.eat('+') {
337                v += self.product()?;
338            } else if self.eat('-') {
339                v -= self.product()?;
340            } else {
341                return Ok(v);
342            }
343        }
344    }
345
346    fn product(&mut self) -> GeopResult<f64> {
347        let mut v = self.unary()?;
348        loop {
349            if self.eat('*') {
350                v *= self.unary()?;
351            } else if self.eat('/') {
352                v /= self.unary()?;
353            } else {
354                return Ok(v);
355            }
356        }
357    }
358
359    fn unary(&mut self) -> GeopResult<f64> {
360        if self.eat('-') {
361            return Ok(-self.unary()?);
362        }
363        if self.eat('+') {
364            return self.unary();
365        }
366        let base = self.atom()?;
367        if self.eat('^') {
368            return Ok(base.powf(self.unary()?));
369        }
370        Ok(base)
371    }
372
373    fn atom(&mut self) -> GeopResult<f64> {
374        self.skip_space();
375        if self.eat('(') {
376            let v = self.comparison()?;
377            if !self.eat(')') {
378                return Err(GeopError::new("a ( is not closed"));
379            }
380            return Ok(v);
381        }
382        match self.rest.chars().next() {
383            Some(c) if c.is_ascii_digit() || c == '.' => {
384                let number = self.take(number_len(self.rest));
385                number
386                    .parse()
387                    .map_err(|_| GeopError::new(format!("{number:?} is no number")))
388            }
389            Some(c) if c.is_ascii_alphabetic() || c == '_' => {
390                let name = self.take(name_len(self.rest));
391                if self.eat('(') {
392                    return self.call(name);
393                }
394                if name == "pi" {
395                    return Ok(std::f64::consts::PI);
396                }
397                (self.lookup)(name)
398                    .ok_or_else(|| GeopError::new(format!("there is no parameter {name:?}")))
399            }
400            Some(c) => Err(GeopError::new(format!("unexpected {c:?}"))),
401            None => Err(GeopError::new("the formula ends too early")),
402        }
403    }
404
405    /// The function `name` called on the arguments that follow its `(`.
406    fn call(&mut self, name: &str) -> GeopResult<f64> {
407        let function = function(name)
408            .ok_or_else(|| GeopError::new(format!("there is no function {name:?}")))?;
409        let mut args = vec![self.sum()?];
410        while self.eat(',') {
411            args.push(self.sum()?);
412        }
413        if !self.eat(')') {
414            return Err(GeopError::new(format!("{name}( is not closed")));
415        }
416        match (function, args.as_slice()) {
417            (Function::One(f), [x]) => Ok(f(*x)),
418            (Function::Two(f), [x, y]) => Ok(f(*x, *y)),
419            (Function::One(_), _) => Err(GeopError::new(format!(
420                "{name} takes 1 argument, not {}",
421                args.len()
422            ))),
423            (Function::Two(_), _) => Err(GeopError::new(format!(
424                "{name} takes 2 arguments, not {}",
425                args.len()
426            ))),
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434    use crate::{operation_registry::OperationRegistry, part::Part, runner::Runner};
435
436    fn eval(text: &str) -> GeopResult<f64> {
437        evaluate(text, |name| match name {
438            "width" => Some(10.0),
439            "screw.d" => Some(5.0),
440            _ => None,
441        })
442    }
443
444    #[test]
445    fn formulas_evaluate() {
446        assert_eq!(eval("1 + 2 * 3").unwrap(), 7.0);
447        assert_eq!(eval("(1 + 2) * 3").unwrap(), 9.0);
448        assert_eq!(eval("-2 ^ 2").unwrap(), -4.0);
449        assert_eq!(eval("2 ^ 3 ^ 2").unwrap(), 512.0);
450        assert_eq!(eval("width / 2 - screw.d").unwrap(), 0.0);
451        assert_eq!(eval("max(width, 3) + min(1, 2)").unwrap(), 11.0);
452        assert!((eval("cos(60)").unwrap() - 0.5).abs() < 1e-12);
453        assert_eq!(eval("1.5e1").unwrap(), 15.0);
454        assert!((eval("2*pi").unwrap() - std::f64::consts::TAU).abs() < 1e-12);
455        for bad in [
456            "",
457            "1 +",
458            "(1",
459            "height",
460            "sqrt(1, 2)",
461            "min(1)",
462            "1 / 0",
463            "2 3",
464            "foo(1)",
465        ] {
466            assert!(eval(bad).is_err(), "{bad:?}");
467        }
468        assert_eq!(names_in("width * screw.d + width"), ["width", "screw.d"]);
469        // A comparison is 1 where it holds, 0 where not, after the sums.
470        assert_eq!(eval("width > 5").unwrap(), 1.0);
471        assert_eq!(eval("width + 1 <= 5 * 2").unwrap(), 0.0);
472        assert_eq!(eval("screw.d == 5").unwrap(), 1.0);
473        assert_eq!(eval("screw.d != 5").unwrap(), 0.0);
474        assert_eq!(eval("(width >= 10) + (width < 3)").unwrap(), 1.0);
475        for bad in ["1 <", "1 < 2 < 3", "1 = 2"] {
476            assert!(eval(bad).is_err(), "{bad:?}");
477        }
478    }
479
480    /// A formula typed is plain if it is a number, and saves as one; any
481    /// other saves as `{"formula": text}`. Both read back as they were.
482    #[test]
483    fn formulas_save_as_numbers_or_formulas() {
484        let plain = NumericInput::from(" 12.5 ");
485        let formula = NumericInput::from(" width / 2 ");
486        assert_eq!(plain, NumericInput::Plain(12.5));
487        assert_eq!(formula.to_string(), "width / 2");
488        assert_eq!(
489            serde_json::to_value(&plain).unwrap(),
490            serde_json::json!(12.5)
491        );
492        assert_eq!(
493            serde_json::to_value(&formula).unwrap(),
494            serde_json::json!({"formula": "width / 2"})
495        );
496        for f in [&plain, &formula] {
497            let back: NumericInput =
498                serde_json::from_value(serde_json::to_value(f).unwrap()).unwrap();
499            assert_eq!(&back, f);
500        }
501    }
502
503    /// A formula resolved in a target's generator reads its parameters as
504    /// inputs: a change to one of them rebuilds the target, a change to any
505    /// other does not.
506    #[test]
507    fn a_formula_reads_the_parameters_it_names() {
508        use std::sync::atomic::{AtomicUsize, Ordering};
509        static MADE: AtomicUsize = AtomicUsize::new(0);
510        let runner = Runner::new(OperationRegistry::default());
511        let mut part = Part::default();
512        let half = TargetReference::<f64>::new("half");
513        let formula = NumericInput::from("width / 2");
514        let run = |part: &mut Part, width: f64, other: f64| {
515            let given = |_: &Reader, &value: &f64| Ok(ParameterValue::Number(value));
516            (runner.run_step(part, "parameters", |ctx| {
517                ctx.produce(&parameter("width"), &width, given)?;
518                ctx.produce(&parameter("other"), &other, given)
519            }))
520            .unwrap();
521            (runner.run_step(part, "half", |ctx| {
522                ctx.produce(&half, &formula, |reader, f| {
523                    MADE.fetch_add(1, Ordering::Relaxed);
524                    f.resolve(reader)
525                })
526            }))
527            .unwrap();
528            MADE.load(Ordering::Relaxed)
529        };
530        assert_eq!(run(&mut part, 4.0, 0.0), 1);
531        assert_eq!(run(&mut part, 4.0, 1.0), 1, "it reads no other parameter");
532        assert_eq!(run(&mut part, 6.0, 1.0), 2);
533        assert_eq!(formula.resolve(&part.current()).unwrap(), 3.0);
534    }
535}