1use ltrait::{async_trait::async_trait, generator::Generator};
37use std::fs::File;
38use std::io::Read;
39use std::path::PathBuf;
40
41pub struct CalcConfig {
42 prefix: (Option<char>, Option<char>),
55 kalk_init_path: Option<PathBuf>,
56 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}