ltrait_gen_calc/
lib.rs

1//! # Example Usage
2//! ```rust
3//! # use ltrait::{color_eyre::Result, Launcher};
4//!
5//! # struct DummyUI;
6//! #
7//! # impl<'a> ltrait::UI<'a> for DummyUI {
8//! #     type Context = ();
9//! #
10//! #     async fn run<Cushion: 'a + Send>(
11//! #         &self,
12//! #         _: ltrait::launcher::batcher::Batcher<'a, Cushion, Self::Context>,
13//! #     ) -> Result<Option<Cushion>> {
14//! #         unimplemented!()
15//! #     }
16//! # }
17//! #
18//! # fn main() -> Result<()> {
19//! #
20//! use ltrait_gen_calc::{Calc, CalcConfig};
21//!
22//! let launcher = Launcher::default()
23//!     .set_ui(DummyUI, |c| unimplemented!())
24//!     .add_raw_generator(
25//!         Calc::new(CalcConfig::new(
26//!             (Some('k'), None), // default evaltor is is numbat. if prefix is k, use kalk
27//!             None,
28//!             None, // kalk precision, use default(53)
29//!             None,
30//!         )),
31//!     );
32//! #
33//! # Ok(()) }
34//! ```
35
36use ltrait::{async_trait::async_trait, generator::Generator};
37use std::fs::File;
38use std::io::Read;
39use std::path::PathBuf;
40
41pub struct CalcConfig {
42    /// calc accepts inputs of the following form as input to calc. Other inputs are ignored.
43    /// ={{prefix}} {{formula}}
44    ///
45    /// Some problems (e.g. finding solutions to nth order equations) cannot be solved
46    /// with numbat or converted in units with kalk, so they can be branched with prefixes.
47    ///
48    /// It is not possible to set both to `None`.
49    ///
50    /// (kalk_prefix, numbat_prefix)
51    ///
52    /// If `(Some('k'), None)` and you type `=k `, the formula that is continued is evaluated with
53    /// kalk.
54    prefix: (Option<char>, Option<char>),
55    kalk_init_path: Option<PathBuf>,
56    /// The precision passed to kalk. the default is 53.
57    kalk_precision: Option<u32>,
58    numbat_init_path: Option<PathBuf>,
59}
60
61impl CalcConfig {
62    pub fn new(
63        prefix: (Option<char>, Option<char>),
64        kalk_init_path: Option<PathBuf>,
65        kalk_precision: Option<u32>,
66        numbat_init_path: Option<PathBuf>,
67    ) -> Self {
68        if prefix.0.is_none() && prefix.1.is_none() {
69            panic!("It is not possible to set both to None.")
70        }
71        Self {
72            prefix,
73            kalk_init_path,
74            kalk_precision,
75            numbat_init_path,
76        }
77    }
78}
79
80pub struct Calc {
81    config: CalcConfig,
82}
83
84#[derive(Debug, PartialEq, Eq)]
85enum Type {
86    Numbat,
87    Kalk,
88}
89
90fn parse(
91    input: &str,
92    (kalk_prefix, numbat_prefix): (Option<char>, Option<char>),
93) -> Option<(Type, &str)> {
94    if input.starts_with(&format!(
95        "={} ",
96        numbat_prefix.map(|s| format!("{s}")).unwrap_or_default()
97    )) {
98        Some((
99            Type::Numbat,
100            if numbat_prefix.is_some() {
101                &input[3..]
102            } else {
103                &input[2..]
104            },
105        ))
106    } else if input.starts_with(&format!(
107        "={} ",
108        kalk_prefix.map(|s| format!("{s}")).unwrap_or_default()
109    )) {
110        Some((
111            Type::Kalk,
112            if kalk_prefix.is_some() {
113                &input[3..]
114            } else {
115                &input[2..]
116            },
117        ))
118    } else {
119        None
120    }
121}
122
123impl Calc {
124    pub fn new(config: CalcConfig) -> Self {
125        Self { config }
126    }
127
128    fn numbat(&self, input: &str) -> Result<String, Box<dyn std::error::Error>> {
129        use numbat::{module_importer::BuiltinModuleImporter, resolver::CodeSource, Context};
130
131        let mut ctx = Context::new(BuiltinModuleImporter::default());
132
133        let _ = ctx.interpret("use prelude", CodeSource::Internal)?;
134
135        if let Some(ref config_path) = self.config.numbat_init_path {
136            let config_content = std::fs::read_to_string(config_path)?;
137
138            let _ = ctx.interpret(&config_content, CodeSource::File(config_path.to_path_buf()))?;
139        }
140
141        match ctx.interpret(input, CodeSource::Text) {
142            Ok((statements, results)) => {
143                let result_markup =
144                    results.to_markup(statements.last(), ctx.dimension_registry(), true, true);
145                let output = numbat::markup::plain_text_format(&result_markup, false).to_string();
146                let output = output.trim().trim_end_matches('\n');
147
148                Ok(output.into())
149            }
150            Err(e) => Err(format!("{e}").into()),
151        }
152    }
153
154    fn kalk(&self, input: &str) -> Result<String, Box<dyn std::error::Error>> {
155        use kalk::parser;
156        let mut parser_context = parser::Context::new();
157
158        let precision = self.config.kalk_precision.unwrap_or(53);
159
160        if let Some(ref path) = self.config.kalk_init_path {
161            let mut file_content = String::new();
162            File::open(path)?.read_to_string(&mut file_content)?;
163
164            parser::eval(&mut parser_context, &file_content, precision)
165                .map_err(|e| e.to_string())?;
166        }
167
168        let result = parser::eval(&mut parser_context, input, precision)
169            .map_err(|e| e.to_string())?
170            .ok_or("The Result is nothing")?;
171
172        Ok(result.to_string_pretty())
173    }
174}
175
176#[async_trait]
177impl Generator for Calc {
178    type Item = String;
179
180    async fn generate(&self, input: &str) -> Vec<Self::Item> {
181        match parse(input, self.config.prefix) {
182            Some((engine_type, expr)) => match engine_type {
183                Type::Kalk => self.kalk(expr).map(|res| vec![res]).unwrap_or_default(),
184                Type::Numbat => self.numbat(expr).map(|res| vec![res]).unwrap_or_default(),
185            },
186            _ => Vec::new(),
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use crate::Calc;
194
195    use super::{parse, Type};
196
197    #[test]
198    fn test_parse() -> Result<(), Box<dyn std::error::Error>> {
199        let prefix = (Some('k'), None);
200
201        assert_eq!(parse("=k 1 + 1", prefix), Some((Type::Kalk, "1 + 1")));
202        assert_eq!(parse("= 1 + 1", prefix), Some((Type::Numbat, "1 + 1")),);
203
204        let prefix = (None, Some('n'));
205
206        assert_eq!(parse("= 1 + 1", prefix), Some((Type::Kalk, "1 + 1")),);
207        assert_eq!(parse("= 2 + 1", prefix), Some((Type::Kalk, "2 + 1")),);
208        assert_eq!(parse("=n 1 + 1", prefix), Some((Type::Numbat, "1 + 1")),);
209
210        let prefix = (Some('k'), Some('n'));
211        assert_eq!(parse("=k 1 + 1", prefix), Some((Type::Kalk, "1 + 1")),);
212        assert_eq!(parse("=k 2 + 1", prefix), Some((Type::Kalk, "2 + 1")),);
213        assert_eq!(parse("=n 1 + 1", prefix), Some((Type::Numbat, "1 + 1")),);
214
215        assert_eq!(parse("Hello World!", prefix), None);
216
217        Ok(())
218    }
219
220    #[test]
221    fn test_calc() -> Result<(), Box<dyn std::error::Error>> {
222        let calc = Calc::new(crate::CalcConfig {
223            prefix: (Some('k'), None),
224            kalk_init_path: None,
225            kalk_precision: None,
226            numbat_init_path: None,
227        });
228
229        assert_eq!(&calc.kalk("1 + 1")?, "= 2");
230        assert_eq!(&calc.kalk("x^2 = 4")?, "x ≈ 2");
231        assert_eq!(&calc.numbat("1 + 1")?, "= 2");
232
233        Ok(())
234    }
235}