Skip to main content

kcl_lib/std/
math.rs

1//! Functions related to mathematics.
2
3use anyhow::Result;
4
5use crate::{
6    CompilationError,
7    errors::{KclError, KclErrorDetails},
8    execution::{
9        ExecState, KclValue, annotations,
10        types::{ArrayLen, NumericType, RuntimeType},
11    },
12    std::args::{Args, TyF64},
13};
14
15/// Compute the remainder after dividing `num` by `div`.
16/// If `num` is negative, the result will be too.
17pub async fn rem(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
18    let n: TyF64 = args.get_unlabeled_kw_arg("number to divide", &RuntimeType::num_any(), exec_state)?;
19    let d: TyF64 = args.get_kw_arg("divisor", &RuntimeType::num_any(), exec_state)?;
20    let valid_d = d.n != 0.0;
21    if !valid_d {
22        exec_state.warn(
23            CompilationError::err(args.source_range, "Divisor cannot be 0".to_string()),
24            annotations::WARN_INVALID_MATH,
25        );
26    }
27
28    let (n, d, ty) = NumericType::combine_mod(n, d);
29    if ty == NumericType::Unknown {
30        exec_state.err(CompilationError::err(
31            args.source_range,
32            "Calling `rem` on numbers which have unknown or incompatible units.\n\nYou may need to add information about the type of the argument, for example:\n  using a numeric suffix: `42{ty}`\n  or using type ascription: `foo(): number({ty})`"
33        ));
34    }
35    let remainder = n % d;
36
37    Ok(args.make_user_val_from_f64_with_type(TyF64::new(remainder, ty)))
38}
39
40/// Compute the cosine of a number (in radians).
41pub async fn cos(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
42    let num: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::angle(), exec_state)?;
43    let num = num.to_radians(exec_state, args.source_range);
44    Ok(args.make_user_val_from_f64_with_type(TyF64::new(libm::cos(num), exec_state.current_default_units())))
45}
46
47/// Compute the sine of a number (in radians).
48pub async fn sin(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
49    let num: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::angle(), exec_state)?;
50    let num = num.to_radians(exec_state, args.source_range);
51    Ok(args.make_user_val_from_f64_with_type(TyF64::new(libm::sin(num), exec_state.current_default_units())))
52}
53
54/// Compute the tangent of a number (in radians).
55pub async fn tan(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
56    let num: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::angle(), exec_state)?;
57    let num = num.to_radians(exec_state, args.source_range);
58    Ok(args.make_user_val_from_f64_with_type(TyF64::new(libm::tan(num), exec_state.current_default_units())))
59}
60
61/// Compute the square root of a number.
62pub async fn sqrt(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
63    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
64
65    if input.n < 0.0 {
66        return Err(KclError::new_semantic(KclErrorDetails::new(
67            format!(
68                "Attempt to take square root (`sqrt`) of a number less than zero ({})",
69                input.n
70            ),
71            vec![args.source_range],
72        )));
73    }
74
75    let result = input.n.sqrt();
76
77    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
78}
79
80/// Compute the absolute value of a number.
81pub async fn abs(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
82    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
83    let result = input.n.abs();
84
85    Ok(args.make_user_val_from_f64_with_type(input.map_value(result)))
86}
87
88/// Round a number to the nearest integer.
89pub async fn round(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
90    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
91    let result = input.n.round();
92
93    Ok(args.make_user_val_from_f64_with_type(input.map_value(result)))
94}
95
96/// Compute the largest integer less than or equal to a number.
97pub async fn floor(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
98    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
99    let result = input.n.floor();
100
101    Ok(args.make_user_val_from_f64_with_type(input.map_value(result)))
102}
103
104/// Compute the smallest integer greater than or equal to a number.
105pub async fn ceil(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
106    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
107    let result = input.n.ceil();
108
109    Ok(args.make_user_val_from_f64_with_type(input.map_value(result)))
110}
111
112/// Compute the minimum of the given arguments.
113pub async fn min(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
114    let nums: Vec<TyF64> = args.get_unlabeled_kw_arg(
115        "input",
116        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Minimum(1)),
117        exec_state,
118    )?;
119    let (nums, ty) = NumericType::combine_eq_array(&nums);
120    if ty == NumericType::Unknown {
121        exec_state.warn(CompilationError::err(
122            args.source_range,
123            "Calling `min` on numbers which have unknown or incompatible units.\n\nYou may need to add information about the type of the argument, for example:\n  using a numeric suffix: `42{ty}`\n  or using type ascription: `foo(): number({ty})`",
124        ), annotations::WARN_UNKNOWN_UNITS);
125    }
126
127    let mut result = f64::MAX;
128    for num in nums {
129        if num < result {
130            result = num;
131        }
132    }
133
134    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, ty)))
135}
136
137/// Compute the maximum of the given arguments.
138pub async fn max(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
139    let nums: Vec<TyF64> = args.get_unlabeled_kw_arg(
140        "input",
141        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Minimum(1)),
142        exec_state,
143    )?;
144    let (nums, ty) = NumericType::combine_eq_array(&nums);
145    if ty == NumericType::Unknown {
146        exec_state.warn(CompilationError::err(
147            args.source_range,
148            "Calling `max` on numbers which have unknown or incompatible units.\n\nYou may need to add information about the type of the argument, for example:\n  using a numeric suffix: `42{ty}`\n  or using type ascription: `foo(): number({ty})`",
149        ), annotations::WARN_UNKNOWN_UNITS);
150    }
151
152    let mut result = f64::MIN;
153    for num in nums {
154        if num > result {
155            result = num;
156        }
157    }
158
159    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, ty)))
160}
161
162/// Compute the number to a power.
163pub async fn pow(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
164    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
165    let exp: TyF64 = args.get_kw_arg("exp", &RuntimeType::count(), exec_state)?;
166    let exp_is_int = exp.n.fract() == 0.0;
167    if input.n < 0.0 && !exp_is_int {
168        exec_state.warn(
169            CompilationError::err(
170                args.source_range,
171                format!(
172                    "Exponent must be an integer when input is negative, but it was {}",
173                    exp.n
174                ),
175            ),
176            annotations::WARN_INVALID_MATH,
177        );
178    }
179    let valid_input = !(input.n == 0.0 && exp.n < 0.0);
180    if !valid_input {
181        exec_state.warn(
182            CompilationError::err(args.source_range, "Input cannot be 0 when exp < 0".to_string()),
183            annotations::WARN_INVALID_MATH,
184        );
185    }
186    let result = input.n.powf(exp.n);
187
188    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
189}
190
191/// Compute the arccosine of a number (in radians).
192pub async fn acos(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
193    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::count(), exec_state)?;
194    let in_range = (-1.0..=1.0).contains(&input.n);
195    if !in_range {
196        exec_state.warn(
197            CompilationError::err(
198                args.source_range,
199                format!("The argument must be between -1 and 1, but it was {}", input.n),
200            ),
201            annotations::WARN_INVALID_MATH,
202        );
203    }
204    let result = libm::acos(input.n);
205
206    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::radians())))
207}
208
209/// Compute the arcsine of a number (in radians).
210pub async fn asin(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
211    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::count(), exec_state)?;
212    let in_range = (-1.0..=1.0).contains(&input.n);
213    if !in_range {
214        exec_state.warn(
215            CompilationError::err(
216                args.source_range,
217                format!("The argument must be between -1 and 1, but it was {}", input.n),
218            ),
219            annotations::WARN_INVALID_MATH,
220        );
221    }
222    let result = libm::asin(input.n);
223
224    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::radians())))
225}
226
227/// Compute the arctangent of a number (in radians).
228pub async fn atan(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
229    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::count(), exec_state)?;
230    let result = libm::atan(input.n);
231
232    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::radians())))
233}
234
235/// Compute the four quadrant arctangent of Y and X (in radians).
236pub async fn atan2(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
237    let y = args.get_kw_arg("y", &RuntimeType::length(), exec_state)?;
238    let x = args.get_kw_arg("x", &RuntimeType::length(), exec_state)?;
239    let (y, x, _) = NumericType::combine_eq_coerce(y, x, Some((exec_state, args.source_range)));
240    let result = libm::atan2(y, x);
241
242    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::radians())))
243}
244
245/// Compute the logarithm of the number with respect to an arbitrary base.
246///
247/// The result might not be correctly rounded owing to implementation
248/// details; `log2()` can produce more accurate results for base 2,
249/// and `log10()` can produce more accurate results for base 10.
250pub async fn log(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
251    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
252    let base: TyF64 = args.get_kw_arg("base", &RuntimeType::count(), exec_state)?;
253    let valid_input = input.n > 0.0;
254    if !valid_input {
255        exec_state.warn(
256            CompilationError::err(args.source_range, format!("Input must be > 0, but it was {}", input.n)),
257            annotations::WARN_INVALID_MATH,
258        );
259    }
260    let valid_base = base.n > 0.0;
261    if !valid_base {
262        exec_state.warn(
263            CompilationError::err(args.source_range, format!("Base must be > 0, but it was {}", base.n)),
264            annotations::WARN_INVALID_MATH,
265        );
266    }
267    let base_not_1 = base.n != 1.0;
268    if !base_not_1 {
269        exec_state.warn(
270            CompilationError::err(args.source_range, "Base cannot be 1".to_string()),
271            annotations::WARN_INVALID_MATH,
272        );
273    }
274    let result = input.n.log(base.n);
275
276    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
277}
278
279/// Compute the base 2 logarithm of the number.
280pub async fn log2(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
281    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
282    let valid_input = input.n > 0.0;
283    if !valid_input {
284        exec_state.warn(
285            CompilationError::err(args.source_range, format!("Input must be > 0, but it was {}", input.n)),
286            annotations::WARN_INVALID_MATH,
287        );
288    }
289    let result = input.n.log2();
290
291    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
292}
293
294/// Compute the base 10 logarithm of the number.
295pub async fn log10(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
296    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
297    let valid_input = input.n > 0.0;
298    if !valid_input {
299        exec_state.warn(
300            CompilationError::err(args.source_range, format!("Input must be > 0, but it was {}", input.n)),
301            annotations::WARN_INVALID_MATH,
302        );
303    }
304    let result = input.n.log10();
305
306    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
307}
308
309/// Compute the natural logarithm of the number.
310pub async fn ln(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
311    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
312    let valid_input = input.n > 0.0;
313    if !valid_input {
314        exec_state.warn(
315            CompilationError::err(args.source_range, format!("Input must be > 0, but it was {}", input.n)),
316            annotations::WARN_INVALID_MATH,
317        );
318    }
319    let result = input.n.ln();
320
321    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
322}
323
324/// Compute the length of the given leg.
325pub async fn leg_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
326    let hypotenuse: TyF64 = args.get_kw_arg("hypotenuse", &RuntimeType::length(), exec_state)?;
327    let leg: TyF64 = args.get_kw_arg("leg", &RuntimeType::length(), exec_state)?;
328    let (hypotenuse, leg, ty) = NumericType::combine_eq_coerce(hypotenuse, leg, Some((exec_state, args.source_range)));
329    let result = (hypotenuse.powi(2) - f64::min(hypotenuse.abs(), leg.abs()).powi(2)).sqrt();
330    Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
331}
332
333/// Compute the angle of the given leg for x.
334pub async fn leg_angle_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
335    let hypotenuse: TyF64 = args.get_kw_arg("hypotenuse", &RuntimeType::length(), exec_state)?;
336    let leg: TyF64 = args.get_kw_arg("leg", &RuntimeType::length(), exec_state)?;
337    let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg, Some((exec_state, args.source_range)));
338    let valid_hypotenuse = hypotenuse > 0.0;
339    if !valid_hypotenuse {
340        exec_state.warn(
341            CompilationError::err(
342                args.source_range,
343                format!("Hypotenuse must be > 0, but it was {}", hypotenuse),
344            ),
345            annotations::WARN_INVALID_MATH,
346        );
347    }
348    let ratio = leg.min(hypotenuse) / hypotenuse;
349    let in_range = (-1.0..=1.0).contains(&ratio);
350    if !in_range {
351        exec_state.warn(
352            CompilationError::err(
353                args.source_range,
354                format!("The argument must be between -1 and 1, but it was {}", ratio),
355            ),
356            annotations::WARN_INVALID_MATH,
357        );
358    }
359    let result = libm::acos(ratio).to_degrees();
360    Ok(KclValue::from_number_with_type(
361        result,
362        NumericType::degrees(),
363        vec![args.into()],
364    ))
365}
366
367/// Compute the angle of the given leg for y.
368pub async fn leg_angle_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
369    let hypotenuse: TyF64 = args.get_kw_arg("hypotenuse", &RuntimeType::length(), exec_state)?;
370    let leg: TyF64 = args.get_kw_arg("leg", &RuntimeType::length(), exec_state)?;
371    let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg, Some((exec_state, args.source_range)));
372    let valid_hypotenuse = hypotenuse > 0.0;
373    if !valid_hypotenuse {
374        exec_state.warn(
375            CompilationError::err(
376                args.source_range,
377                format!("Hypotenuse must be > 0, but it was {}", hypotenuse),
378            ),
379            annotations::WARN_INVALID_MATH,
380        );
381    }
382    let ratio = leg.min(hypotenuse) / hypotenuse;
383    let in_range = (-1.0..=1.0).contains(&ratio);
384    if !in_range {
385        exec_state.warn(
386            CompilationError::err(
387                args.source_range,
388                format!("The argument must be between -1 and 1, but it was {}", ratio),
389            ),
390            annotations::WARN_INVALID_MATH,
391        );
392    }
393    let result = libm::asin(ratio).to_degrees();
394    Ok(KclValue::from_number_with_type(
395        result,
396        NumericType::degrees(),
397        vec![args.into()],
398    ))
399}