smartcalc_tauri/
smartcalc.rs

1/*
2 * smartcalc v1.0.8
3 * Copyright (c) Erhan BARIS (Ruslan Ognyanov Asenov)
4 * Licensed under the GNU General Public License v2.0.
5 */
6
7use crate::tokinizer::{read_currency, small_date, RuleType};
8use crate::{Session, TimeOffset};
9use alloc::collections::BTreeMap;
10use alloc::rc::Rc;
11use alloc::string::{String, ToString};
12use alloc::vec;
13use alloc::vec::Vec;
14use anyhow::anyhow;
15use core::borrow::Borrow;
16use core::ops::Deref;
17
18use crate::compiler::Interpreter;
19use crate::config::{DynamicType, SmartCalcConfig};
20use crate::formatter::format_result;
21use crate::logger::{initialize_logger, LOGGER};
22use crate::syntax::SyntaxParser;
23use crate::token::ui_token::UiToken;
24use crate::tokinizer::TokenInfo;
25use crate::tokinizer::Tokinizer;
26use crate::tools::parse_timezone;
27use crate::types::SmartCalcAstType;
28use crate::types::{ExpressionFunc, TokenType};
29
30pub type ExecutionLine = Option<ExecuteLine>;
31
32pub trait RuleTrait {
33    fn name(&self) -> String;
34    fn call(
35        &self,
36        smartcalc: &SmartCalcConfig,
37        fields: &BTreeMap<String, TokenType>,
38    ) -> Option<TokenType>;
39}
40
41#[derive(Debug, Default)]
42pub struct ExecuteResult {
43    pub status: bool,
44    pub lines: Vec<ExecutionLine>,
45}
46
47#[derive(Debug, Clone)]
48pub struct ExecuteLineResult {
49    pub output: String,
50    pub ast: Rc<SmartCalcAstType>,
51}
52
53impl ExecuteLineResult {
54    pub fn new(output: String, ast: Rc<SmartCalcAstType>) -> Self {
55        ExecuteLineResult { output, ast }
56    }
57}
58
59#[derive(Debug)]
60pub struct ExecuteLine {
61    pub result: Result<ExecuteLineResult, String>,
62    pub raw_tokens: Vec<Rc<TokenType>>,
63    pub ui_tokens: Vec<UiToken>,
64    pub calculated_tokens: Vec<Rc<TokenInfo>>,
65}
66
67impl ExecuteLine {
68    pub fn new(
69        result: Result<ExecuteLineResult, String>,
70        ui_tokens: Vec<UiToken>,
71        raw_tokens: Vec<Rc<TokenType>>,
72        calculated_tokens: Vec<Rc<TokenInfo>>,
73    ) -> Self {
74        ExecuteLine {
75            result,
76            ui_tokens,
77            raw_tokens,
78            calculated_tokens,
79        }
80    }
81}
82
83pub struct SmartCalc {
84    config: SmartCalcConfig,
85}
86
87unsafe impl Send for SmartCalc {}
88
89impl Default for SmartCalc {
90    fn default() -> Self {
91        initialize_logger();
92        let mut smartcalc = SmartCalc {
93            config: SmartCalcConfig::default(),
94        };
95        smartcalc.set_date_rule(
96            "en",
97            vec![
98                "{MONTH:month} {NUMBER:day}, {NUMBER:year}".to_string(),
99                "{MONTH:month} {NUMBER:day} {NUMBER:year}".to_string(),
100                "{NUMBER:day}/{NUMBER:month}/{NUMBER:year}".to_string(),
101                "{NUMBER:day} {MONTH:month} {NUMBER:year}".to_string(),
102                "{NUMBER:day} {MONTH:month}".to_string(),
103            ],
104        );
105        smartcalc.set_date_rule(
106            "tr",
107            vec![
108                "{NUMBER:day}/{NUMBER:month}/{NUMBER:year}".to_string(),
109                "{NUMBER:day} {MONTH:month} {NUMBER:year}".to_string(),
110                "{NUMBER:day} {MONTH:month}".to_string(),
111            ],
112        );
113        smartcalc
114    }
115}
116
117impl SmartCalc {
118    pub fn add_dynamic_type<T: Borrow<str>>(&mut self, name: T) -> bool {
119        match self.config.types.get(name.borrow()) {
120            Some(_) => false,
121            None => {
122                self.config
123                    .types
124                    .insert(name.borrow().to_string(), BTreeMap::new());
125                true
126            }
127        }
128    }
129
130    pub fn add_dynamic_type_item<T: Borrow<str>>(
131        &mut self,
132        name: T,
133        index: usize,
134        format: T,
135        parse: Vec<T>,
136        upgrade_code: T,
137        downgrade_code: T,
138        names: Vec<String>,
139        decimal_digits: Option<u8>,
140        use_fract_rounding: Option<bool>,
141        remove_fract_if_zero: Option<bool>,
142    ) -> bool {
143        match self.config.types.get(name.borrow()) {
144            Some(dynamic_type) => {
145                if dynamic_type.contains_key(&index) {
146                    return false;
147                }
148            }
149            None => return false,
150        };
151
152        let mut parse_tokens = Vec::new();
153        for type_parse_item in parse.iter() {
154            let mut session = Session::new();
155            session.set_language("en".to_string());
156            session.set_text(type_parse_item.borrow().to_string());
157
158            let tokens = Tokinizer::token_infos(&self.config, &session);
159            parse_tokens.push(tokens);
160        }
161
162        if let Some(dynamic_type) = self.config.types.get_mut(name.borrow()) {
163            dynamic_type.insert(
164                index,
165                Rc::new(DynamicType::new(
166                    name.borrow().to_string(),
167                    index,
168                    format.borrow().to_string(),
169                    parse_tokens,
170                    upgrade_code.borrow().to_string(),
171                    downgrade_code.borrow().to_string(),
172                    names,
173                    decimal_digits,
174                    use_fract_rounding,
175                    remove_fract_if_zero,
176                )),
177            );
178        }
179        true
180    }
181
182    pub fn set_money_configuration(
183        &mut self,
184        remove_fract_if_zero: bool,
185        use_fract_rounding: bool,
186    ) {
187        self.config.money_config.remove_fract_if_zero = remove_fract_if_zero;
188        self.config.money_config.use_fract_rounding = use_fract_rounding;
189    }
190
191    pub fn set_number_configuration(
192        &mut self,
193        decimal_digits: u8,
194        remove_fract_if_zero: bool,
195        use_fract_rounding: bool,
196    ) {
197        self.config.number_config.decimal_digits = decimal_digits;
198        self.config.number_config.remove_fract_if_zero = remove_fract_if_zero;
199        self.config.number_config.use_fract_rounding = use_fract_rounding;
200    }
201
202    pub fn set_percentage_configuration(
203        &mut self,
204        decimal_digits: u8,
205        remove_fract_if_zero: bool,
206        use_fract_rounding: bool,
207    ) {
208        self.config.percentage_config.decimal_digits = decimal_digits;
209        self.config.percentage_config.remove_fract_if_zero = remove_fract_if_zero;
210        self.config.percentage_config.use_fract_rounding = use_fract_rounding;
211    }
212
213    pub fn set_decimal_seperator(&mut self, decimal_seperator: String) {
214        self.config.decimal_seperator = decimal_seperator;
215    }
216
217    pub fn set_thousand_separator(&mut self, thousand_separator: String) {
218        self.config.thousand_separator = thousand_separator;
219    }
220
221    pub fn set_date_rule(&mut self, language: &str, rules: Vec<String>) {
222        let mut function_items = Vec::new();
223        for rule_item in rules {
224            let mut session = Session::new();
225            session.set_language(language.to_string());
226            session.set_text(rule_item.to_string());
227            function_items.push(Tokinizer::token_infos(&self.config, &session));
228        }
229
230        let current_rules = match self.config.rule.get_mut(language) {
231            Some(current_rules) => current_rules,
232            None => return,
233        };
234
235        /* Remove small_date rule */
236        current_rules.retain(|rule| {
237            let is_small_date = if let RuleType::Internal { function_name, .. } = rule {
238                function_name == "small_date"
239            } else {
240                false
241            };
242
243            !is_small_date
244        });
245
246        current_rules.push(RuleType::Internal {
247            function_name: "small_date".to_string(),
248            function: small_date as ExpressionFunc,
249            tokens_list: function_items,
250        });
251    }
252
253    pub fn set_timezone(&mut self, timezone: String) -> Result<(), String> {
254        let timezone = match self.config.token_parse_regex.get("timezone") {
255            Some(regexes) => {
256                let capture = match regexes[0].captures(&timezone) {
257                    Some(capture) => capture,
258                    None => return Err("Timezone information not found".to_string()),
259                };
260                match capture.name("timezone") {
261                    Some(_) => parse_timezone(&self.config, &capture),
262                    None => None,
263                }
264            }
265            _ => None,
266        };
267
268        match timezone {
269            Some((timezone, offset)) => {
270                self.config.timezone = timezone.to_uppercase();
271                self.config.timezone_offset = offset;
272                Ok(())
273            }
274            None => Err("Timezone information not found".to_string()),
275        }
276    }
277
278    pub fn get_time_offset(&self) -> TimeOffset {
279        self.config.get_time_offset()
280    }
281
282    pub fn load_from_json(json_data: &str) -> Self {
283        SmartCalc {
284            config: SmartCalcConfig::load_from_json(json_data),
285        }
286    }
287
288    pub fn update_currency(&mut self, currency: &str, rate: f64) -> bool {
289        match read_currency(&self.config, currency) {
290            Some(real_currency) => {
291                self.config.currency_rate.insert(real_currency, rate);
292                true
293            }
294            _ => false,
295        }
296    }
297
298    pub fn delete_rule(&mut self, language: String, rule_name: String) -> bool {
299        match self.config.rule.get_mut(&language) {
300            Some(language_collection) => {
301                let position = language_collection.iter().position(|item| match item {
302                    RuleType::API {
303                        tokens_list: _,
304                        rule: rule_item,
305                    } => rule_name == rule_item.name(),
306                    _ => false,
307                });
308
309                match position {
310                    Some(location) => {
311                        language_collection.remove(location);
312                        true
313                    }
314                    _ => false,
315                }
316            }
317            None => false,
318        }
319    }
320
321    pub fn add_rule(
322        &mut self,
323        language: String,
324        rules: Vec<String>,
325        rule: Rc<dyn RuleTrait>,
326    ) -> bool {
327        let mut rule_tokens = Vec::new();
328
329        for rule_item in rules.iter() {
330            let mut session = Session::new();
331            session.set_language(language.to_string());
332            session.set_text(rule_item.to_string());
333            let tokens = Tokinizer::token_infos(&self.config, &session);
334            rule_tokens.push(tokens);
335        }
336
337        let language_data = match self.config.rule.get_mut(&language) {
338            Some(language) => language,
339            None => return false,
340        };
341
342        language_data.push(RuleType::API {
343            tokens_list: rule_tokens,
344            rule,
345        });
346        true
347    }
348
349    pub fn format_result(&self, session: &Session, result: Rc<SmartCalcAstType>) -> String {
350        format_result(&self.config, session, result)
351    }
352
353    pub fn initialize() {
354        if log::set_logger(&LOGGER).is_ok() {
355            if cfg!(debug_assertions) {
356                log::set_max_level(log::LevelFilter::Debug)
357            } else {
358                log::set_max_level(log::LevelFilter::Info)
359            }
360        }
361    }
362
363    pub(crate) fn execute_text(&self, session: &Session) -> ExecutionLine {
364        log::debug!("> {}", session.current_line());
365        if session.current_line().is_empty() {
366            return None;
367        }
368
369        let mut tokinizer = Tokinizer::new(&self.config, session);
370        if !tokinizer.tokinize() {
371            return None;
372        }
373
374        let mut syntax = SyntaxParser::new(session, &tokinizer);
375        log::debug!(" > parse starting");
376
377        let execution_result = match syntax.parse() {
378            Ok(ast) => {
379                log::debug!(" > parse Ok {:?}", ast);
380                let ast_rc = Rc::new(ast);
381
382                match Interpreter::execute(&self.config, ast_rc, session) {
383                    Ok(ast) => Ok(ExecuteLineResult::new(
384                        self.format_result(session, ast.clone()),
385                        ast,
386                    )),
387                    Err(error) => Err(error),
388                }
389            }
390            Err((error, _, _)) => {
391                log::debug!(" > parse Err");
392                log::info!("Syntax parse error, {}", error);
393                Err(error.to_string())
394            }
395        };
396
397        Some(ExecuteLine::new(
398            execution_result,
399            tokinizer.ui_tokens.get_tokens(),
400            tokinizer.tokens,
401            tokinizer.token_infos.clone(),
402        ))
403    }
404
405    pub fn execute<Tlan: Borrow<str>, Tdata: Borrow<str>>(
406        &self,
407        language: Tlan,
408        data: Tdata,
409    ) -> ExecuteResult {
410        let mut session = Session::new();
411
412        session.set_text(data.borrow().to_string());
413        session.set_language(language.borrow().to_string());
414        self.execute_session(&session)
415    }
416
417    pub fn basic_execute<T: Borrow<str>>(data: T, config: &SmartCalcConfig) -> anyhow::Result<f64> {
418        let mut session = Session::new();
419
420        session.set_text(data.borrow().to_string());
421        session.set_language("en".borrow().to_string());
422
423        if session.line_count() != 1 {
424            return Err(anyhow!("Multiline calculation not supported"));
425        }
426
427        if !session.has_value() {
428            return Err(anyhow!("No data found"));
429        }
430
431        log::debug!("> {}", session.current_line());
432        if session.current_line().is_empty() {
433            return Err(anyhow!("Calculation empty"));
434        }
435
436        let mut tokinizer = Tokinizer::new(config, &session);
437        if !tokinizer.basic_tokinize() {
438            return Err(anyhow!("Syntax error"));
439        }
440
441        let mut syntax = SyntaxParser::new(&session, &tokinizer);
442        log::debug!(" > parse starting");
443
444        match syntax.parse() {
445            Ok(ast) => {
446                log::debug!(" > parse Ok {:?}", ast);
447                let ast_rc = Rc::new(ast);
448
449                match Interpreter::execute(config, ast_rc, &session) {
450                    Ok(ast) => match ast.deref() {
451                        SmartCalcAstType::Item(item) => Ok(item.get_underlying_number()),
452                        _ => Err(anyhow!("Number not found")),
453                    },
454                    Err(error) => Err(anyhow!(error)),
455                }
456            }
457            Err((error, _, _)) => {
458                log::debug!(" > parse Err");
459                log::info!("Syntax parse error, {}", error);
460                Err(anyhow!(error))
461            }
462        }
463    }
464
465    pub fn execute_session(&self, session: &Session) -> ExecuteResult {
466        let mut results = ExecuteResult::default();
467
468        if session.has_value() {
469            results.status = true;
470            loop {
471                let line_result = self.execute_text(session);
472                results.lines.push(line_result);
473                if session.next_line().is_none() {
474                    break;
475                }
476            }
477        }
478
479        results
480    }
481}
482
483#[cfg(test)]
484mod test {
485    use alloc::{
486        collections::BTreeMap,
487        rc::Rc,
488        string::{String, ToString},
489        vec,
490    };
491    use core::ops::Deref;
492
493    use crate::{
494        types::{NumberType, TokenType},
495        RuleTrait, SmartCalc, SmartCalcConfig,
496    };
497
498    #[derive(Default)]
499    pub struct Test1;
500    impl RuleTrait for Test1 {
501        fn name(&self) -> String {
502            "test1".to_string()
503        }
504        fn call(
505            &self,
506            _: &SmartCalcConfig,
507            fields: &BTreeMap<String, TokenType>,
508        ) -> Option<TokenType> {
509            match fields.get("surname") {
510                Some(TokenType::Text(surname)) => {
511                    assert_eq!(surname, &"baris".to_string());
512                    Some(TokenType::Number(2022.0, NumberType::Decimal))
513                }
514                _ => None,
515            }
516        }
517    }
518
519    macro_rules! check_basic_rule_output {
520        ($result:ident, $expected:expr) => {
521            assert!($result.status);
522            assert_eq!($result.lines.len(), 1);
523            assert_eq!($result.lines[0].is_some(), true);
524            match $result.lines.get(0) {
525                Some(line) => match line {
526                    Some(item) => match item.calculated_tokens.get(0) {
527                        Some(calculated_token) => assert_eq!(
528                            calculated_token
529                                .token_type
530                                .borrow()
531                                .deref()
532                                .as_ref()
533                                .unwrap(),
534                            &$expected
535                        ),
536                        None => assert!(false, "Calculated token not found"),
537                    },
538                    None => assert!(false, "Result line does not have value"),
539                },
540                None => assert!(false, "Result line not found"),
541            };
542        };
543    }
544
545    macro_rules! check_dynamic_type_output {
546        ($result:ident, $type_name:literal, $expected:literal) => {
547            assert!($result.status);
548            assert_eq!($result.lines.len(), 1);
549            assert_eq!($result.lines[0].is_some(), true);
550
551            match $result.lines.get(0) {
552                Some(line) => match line {
553                    Some(item) => match item.calculated_tokens.get(0) {
554                        Some(calculated_token) => {
555                            match calculated_token.token_type.borrow().deref() {
556                                Some(TokenType::DynamicType(number, item)) => {
557                                    assert_eq!(*number, $expected);
558                                    assert_eq!(item.deref().group_name, $type_name.to_string());
559                                }
560                                _ => assert!(
561                                    false,
562                                    "Expected token wrong. Token: {:?}",
563                                    calculated_token.token_type.borrow().deref()
564                                ),
565                            }
566                        }
567                        None => assert!(false, "Calculated token not found"),
568                    },
569                    None => assert!(false, "Result line does not have value"),
570                },
571                None => assert!(false, "Result line not found"),
572            };
573        };
574    }
575
576    #[test]
577    fn add_rule_1() -> Result<(), ()> {
578        let mut calculater = SmartCalc::default();
579        let test1 = Rc::new(Test1::default());
580        calculater.add_rule(
581            "en".to_string(),
582            vec![
583                "erhan {TEXT:surname}".to_string(),
584                "{TEXT:surname} erhan".to_string(),
585            ],
586            test1.clone(),
587        );
588        let result = calculater.execute("en".to_string(), "erhan baris");
589        check_basic_rule_output!(result, TokenType::Number(2022.0, NumberType::Decimal));
590
591        let result = calculater.execute("en".to_string(), "baris erhan");
592        check_basic_rule_output!(result, TokenType::Number(2022.0, NumberType::Decimal));
593
594        Ok(())
595    }
596
597    #[test]
598    fn add_rule_2() -> Result<(), ()> {
599        let mut calculater = SmartCalc::default();
600        let test1 = Rc::new(Test1::default());
601        calculater.add_rule(
602            "en".to_string(),
603            vec![
604                "erhan {TEXT:surname:baris}".to_string(),
605                "{TEXT:surname:baris} erhan".to_string(),
606            ],
607            test1.clone(),
608        );
609        let result = calculater.execute("en".to_string(), "erhan baris");
610        check_basic_rule_output!(result, TokenType::Number(2022.0, NumberType::Decimal));
611
612        let result = calculater.execute("en".to_string(), "baris erhan");
613        check_basic_rule_output!(result, TokenType::Number(2022.0, NumberType::Decimal));
614
615        Ok(())
616    }
617
618    #[test]
619    fn delete_rule_2() -> Result<(), ()> {
620        let mut calculater = SmartCalc::default();
621        let test1 = Rc::new(Test1::default());
622        assert!(calculater.add_rule(
623            "en".to_string(),
624            vec![
625                "erhan {TEXT:surname:baris}".to_string(),
626                "{TEXT:surname:baris} erhan".to_string()
627            ],
628            test1.clone()
629        ));
630        assert!(calculater.delete_rule("en".to_string(), test1.name().clone()));
631
632        Ok(())
633    }
634
635    #[test]
636    fn delete_rule_3() -> Result<(), ()> {
637        let mut calculater = SmartCalc::default();
638        let test1 = Rc::new(Test1::default());
639        assert!(!calculater.delete_rule("en".to_string(), test1.name().clone()));
640
641        Ok(())
642    }
643
644    #[test]
645    fn dynamic_type_1() -> Result<(), ()> {
646        let mut calculater = SmartCalc::default();
647        assert!(calculater.add_dynamic_type("test1"));
648        assert!(calculater.add_dynamic_type_item(
649            "test1",
650            1,
651            "{value} a",
652            vec!["{NUMBER:value} {TEXT:type:a}"],
653            "{value} / 2",
654            "{value} 2",
655            vec!["a".to_string()],
656            None,
657            None,
658            None
659        ));
660        assert!(calculater.add_dynamic_type_item(
661            "test1",
662            2,
663            "{value} b",
664            vec!["{NUMBER:value} {TEXT:type:b}"],
665            "{value} / 2",
666            "{value} * 2",
667            vec!["b".to_string()],
668            None,
669            None,
670            None
671        ));
672        assert!(calculater.add_dynamic_type_item(
673            "test1",
674            3,
675            "{value} c",
676            vec!["{NUMBER:value} {TEXT:type:c}"],
677            "{value} / 2",
678            "{value} * 2",
679            vec!["c".to_string()],
680            None,
681            None,
682            None
683        ));
684        assert!(calculater.add_dynamic_type_item(
685            "test1",
686            4,
687            "{value} d",
688            vec!["{NUMBER:value} {TEXT:type:d}"],
689            "{value}",
690            "{value} * 2",
691            vec!["d".to_string()],
692            None,
693            None,
694            None
695        ));
696
697        let result = calculater.execute("en".to_string(), "10 a to b");
698        check_dynamic_type_output!(result, "test1", 5.0);
699
700        let result = calculater.execute("en".to_string(), "1011 b to a");
701        check_dynamic_type_output!(result, "test1", 2022.0);
702
703        let result = calculater.execute("en".to_string(), "1 d to a");
704        check_dynamic_type_output!(result, "test1", 8.0);
705
706        let result = calculater.execute("en".to_string(), "8 a to d");
707        check_dynamic_type_output!(result, "test1", 1.0);
708        Ok(())
709    }
710
711    #[test]
712    fn dynamic_type_2() -> Result<(), ()> {
713        let mut calculater = SmartCalc::default();
714        assert!(calculater.add_dynamic_type("test1"));
715        assert!(!calculater.add_dynamic_type("test1"));
716
717        assert!(calculater.add_dynamic_type_item(
718            "test1",
719            1,
720            "{value} a",
721            vec!["{NUMBER:value} {TEXT:type:a}"],
722            "{value} / 2",
723            "{value} 2",
724            vec!["a".to_string()],
725            None,
726            None,
727            None
728        ));
729        assert!(!calculater.add_dynamic_type_item(
730            "test1",
731            1,
732            "{value} a",
733            vec!["{NUMBER:value} {TEXT:type:a}"],
734            "{value} / 2",
735            "{value} 2",
736            vec!["a".to_string()],
737            None,
738            None,
739            None
740        ));
741        Ok(())
742    }
743
744    #[test]
745    fn basic_test_1() -> anyhow::Result<()> {
746        let config = SmartCalcConfig::default();
747        let result = SmartCalc::basic_execute("1024", &config)?;
748        assert_eq!(result, 1024.0);
749        Ok(())
750    }
751
752    #[test]
753    fn basic_test_2() -> anyhow::Result<()> {
754        let config = SmartCalcConfig::default();
755        let result = SmartCalc::basic_execute("1024 * 2", &config)?;
756        assert_eq!(result, 2048.0);
757        Ok(())
758    }
759
760    #[test]
761    fn basic_test_3() -> anyhow::Result<()> {
762        let config = SmartCalcConfig::default();
763        let error = match SmartCalc::basic_execute("a + 1024 * 2", &config) {
764            Ok(_) => return Ok(()),
765            Err(error) => error,
766        };
767        assert_eq!(error.to_string(), "Number not found".to_string());
768        Ok(())
769    }
770
771    #[test]
772    fn basic_test_4() -> anyhow::Result<()> {
773        let config = SmartCalcConfig::default();
774        let error = match SmartCalc::basic_execute("+ 1024 * 2", &config) {
775            Ok(_) => return Ok(()),
776            Err(error) => error,
777        };
778        assert_eq!(error.to_string(), "No more token".to_string());
779        Ok(())
780    }
781
782    #[test]
783    fn basic_test_5() -> anyhow::Result<()> {
784        let config = SmartCalcConfig::default();
785        let error = match SmartCalc::basic_execute(
786            r#"1+ 1024 * 2
787"#,
788            &config,
789        ) {
790            Ok(_) => return Ok(()),
791            Err(error) => error,
792        };
793        assert_eq!(
794            error.to_string(),
795            "Multiline calculation not supported".to_string()
796        );
797        Ok(())
798    }
799
800    #[test]
801    fn basic_test_6() -> anyhow::Result<()> {
802        let config = SmartCalcConfig::default();
803        let error = match SmartCalc::basic_execute(r#""#, &config) {
804            Ok(_) => return Ok(()),
805            Err(error) => error,
806        };
807        assert_eq!(error.to_string(), "Calculation empty".to_string());
808        Ok(())
809    }
810
811    #[derive(Default)]
812    pub struct Coin;
813
814    impl RuleTrait for Coin {
815        fn name(&self) -> String {
816            "Coin".to_string()
817        }
818
819        fn call(
820            &self,
821            smartcalc: &SmartCalcConfig,
822            fields: &BTreeMap<String, TokenType>,
823        ) -> Option<TokenType> {
824            let count = match fields.get("count") {
825                Some(TokenType::Number(number, NumberType::Decimal)) => *number,
826                _ => return None,
827            };
828            let coin = match fields.get("coin") {
829                Some(TokenType::Text(text)) => text.clone(),
830                _ => return None,
831            };
832
833            let price = match &coin[..] {
834                "btc" => 1000.0 * count,
835                "eth" => 800.0 * count,
836                _ => return None,
837            };
838
839            return Some(TokenType::Money(
840                price,
841                smartcalc.get_currency("usd".to_string())?,
842            ));
843        }
844    }
845
846    #[test]
847    fn add_rule_3() -> Result<(), ()> {
848        let mut calculater = SmartCalc::default();
849        let test1 = Rc::new(Coin::default());
850        calculater.add_rule(
851            "en".to_string(),
852            vec!["{NUMBER:count} {TEXT:coin}".to_string()],
853            test1.clone(),
854        );
855        let result = calculater.execute("en".to_string(), "10 btc to usd");
856        check_basic_rule_output!(
857            result,
858            TokenType::Money(
859                10000.0,
860                calculater.config.get_currency("usd".to_string()).unwrap()
861            )
862        );
863        Ok(())
864    }
865
866    #[test]
867    fn add_rule_4() -> Result<(), ()> {
868        let mut calculater = SmartCalc::default();
869        let test1 = Rc::new(Coin::default());
870        calculater.add_rule(
871            "en".to_string(),
872            vec!["{NUMBER:count} {TEXT:coin}".to_string()],
873            test1.clone(),
874        );
875        let result = calculater.execute("en".to_string(), "10 eth to usd");
876        check_basic_rule_output!(
877            result,
878            TokenType::Money(
879                8000.0,
880                calculater.config.get_currency("usd".to_string()).unwrap()
881            )
882        );
883        Ok(())
884    }
885
886    #[test]
887    fn add_rule_5() -> Result<(), ()> {
888        let mut calculater = SmartCalc::default();
889        let test1 = Rc::new(Coin::default());
890        calculater.add_rule(
891            "en".to_string(),
892            vec!["{NUMBER:count} {TEXT:coin}".to_string()],
893            test1.clone(),
894        );
895        let result = calculater.execute("en".to_string(), "10 eth to dkk");
896        check_basic_rule_output!(
897            result,
898            TokenType::Money(
899                49644.9970792,
900                calculater.config.get_currency("dkk".to_string()).unwrap()
901            )
902        );
903        Ok(())
904    }
905}