1use 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#[derive(Clone, Debug, PartialEq)]
27pub enum ParameterValue {
28 Number(f64),
29 Text(String),
30}
31
32impl Target for ParameterValue {}
34
35impl 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
48pub fn parameter(name: &str) -> TargetReference<ParameterValue> {
51 TargetReference::new(format!("parameter:{name}"))
52}
53
54#[derive(Clone, Debug, Default, PartialEq)]
58pub struct ParameterValues(pub Vec<(String, ParameterValue)>);
59
60impl Target for ParameterValues {
62 const CHAIN: bool = true;
63}
64
65impl 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
79pub fn parameter_values() -> TargetReference<ParameterValues> {
81 TargetReference::new("parameter_values")
82}
83
84pub fn read_parameter_values<'a>(reader: &Reader<'a>) -> GeopResult<Cow<'a, ParameterValues>> {
87 if !reader.defines(¶meter_values()) {
88 return Ok(Cow::Owned(ParameterValues::default()));
89 }
90 Ok(Cow::Borrowed(reader.retrieve_element(
91 &ElementReference::whole(parameter_values()),
92 )?))
93}
94
95pub 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
114#[serde(untagged)]
115pub enum NumericInput {
116 Plain(f64),
117 Formula { formula: String },
118}
119
120impl 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 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 pub fn plain(&self) -> Option<f64> {
164 match self {
165 NumericInput::Plain(value) => Some(*value),
166 NumericInput::Formula { .. } => None,
167 }
168 }
169
170 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
183fn 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
202fn name_len(text: &str) -> usize {
204 text.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '.'))
205 .unwrap_or(text.len())
206}
207
208pub 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#[derive(Clone, Copy)]
222enum Function {
223 One(fn(f64) -> f64),
224 Two(fn(f64, f64) -> f64),
225}
226
227pub 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
254pub 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
278type Comparison = (&'static str, fn(f64, f64) -> bool);
281
282struct 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 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 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 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 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 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 #[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 #[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(¶meter("width"), &width, given)?;
518 ctx.produce(¶meter("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}