gravel_provider_calculator/
lib.rs1use abi_stable::reexports::SelfOps;
9use gravel_ffi::prelude::*;
10use mexprp::Answer;
11use serde::Deserialize;
12
13const DEFAULT_CONFIG: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/config.yml"));
14
15struct CalculatorProvider {
16 config: Config,
17}
18
19#[gravel_provider("calculator")]
20impl Provider for CalculatorProvider {
21 fn new(config: &PluginConfigAdapter<'_>) -> Self {
22 Self {
23 config: config.get(DEFAULT_CONFIG),
24 }
25 }
26
27 fn query(&self, query: &str) -> ProviderResult {
28 let query = query.trim();
29
30 eval(query)
31 .filter(|r| !query_was_const(query, r))
32 .map(|r| self.get_hit(r))
33 .piped(ProviderResult::from_option)
34 }
35}
36
37impl CalculatorProvider {
38 fn get_hit(&self, result: String) -> SimpleHit {
39 SimpleHit::new(result, self.config.subtitle.clone(), move |hit, ctx| {
40 ctx.set_clipboard_text(hit.title().to_string().into_c());
41 ctx.hide_frontend();
42 })
43 .with_secondary(|hit, ctx| {
44 ctx.set_query(hit.title().as_str().to_owned().into_c());
45 })
46 .with_score(MAX_SCORE)
47 }
48}
49
50fn query_was_const(query: &str, result: &str) -> bool {
52 query == result || matches!(query, "e" | "pi" | "i")
53}
54
55fn eval(expression: &str) -> Option<String> {
56 match mexprp::eval(expression) {
57 Ok(Answer::Single(result)) => Some(result),
58 Ok(Answer::Multiple(results)) => results.into_iter().next(),
59 _ => None,
60 }
61 .map(|r| round(r, 10).to_string())
62}
63
64fn round(number: f64, precision: u32) -> f64 {
65 let factor = 10_u64.pow(precision) as f64;
66 (number * factor).round() / factor
67}
68
69#[derive(Deserialize, Debug)]
70struct Config {
71 pub subtitle: String,
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use rstest::rstest;
78
79 #[rstest]
80 #[case("1", "1")]
81 #[case("1 + 1", "2")]
82 #[case("1 - 1", "0")]
83 #[case("1 / 1", "1")]
84 #[case("1 * 1", "1")]
85 #[case("3 * 0.2", "0.6")]
86 #[case("1 / 20", "0.05")]
87 #[case("2 ^ 10", "1024")]
88 #[case("0.1 + 0.2", "0.3")]
89 #[case("(2 + 3) * (3 - 5)", "-10")]
90 #[case("-2 ^ 3", "-8")]
91 #[case("round(2pi)", "6")]
92 #[case("sqrt(2)", "1.4142135624")]
93 #[case("sin(asin(0.5))", "0.5")]
94 fn should_eval(#[case] expression: &str, #[case] expected: &str) {
95 let actual = eval(expression);
96 assert_eq!(Some(expected), actual.as_deref(), "{expression}");
97 }
98
99 #[rstest]
100 #[case("clippy")]
101 #[case("1 1")]
102 #[case("1 / 0")]
103 #[case("x + 5")]
104 fn should_fail(#[case] expression: &str) {
105 let actual = eval(expression);
106 assert_eq!(None, actual, "{expression}");
107 }
108}