factorion_lib/
parse.rs

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