factorion_lib/
parse.rs

1//! Parses text and extracts calculations
2
3use crate::locale::{self, NumFormat};
4use crate::rug::{Complete, Float, Integer, integer::IntegerExt64};
5
6use crate::Consts;
7use crate::{
8    calculation_results::Number,
9    calculation_tasks::{CalculationBase, CalculationJob},
10};
11
12pub mod recommended {
13    use factorion_math::rug::Integer;
14
15    pub static INTEGER_CONSTRUCTION_LIMIT: fn() -> Integer = || 100_000_000u128.into();
16}
17
18const POI_STARTS: &[char] = &[
19    NEGATION,
20    '!', // PREFIX_OPS
21    '.', // Decimal separators
22    ESCAPE,
23    '0', // Digits
24    '1',
25    '2',
26    '3',
27    '4',
28    '5',
29    '6',
30    '7',
31    '8',
32    '9',
33    'p', // Constants
34    'e',
35    't',
36    'π',
37    'ɸ',
38    'τ',
39    URI_POI,
40    SPOILER_POI,
41    SPOILER_HTML_POI,
42    PAREN_START,
43    PAREN_END,
44];
45
46const NEGATION: char = '-';
47const PAREN_START: char = '(';
48const PAREN_END: char = ')';
49const ESCAPE: char = '\\';
50const URI_START: &str = "://";
51const URI_POI: char = ':';
52const SPOILER_START: &str = ">!";
53const SPOILER_END: &str = "!<";
54const SPOILER_POI: char = '>';
55const SPOILER_HTML_START: &str = "&gt;!";
56const SPOILER_HTML_END: &str = "!&lt;";
57const SPOILER_HTML_POI: char = '&';
58
59const CONSTANT_STARTS: &[char] = &['p', 'e', 't', 'π', 'ɸ', 'τ'];
60static E: fn(u32) -> Number = |prec| Number::Float(Float::with_val(prec, 1).exp().into());
61static PHI: fn(u32) -> Number = |prec| {
62    Number::Float(Float::into(
63        ((1.0 + Float::with_val(prec, 5).sqrt()) as Float) / 2.0,
64    ))
65};
66static PI: fn(u32) -> Number =
67    |prec| Number::Float(Float::with_val(prec, crate::rug::float::Constant::Pi).into());
68static TAU: fn(u32) -> Number = |prec| {
69    Number::Float(Float::into(
70        Float::with_val(prec, crate::rug::float::Constant::Pi) * 2.0,
71    ))
72};
73
74const PREFIX_OPS: [char; 1] = ['!'];
75#[allow(dead_code)]
76const POSTFIX_OPS: [char; 2] = ['!', '?'];
77
78const INTEGER_ONLY_OPS: [i32; 1] = [0];
79
80pub fn parse(
81    mut text: &str,
82    do_termial: bool,
83    consts: &Consts,
84    locale: &NumFormat,
85) -> Vec<CalculationJob> {
86    // Parsing rules:
87    // - prefix has precedence before suffix (unimplemented)
88    // - anything within a spoiler should be ignored
89    // - operations may be nested through parentheses
90    // - operations can be negated through -
91    // - parens may contain:
92    //   - numbers
93    //   - operations
94    //   - parens
95    //   - whitespace
96    // - operations are:
97    //   - subfactorials !n
98    //   - (multi-)factorials n!+
99    //   - termials n?
100    // - numbers are in order:
101    //   - a string of digits
102    //   - a decimal separator and further digits
103    //   - a base 10 exponent, which is:
104    //     - an e or E followed by
105    //     - optionally a + or -
106    //     - a string of digits
107    // - numbers need to at least have the first or second criteria
108
109    // Parsing:
110    // 1. skip to interesting
111    // 2. If spoiler, skip
112    // 3. If negation, save
113    // 4. If paren start, push (with negation)
114    // 5. If paren end, pop
115    //   1. If had prefix, use base, set base
116    //   2. If has postfix, use base, set base
117    // 6. If prefix
118    //   1. If on number, set base
119    //     1. If has postfix, use base, set base
120    //   2. If on paren, push (with negation and level)
121    // 7. If number, parse
122    //   1. If has postfix, set base
123    //   2. If in parens, set as base
124    // 8. If on toplevel, add base to jobs
125    //
126    // when setting base:
127    // 1. If base is set, add previous to jobs
128    // 2. override base
129    let mut jobs = Vec::new();
130    let mut base: Option<CalculationBase> = None;
131    let mut paren_steps: Vec<(u32, Option<i32>, bool)> = Vec::new();
132    let mut current_negative: u32 = 0;
133    let mut last_len = usize::MAX;
134    let mut had_text_before = false;
135    while !text.is_empty() {
136        if last_len == text.len() {
137            panic!("Parser caught in a loop! Text: \"{text}\"")
138        }
139        last_len = text.len();
140
141        text = text.trim_start();
142        if text.len() != last_len {
143            current_negative = 0;
144            had_text_before = false;
145        }
146        // Text (1.)
147        let Some(position_of_interest) = text.find(POI_STARTS) else {
148            break;
149        };
150        if position_of_interest != 0 {
151            // poison paren
152            if let Some(step) = paren_steps.last_mut() {
153                step.2 = true;
154            }
155            current_negative = 0;
156            had_text_before = false;
157        }
158        let had_text =
159            text[..position_of_interest].ends_with(char::is_alphabetic) || had_text_before;
160        had_text_before = false;
161        // so we can just ignore everything before
162        text = &text[position_of_interest..];
163        if text.starts_with(ESCAPE) {
164            // Escapes
165            text = &text[1..];
166            let end = if text.starts_with(SPOILER_START) {
167                1
168            } else if text.starts_with(SPOILER_HTML_START) {
169                4
170            } else if text.starts_with(URI_START) {
171                3
172            } else {
173                0
174            };
175            text = &text[end..];
176            continue;
177        } else if text.starts_with(URI_START) {
178            // URI
179            let end = text.find(char::is_whitespace).unwrap_or(text.len());
180            text = &text[end..];
181            continue;
182        } else if text.starts_with(SPOILER_START) {
183            // Spoiler (2.)
184            let mut end = 0;
185            loop {
186                // look for next end tag
187                if let Some(e) = text[end..].find(SPOILER_END) {
188                    if e == 0 {
189                        panic!("Parser loop Spoiler! Text \"{text}\"");
190                    }
191                    end += e;
192                    // is escaped -> look further
193                    if text[end.saturating_sub(1)..].starts_with(ESCAPE) {
194                        end += 1;
195                        continue;
196                    }
197                    break;
198                } else {
199                    // if we find none, we skip only the start (without the !)
200                    end = 0;
201                    break;
202                }
203            }
204            current_negative = 0;
205            text = &text[end + 1..];
206            continue;
207        } else if text.starts_with(SPOILER_HTML_START) {
208            // Spoiler (html) (2.)
209            let mut end = 0;
210            loop {
211                // look for next end tag
212                if let Some(e) = text[end..].find(SPOILER_HTML_END) {
213                    if e == 0 {
214                        panic!("Parser loop Spoiler! Text \"{text}\"");
215                    }
216                    end += e;
217                    // is escaped -> look further
218                    if text[end.saturating_sub(1)..].starts_with(ESCAPE) {
219                        end += 1;
220                        continue;
221                    }
222                    break;
223                } else {
224                    // if we find none, we skip only the start (without the !)
225                    end = 0;
226                    break;
227                }
228            }
229            current_negative = 0;
230            text = &text[end + 4..];
231            continue;
232        } else if text.starts_with(NEGATION) {
233            // Negation (3.)
234            let end = text.find(|c| c != NEGATION).unwrap_or(text.len());
235            current_negative = end as u32;
236            text = &text[end..];
237            continue;
238        } else if text.starts_with(PAREN_START) {
239            // Paren Start (without prefix op) (4.)
240            paren_steps.push((current_negative, None, false));
241            // Submit current base (we won't use it anymore)
242            if let Some(CalculationBase::Calc(job)) = base.take() {
243                jobs.push(*job);
244            }
245            current_negative = 0;
246            text = &text[1..];
247            continue;
248        } else if text.starts_with(PAREN_END) {
249            // Paren End (5.)
250            text = &text[1..];
251            current_negative = 0;
252            // Paren mismatch?
253            let Some(step) = paren_steps.pop() else {
254                continue;
255            };
256            // poisoned paren
257            if step.2 {
258                if let Some(CalculationBase::Calc(job)) = base.take() {
259                    jobs.push(*job);
260                }
261                // no number (maybe var) => poison outer paren
262                if let Some(step) = paren_steps.last_mut() {
263                    step.2 = true;
264                }
265                continue;
266            }
267            let mut had_op = false;
268            // Prefix? (5.2.)
269            if let Some(level) = step.1 {
270                // base available?
271                let Some(inner) = base.take() else {
272                    // no number (maybe var) => poison outer paren
273                    if let Some(step) = paren_steps.last_mut() {
274                        step.2 = true;
275                    }
276                    continue;
277                };
278                if let (CalculationBase::Num(Number::Float(_)), true) =
279                    (&inner, INTEGER_ONLY_OPS.contains(&level))
280                {
281                    continue;
282                }
283                base = Some(CalculationBase::Calc(Box::new(CalculationJob {
284                    base: inner,
285                    level,
286                    negative: 0,
287                })));
288                had_op = true;
289            }
290            // Postfix? (5.1.)
291            let Some(levels) = parse_ops(&mut text, false, do_termial) else {
292                base.take();
293                // no number (maybe var) => poison outer paren
294                if let Some(step) = paren_steps.last_mut() {
295                    step.2 = true;
296                }
297                continue;
298            };
299            if !levels.is_empty() {
300                // Set as base (5.1.2.)
301                for level in levels {
302                    // base available?
303                    let Some(inner) = base.take() else {
304                        continue;
305                    };
306                    base = Some(CalculationBase::Calc(Box::new(CalculationJob {
307                        base: inner,
308                        level,
309                        negative: 0,
310                    })));
311                    had_op = true;
312                }
313            }
314            if !had_op {
315                match &mut base {
316                    Some(CalculationBase::Calc(job)) => job.negative += step.0,
317                    Some(CalculationBase::Num(n)) => {
318                        if step.0 % 2 != 0 {
319                            n.negate();
320                        }
321                    }
322                    None => {}
323                }
324            } else {
325                match &mut base {
326                    Some(CalculationBase::Num(n)) => {
327                        if step.0 % 2 == 1 {
328                            n.negate();
329                        }
330                    }
331                    Some(CalculationBase::Calc(job)) => job.negative += step.0,
332                    None => {
333                        // no number (maybe var) => poison outer paren
334                        if let Some(step) = paren_steps.last_mut() {
335                            step.2 = true;
336                        }
337                    }
338                }
339                continue;
340            };
341        } else if text.starts_with(PREFIX_OPS) {
342            // Prefix OP (6.)
343            let Ok(level) = parse_op(&mut text, true, do_termial) else {
344                // also skip number to prevent stuff like "!!!1!" getting through
345                parse_num(&mut text, false, true, consts, locale);
346                continue;
347            };
348            // On number (6.1.)
349            if let Some(num) = parse_num(&mut text, false, true, consts, locale) {
350                // set base (6.1.2.)
351                if let Some(CalculationBase::Calc(job)) = base.take() {
352                    // multiple number, likely expression => poision paren
353                    if let Some(step) = paren_steps.last_mut() {
354                        step.2 = true;
355                    }
356                    jobs.push(*job);
357                }
358                if let (Number::Float(_), true) = (&num, INTEGER_ONLY_OPS.contains(&level)) {
359                    continue;
360                }
361                base = Some(CalculationBase::Calc(Box::new(CalculationJob {
362                    base: CalculationBase::Num(num),
363                    level,
364                    negative: current_negative,
365                })));
366                current_negative = 0;
367                let Some(levels) = parse_ops(&mut text, false, do_termial) else {
368                    continue;
369                };
370                for level in levels {
371                    // base available?
372                    let Some(inner) = base.take() else {
373                        continue;
374                    };
375                    base = Some(CalculationBase::Calc(Box::new(CalculationJob {
376                        base: inner,
377                        level,
378                        negative: 0,
379                    })));
380                }
381            } else {
382                // on paren? (6.2.)
383                if text.starts_with(PAREN_START) {
384                    paren_steps.push((current_negative, Some(level), false));
385                    current_negative = 0;
386                    text = &text[1..];
387                }
388                continue;
389            };
390        } else {
391            // Number (7.)
392            if text.starts_with('.') && !text[1..].starts_with(char::is_numeric) {
393                // Is a period
394                text = &text[1..];
395                continue;
396            }
397            let Some(num) = parse_num(&mut text, had_text, false, consts, locale) else {
398                had_text_before = true;
399                // advance one char to avoid loop
400                let mut end = 1;
401                while !text.is_char_boundary(end) && end < text.len() {
402                    end += 1;
403                }
404                text = &text[end.min(text.len())..];
405                continue;
406            };
407            // postfix? (7.1.)
408            let Some(levels) = parse_ops(&mut text, false, do_termial) else {
409                continue;
410            };
411            if !levels.is_empty() {
412                let levels = levels.into_iter();
413                if let Some(CalculationBase::Calc(job)) = base.take() {
414                    // multiple number, likely expression => poision paren
415                    if let Some(step) = paren_steps.last_mut() {
416                        step.2 = true;
417                    }
418                    jobs.push(*job);
419                }
420                base = Some(CalculationBase::Num(num));
421                for level in levels {
422                    let previous = base.take().unwrap();
423                    if let (CalculationBase::Num(Number::Float(_)), true) =
424                        (&previous, INTEGER_ONLY_OPS.contains(&level))
425                    {
426                        continue;
427                    }
428                    base = Some(CalculationBase::Calc(Box::new(CalculationJob {
429                        base: previous,
430                        level,
431                        negative: 0,
432                    })))
433                }
434                if let Some(CalculationBase::Calc(job)) = &mut base {
435                    job.negative = current_negative;
436                }
437            } else {
438                // in parens? (7.2.)
439                if !paren_steps.is_empty() {
440                    let mut num = num;
441                    if current_negative % 2 == 1 {
442                        num.negate();
443                    }
444
445                    if base.is_none() {
446                        base = Some(CalculationBase::Num(num))
447                    } else {
448                        // multiple number, likely expression => poision paren
449                        if let Some(step) = paren_steps.last_mut() {
450                            step.2 = true;
451                        }
452                    }
453                }
454            }
455            current_negative = 0;
456        };
457        // toplevel? (8.)
458        if paren_steps.is_empty()
459            && let Some(CalculationBase::Calc(job)) = base.take()
460        {
461            jobs.push(*job);
462        }
463    }
464    if let Some(CalculationBase::Calc(job)) = base.take() {
465        jobs.push(*job);
466    }
467    jobs.sort();
468    jobs.dedup();
469    jobs
470}
471
472enum ParseOpErr {
473    NonOp,
474    InvalidOp,
475}
476
477fn parse_op(text: &mut &str, prefix: bool, do_termial: bool) -> Result<i32, ParseOpErr> {
478    let op = text.chars().next().ok_or(ParseOpErr::NonOp)?;
479    let end = text.find(|c| c != op).unwrap_or(text.len());
480    let res = match op {
481        '!' => {
482            if prefix {
483                if end != 1 {
484                    Err(ParseOpErr::InvalidOp)
485                } else {
486                    Ok(0)
487                }
488            } else {
489                Ok(end as i32)
490            }
491        }
492        '?' => {
493            if !do_termial {
494                Err(ParseOpErr::NonOp)
495            } else if prefix {
496                Err(ParseOpErr::InvalidOp)
497            } else {
498                Ok(-(end as i32))
499            }
500        }
501        _ => return Err(ParseOpErr::NonOp),
502    };
503    *text = &text[end..];
504    res
505}
506
507fn parse_ops(text: &mut &str, prefix: bool, do_termial: bool) -> Option<Vec<i32>> {
508    let mut res = Vec::new();
509    loop {
510        match parse_op(text, prefix, do_termial) {
511            Ok(op) => res.push(op),
512            Err(ParseOpErr::NonOp) => break,
513            Err(ParseOpErr::InvalidOp) => return None,
514        }
515    }
516    Some(res)
517}
518
519fn parse_num(
520    text: &mut &str,
521    had_text: bool,
522    had_op: bool,
523    consts: &Consts,
524    locale: &NumFormat,
525) -> Option<Number> {
526    let prec = consts.float_precision;
527    if text.starts_with(CONSTANT_STARTS) {
528        let (n, x) = if text.starts_with("pi") {
529            ("pi".len(), PI(prec))
530        } else if text.starts_with("π") {
531            ("π".len(), PI(prec))
532        } else if text.starts_with("phi") {
533            ("phi".len(), PHI(prec))
534        } else if text.starts_with("ɸ") {
535            ("ɸ".len(), PHI(prec))
536        } else if text.starts_with("tau") {
537            ("tau".len(), TAU(prec))
538        } else if text.starts_with("τ") {
539            ("τ".len(), TAU(prec))
540        } else if text.starts_with("e") {
541            ("e".len(), E(prec))
542        } else {
543            return None;
544        };
545        if had_text || text[n..].starts_with(char::is_alphabetic) {
546            return None;
547        }
548        *text = &text[n..];
549        return Some(x);
550    }
551
552    let integer_part = {
553        let end = text.find(|c: char| !c.is_numeric()).unwrap_or(text.len());
554        let part = &text[..end];
555        *text = &text[end..];
556        part
557    };
558    let decimal_part = if text.starts_with(*locale.decimal()) {
559        *text = &text[1..];
560        let end = text.find(|c: char| !c.is_numeric()).unwrap_or(text.len());
561        let part = &text[..end];
562        *text = &text[end..];
563        part
564    } else {
565        &text[..0]
566    };
567    let exponent_part = if text.starts_with(['e', 'E']) {
568        *text = &text[1..];
569        let negative = if text.starts_with('+') {
570            *text = &text[1..];
571            false
572        } else if text.starts_with('-') {
573            *text = &text[1..];
574            true
575        } else {
576            false
577        };
578        let end = text.find(|c: char| !c.is_numeric()).unwrap_or(text.len());
579        let part = &text[..end];
580        *text = &text[end..];
581        (part, negative)
582    } else {
583        (&text[..0], false)
584    };
585    let fraction_part = if !had_op && text.starts_with(['/']) {
586        *text = &text[1..];
587        let end = text.find(|c: char| !c.is_numeric()).unwrap_or(text.len());
588        let part = &text[..end];
589        *text = &text[end..];
590        part
591    } else {
592        &text[..0]
593    };
594    if text.starts_with(POSTFIX_OPS) && !fraction_part.is_empty() {
595        let n = fraction_part.parse::<Integer>().ok()?;
596        return Some(Number::Exact(n));
597    }
598    if integer_part.is_empty() && decimal_part.is_empty() {
599        return None;
600    }
601    let exponent = if !exponent_part.0.is_empty() {
602        let mut e = exponent_part.0.parse::<Integer>().ok()?;
603        if exponent_part.1 {
604            e *= -1;
605        }
606        e
607    } else {
608        0.into()
609    };
610    let divisor = if !fraction_part.is_empty() {
611        fraction_part.parse::<Integer>().ok()?
612    } else {
613        Integer::ONE.clone()
614    };
615    if exponent >= decimal_part.len() as i64
616        && exponent <= consts.integer_construction_limit.clone() - integer_part.len() as i64
617        && (divisor == 1 || exponent >= consts.integer_construction_limit.clone() / 10)
618    {
619        let exponent = exponent - decimal_part.len();
620        let n = format!("{integer_part}{decimal_part}")
621            .parse::<Integer>()
622            .ok()?;
623        let num = (n * Integer::u64_pow_u64(10, exponent.to_u64().unwrap()).complete()) / divisor;
624        Some(Number::Exact(num))
625    } else if exponent <= consts.integer_construction_limit.clone() - integer_part.len() as i64 {
626        let x = Float::parse(format!(
627            "{integer_part}.{decimal_part}{}{}{}",
628            if !exponent_part.0.is_empty() { "e" } else { "" },
629            if exponent_part.1 { "-" } else { "" },
630            exponent_part.0
631        ))
632        .ok()?;
633        let x = Float::with_val(prec, x) / divisor;
634        if x.is_integer() {
635            let n = x.to_integer().unwrap();
636            Some(Number::Exact(n))
637        } else if x.is_finite() {
638            Some(Number::Float(x.into()))
639        } else {
640            None
641        }
642    } else {
643        let x = Float::parse(format!("{integer_part}.{decimal_part}")).ok()?;
644        let x = Float::with_val(prec, x) / divisor;
645        if x.is_finite() {
646            let (b, e) = crate::math::adjust_approximate((x, exponent));
647            Some(Number::Approximate(b.into(), e))
648        } else {
649            None
650        }
651    }
652}
653
654#[cfg(test)]
655mod test {
656    use super::*;
657    use crate::calculation_tasks::CalculationBase::Num;
658    use arbtest::arbtest;
659
660    use crate::recommended::FLOAT_PRECISION;
661
662    #[test]
663    fn test_text_only() {
664        let consts = Consts::default();
665        let jobs = parse(
666            "just some words of encouragement!",
667            true,
668            &consts,
669            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
670        );
671        assert_eq!(jobs, []);
672    }
673    #[test]
674    fn test_factorial() {
675        let consts = Consts::default();
676        let jobs = parse(
677            "a factorial 15!",
678            true,
679            &consts,
680            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
681        );
682        assert_eq!(
683            jobs,
684            [CalculationJob {
685                base: CalculationBase::Num(15.into()),
686                level: 1,
687                negative: 0
688            }]
689        );
690    }
691    #[test]
692    fn test_multifactorial() {
693        let consts = Consts::default();
694        let jobs = parse(
695            "a factorial 15!!! actually a multi",
696            true,
697            &consts,
698            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
699        );
700        assert_eq!(
701            jobs,
702            [CalculationJob {
703                base: CalculationBase::Num(15.into()),
704                level: 3,
705                negative: 0
706            }]
707        );
708    }
709    #[test]
710    fn test_subfactorial() {
711        let consts = Consts::default();
712        let jobs = parse(
713            "a factorial !15 actually a sub",
714            true,
715            &consts,
716            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
717        );
718        assert_eq!(
719            jobs,
720            [CalculationJob {
721                base: CalculationBase::Num(15.into()),
722                level: 0,
723                negative: 0
724            }]
725        );
726    }
727    #[test]
728    fn test_submultifactorial() {
729        let consts = Consts::default();
730        let jobs = parse(
731            "not well defined !!!15",
732            true,
733            &consts,
734            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
735        );
736        assert_eq!(jobs, []);
737    }
738    #[test]
739    fn test_termial() {
740        let consts = Consts::default();
741        let jobs = parse(
742            "a termial 15?",
743            true,
744            &consts,
745            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
746        );
747        assert_eq!(
748            jobs,
749            [CalculationJob {
750                base: CalculationBase::Num(15.into()),
751                level: -1,
752                negative: 0
753            }]
754        );
755    }
756    #[test]
757    fn test_no_termial() {
758        let consts = Consts::default();
759        let jobs = parse(
760            "not enabled 15?",
761            false,
762            &consts,
763            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
764        );
765        assert_eq!(jobs, []);
766    }
767    #[test]
768    fn test_multitermial() {
769        let consts = Consts::default();
770        let jobs = parse(
771            "a termial 15??? actually a multi",
772            true,
773            &consts,
774            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
775        );
776        assert_eq!(
777            jobs,
778            [CalculationJob {
779                base: CalculationBase::Num(15.into()),
780                level: -3,
781                negative: 0
782            }]
783        );
784    }
785    #[test]
786    fn test_subtermial() {
787        let consts = Consts::default();
788        let jobs = parse(
789            "a termial ?15 actually a sub",
790            true,
791            &consts,
792            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
793        );
794        assert_eq!(jobs, []);
795    }
796    #[test]
797    fn test_chain() {
798        let consts = Consts::default();
799        let jobs = parse(
800            "a factorialchain (15!)!",
801            true,
802            &consts,
803            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
804        );
805        assert_eq!(
806            jobs,
807            [CalculationJob {
808                base: CalculationBase::Calc(Box::new(CalculationJob {
809                    base: CalculationBase::Num(15.into()),
810                    level: 1,
811                    negative: 0
812                })),
813                level: 1,
814                negative: 0
815            }]
816        );
817    }
818    #[test]
819    fn test_mixed_chain() {
820        let consts = Consts::default();
821        let jobs = parse(
822            "a factorialchain !(15!)",
823            true,
824            &consts,
825            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
826        );
827        assert_eq!(
828            jobs,
829            [CalculationJob {
830                base: CalculationBase::Calc(Box::new(CalculationJob {
831                    base: CalculationBase::Num(15.into()),
832                    level: 1,
833                    negative: 0
834                })),
835                level: 0,
836                negative: 0
837            }]
838        );
839    }
840    #[test]
841    fn test_postfix_chain() {
842        let consts = Consts::default();
843        let jobs = parse(
844            "a factorialchain -15!?",
845            true,
846            &consts,
847            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
848        );
849        assert_eq!(
850            jobs,
851            [CalculationJob {
852                base: CalculationBase::Calc(Box::new(CalculationJob {
853                    base: CalculationBase::Num(15.into()),
854                    level: 1,
855                    negative: 0
856                })),
857                level: -1,
858                negative: 1
859            }]
860        );
861    }
862    #[test]
863    fn test_negative() {
864        let consts = Consts::default();
865        let jobs = parse(
866            "a factorial ---15!",
867            true,
868            &consts,
869            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
870        );
871        assert_eq!(
872            jobs,
873            [CalculationJob {
874                base: CalculationBase::Num(15.into()),
875                level: 1,
876                negative: 3
877            }]
878        );
879    }
880    #[test]
881    fn test_negative_gap() {
882        let consts = Consts::default();
883        let jobs = parse(
884            "a factorial --- 15!",
885            true,
886            &consts,
887            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
888        );
889        assert_eq!(
890            jobs,
891            [CalculationJob {
892                base: CalculationBase::Num(15.into()),
893                level: 1,
894                negative: 0
895            }]
896        );
897    }
898    #[test]
899    fn test_paren() {
900        let consts = Consts::default();
901        let jobs = parse(
902            "a factorial (15)!",
903            true,
904            &consts,
905            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
906        );
907        assert_eq!(
908            jobs,
909            [CalculationJob {
910                base: CalculationBase::Num(15.into()),
911                level: 1,
912                negative: 0
913            }]
914        );
915    }
916    #[test]
917    fn test_in_paren() {
918        let consts = Consts::default();
919        let jobs = parse(
920            "a factorial (15!)",
921            true,
922            &consts,
923            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
924        );
925        assert_eq!(
926            jobs,
927            [CalculationJob {
928                base: CalculationBase::Num(15.into()),
929                level: 1,
930                negative: 0
931            }]
932        );
933    }
934    #[test]
935    fn test_decimal() {
936        let consts = Consts::default();
937        let jobs = parse(
938            "a factorial 1.5!",
939            true,
940            &consts,
941            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
942        );
943        assert_eq!(
944            jobs,
945            [CalculationJob {
946                base: CalculationBase::Num(Float::with_val(FLOAT_PRECISION, 1.5).into()),
947                level: 1,
948                negative: 0
949            }]
950        );
951    }
952    #[test]
953    fn test_paren_negation() {
954        let consts = Consts::default();
955        let jobs = parse(
956            "a factorial -(--(-(-(-3))!))!",
957            true,
958            &consts,
959            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
960        );
961        assert_eq!(
962            jobs,
963            [CalculationJob {
964                base: CalculationBase::Calc(Box::new(CalculationJob {
965                    base: CalculationBase::Num(3.into()),
966                    level: 1,
967                    negative: 3
968                })),
969                level: 1,
970                negative: 1
971            }]
972        );
973    }
974    #[test]
975    fn test_tag() {
976        let consts = Consts::default();
977        let jobs = parse(
978            ">!5 a factorial 15! !<",
979            true,
980            &consts,
981            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
982        );
983        assert_eq!(jobs, []);
984    }
985    #[test]
986    fn test_incomplete_tag() {
987        let consts = Consts::default();
988        let jobs = parse(
989            ">!5 a factorial 15!",
990            true,
991            &consts,
992            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
993        );
994        assert_eq!(
995            jobs,
996            [
997                CalculationJob {
998                    base: CalculationBase::Num(5.into()),
999                    level: 0,
1000                    negative: 0
1001                },
1002                CalculationJob {
1003                    base: CalculationBase::Num(15.into()),
1004                    level: 1,
1005                    negative: 0
1006                }
1007            ]
1008        );
1009    }
1010    #[test]
1011    fn test_escaped_tag() {
1012        let consts = Consts::default();
1013        let jobs = parse(
1014            "\\>!5 a factorial 15! !<",
1015            true,
1016            &consts,
1017            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1018        );
1019        assert_eq!(
1020            jobs,
1021            [
1022                CalculationJob {
1023                    base: CalculationBase::Num(5.into()),
1024                    level: 0,
1025                    negative: 0
1026                },
1027                CalculationJob {
1028                    base: CalculationBase::Num(15.into()),
1029                    level: 1,
1030                    negative: 0
1031                }
1032            ]
1033        );
1034    }
1035    #[test]
1036    fn test_escaped_tag2() {
1037        let consts = Consts::default();
1038        let jobs = parse(
1039            ">!5 a factorial 15! \\!<",
1040            true,
1041            &consts,
1042            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1043        );
1044        assert_eq!(
1045            jobs,
1046            [
1047                CalculationJob {
1048                    base: CalculationBase::Num(5.into()),
1049                    level: 0,
1050                    negative: 0
1051                },
1052                CalculationJob {
1053                    base: CalculationBase::Num(15.into()),
1054                    level: 1,
1055                    negative: 0
1056                }
1057            ]
1058        );
1059    }
1060
1061    #[test]
1062    fn test_url() {
1063        let consts = Consts::default();
1064        let jobs = parse(
1065            "https://something.somewhere/with/path/and?tag=siufgiufgia3873844hi8743!hfsf",
1066            true,
1067            &consts,
1068            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1069        );
1070        assert_eq!(jobs, []);
1071    }
1072
1073    #[test]
1074    fn test_uri_poi_doesnt_cause_infinite_loop() {
1075        let consts = Consts::default();
1076        let jobs = parse(
1077            "84!:",
1078            true,
1079            &consts,
1080            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1081        );
1082        assert_eq!(
1083            jobs,
1084            [CalculationJob {
1085                base: Num(84.into()),
1086                level: 1,
1087                negative: 0
1088            }]
1089        );
1090    }
1091    #[test]
1092    fn test_escaped_url() {
1093        let consts = Consts::default();
1094        let jobs = parse(
1095            "\\://something.somewhere/with/path/and?tag=siufgiufgia3873844hi8743!hfsf",
1096            true,
1097            &consts,
1098            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1099        );
1100        assert_eq!(
1101            jobs,
1102            [CalculationJob {
1103                base: CalculationBase::Num(8743.into()),
1104                level: 1,
1105                negative: 0
1106            }]
1107        );
1108    }
1109
1110    #[test]
1111    fn test_word_in_paren() {
1112        let consts = Consts::default();
1113        let jobs = parse(
1114            "(x-2)! (2 word)! ((x/k)-3)! (,x-4)!",
1115            true,
1116            &consts,
1117            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1118        );
1119        assert_eq!(jobs, []);
1120    }
1121
1122    #[test]
1123    fn test_multi_number_paren() {
1124        let consts = Consts::default();
1125        let jobs = parse(
1126            "(5-2)!",
1127            true,
1128            &consts,
1129            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1130        );
1131        assert_eq!(jobs, []);
1132    }
1133    #[test]
1134    fn test_arbitrary_input() {
1135        let consts = Consts::default();
1136        arbtest(|u| {
1137            let text: &str = u.arbitrary()?;
1138            let _ = parse(
1139                text,
1140                u.arbitrary()?,
1141                &consts,
1142                &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1143            );
1144            Ok(())
1145        });
1146    }
1147
1148    #[test]
1149    fn test_constant() {
1150        let consts = Consts::default();
1151        let jobs = parse(
1152            "!espi!",
1153            true,
1154            &consts,
1155            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1156        );
1157        assert_eq!(jobs, []);
1158        let jobs = parse(
1159            "some. pi!",
1160            true,
1161            &consts,
1162            &consts.locales.get("en").unwrap().format().number_format(),
1163        );
1164        assert_eq!(
1165            jobs,
1166            [CalculationJob {
1167                base: CalculationBase::Num(Number::Float(
1168                    Float::with_val(FLOAT_PRECISION, factorion_math::rug::float::Constant::Pi)
1169                        .into()
1170                )),
1171                level: 1,
1172                negative: 0
1173            }]
1174        );
1175    }
1176
1177    #[test]
1178    fn test_fraction() {
1179        let consts = Consts::default();
1180        let jobs = parse(
1181            "!5/6!",
1182            true,
1183            &consts,
1184            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1185        );
1186        assert_eq!(
1187            jobs,
1188            [
1189                CalculationJob {
1190                    base: CalculationBase::Num(Number::Exact(5.into())),
1191                    level: 0,
1192                    negative: 0
1193                },
1194                CalculationJob {
1195                    base: CalculationBase::Num(Number::Exact(6.into())),
1196                    level: 1,
1197                    negative: 0
1198                }
1199            ]
1200        );
1201        let jobs = parse(
1202            "5/6!",
1203            true,
1204            &consts,
1205            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1206        );
1207        assert_eq!(
1208            jobs,
1209            [CalculationJob {
1210                base: CalculationBase::Num(Number::Exact(6.into())),
1211                level: 1,
1212                negative: 0
1213            }]
1214        );
1215        let jobs = parse(
1216            "(10/2)!",
1217            true,
1218            &consts,
1219            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1220        );
1221        assert_eq!(
1222            jobs,
1223            [CalculationJob {
1224                base: CalculationBase::Num(Number::Exact(5.into())),
1225                level: 1,
1226                negative: 0
1227            },]
1228        );
1229    }
1230
1231    #[test]
1232    fn test_parse_num() {
1233        let consts = Consts::default();
1234        let num = parse_num(
1235            &mut "1.5more !",
1236            false,
1237            false,
1238            &consts,
1239            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1240        );
1241        assert_eq!(
1242            num,
1243            Some(Number::Float(Float::with_val(FLOAT_PRECISION, 1.5).into()))
1244        );
1245        let num = parse_num(
1246            &mut "1,5more !",
1247            false,
1248            false,
1249            &consts,
1250            &NumFormat::V1(&locale::v1::NumFormat { decimal: ',' }),
1251        );
1252        assert_eq!(
1253            num,
1254            Some(Number::Float(Float::with_val(FLOAT_PRECISION, 1.5).into()))
1255        );
1256        let num = parse_num(
1257            &mut ".5more !",
1258            false,
1259            false,
1260            &consts,
1261            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1262        );
1263        assert_eq!(
1264            num,
1265            Some(Number::Float(Float::with_val(FLOAT_PRECISION, 0.5).into()))
1266        );
1267        let num = parse_num(
1268            &mut "1more !",
1269            false,
1270            true,
1271            &consts,
1272            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1273        );
1274        assert_eq!(num, Some(1.into()));
1275        let num = parse_num(
1276            &mut "1.0more !",
1277            true,
1278            false,
1279            &consts,
1280            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1281        );
1282        assert_eq!(num, Some(1.into()));
1283        let num = parse_num(
1284            &mut "1.5e2more !",
1285            false,
1286            false,
1287            &consts,
1288            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1289        );
1290        assert_eq!(num, Some(150.into()));
1291        let num = parse_num(
1292            &mut "1e2more !",
1293            false,
1294            false,
1295            &consts,
1296            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1297        );
1298        assert_eq!(num, Some(100.into()));
1299        let num = parse_num(
1300            &mut "1.531e2more !",
1301            false,
1302            false,
1303            &consts,
1304            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1305        );
1306        let Some(Number::Float(f)) = num else {
1307            panic!("Not a float")
1308        };
1309        assert!(Float::abs(f.as_float().clone() - 153.1) < 0.0000001);
1310        let num = parse_num(
1311            &mut "5e-1more !",
1312            false,
1313            false,
1314            &consts,
1315            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1316        );
1317        assert_eq!(
1318            num,
1319            Some(Number::Float(Float::with_val(FLOAT_PRECISION, 0.5).into()))
1320        );
1321        let num = parse_num(
1322            &mut "e2more !",
1323            true,
1324            false,
1325            &consts,
1326            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1327        );
1328        assert_eq!(num, None);
1329        let num = parse_num(
1330            &mut "es !",
1331            false,
1332            false,
1333            &consts,
1334            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1335        );
1336        assert_eq!(num, None);
1337        let num = parse_num(
1338            &mut "e !",
1339            false,
1340            false,
1341            &consts,
1342            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1343        );
1344        assert_eq!(num, Some(E(FLOAT_PRECISION)));
1345        let num = parse_num(
1346            &mut "pi !",
1347            false,
1348            false,
1349            &consts,
1350            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1351        );
1352        assert_eq!(num, Some(PI(FLOAT_PRECISION)));
1353        let num = parse_num(
1354            &mut "π !",
1355            false,
1356            false,
1357            &consts,
1358            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1359        );
1360        assert_eq!(num, Some(PI(FLOAT_PRECISION)));
1361        let num = parse_num(
1362            &mut "phi !",
1363            false,
1364            false,
1365            &consts,
1366            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1367        );
1368        assert_eq!(num, Some(PHI(FLOAT_PRECISION)));
1369        let num = parse_num(
1370            &mut "ɸ !",
1371            false,
1372            false,
1373            &consts,
1374            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1375        );
1376        assert_eq!(num, Some(PHI(FLOAT_PRECISION)));
1377        let num = parse_num(
1378            &mut "tau !",
1379            false,
1380            false,
1381            &consts,
1382            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1383        );
1384        assert_eq!(num, Some(TAU(FLOAT_PRECISION)));
1385        let num = parse_num(
1386            &mut "τ !",
1387            false,
1388            false,
1389            &consts,
1390            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1391        );
1392        assert_eq!(num, Some(TAU(FLOAT_PRECISION)));
1393        let num = parse_num(
1394            &mut "1/2 !",
1395            false,
1396            false,
1397            &consts,
1398            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1399        );
1400        assert_eq!(
1401            num,
1402            Some(Number::Float(Float::with_val(FLOAT_PRECISION, 0.5).into()))
1403        );
1404        let num = parse_num(
1405            &mut "10/2 !",
1406            false,
1407            false,
1408            &consts,
1409            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1410        );
1411        assert_eq!(num, Some(Number::Exact(5.into())));
1412        let num = parse_num(
1413            &mut "1.5/2 !",
1414            false,
1415            false,
1416            &consts,
1417            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1418        );
1419        assert_eq!(
1420            num,
1421            Some(Number::Float(Float::with_val(FLOAT_PRECISION, 0.75).into()))
1422        );
1423        let num = parse_num(
1424            &mut "10e10000000000/2 !",
1425            false,
1426            false,
1427            &consts,
1428            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1429        );
1430        assert_eq!(
1431            num,
1432            Some(Number::Approximate(
1433                Float::with_val(FLOAT_PRECISION, 5).into(),
1434                10000000000u64.into()
1435            ))
1436        );
1437        let num = parse_num(
1438            &mut "10/2 !",
1439            false,
1440            true,
1441            &consts,
1442            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1443        );
1444        assert_eq!(num, Some(Number::Exact(10.into())));
1445        let num = parse_num(
1446            &mut "10/2!",
1447            false,
1448            false,
1449            &consts,
1450            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1451        );
1452        assert_eq!(num, Some(Number::Exact(2.into())));
1453    }
1454    #[allow(clippy::uninlined_format_args)]
1455    #[test]
1456    fn test_biggest_num() {
1457        let consts = Consts::default();
1458        let num = parse_num(
1459            &mut format!("9e{}", recommended::INTEGER_CONSTRUCTION_LIMIT()).as_str(),
1460            true,
1461            false,
1462            &consts,
1463            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1464        );
1465        assert!(matches!(num, Some(Number::Approximate(_, _))));
1466        let num = parse_num(
1467            &mut format!("9e{}", recommended::INTEGER_CONSTRUCTION_LIMIT() - 1).as_str(),
1468            false,
1469            false,
1470            &consts,
1471            &NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }),
1472        );
1473        assert!(num.is_some());
1474    }
1475}