Skip to main content

factorion_lib/
comment.rs

1//! Parses comments and generates the reply.
2
3#[cfg(any(feature = "serde", test))]
4use serde::{Deserialize, Serialize};
5
6use crate::rug::integer::IntegerExt64;
7use crate::rug::{Complete, Integer};
8
9use crate::Consts;
10use crate::calculation_results::{Calculation, FormatOptions};
11use crate::calculation_tasks::{CalculationBase, CalculationJob};
12use crate::parse::parse;
13
14use std::fmt::Write;
15use std::ops::*;
16#[macro_export]
17macro_rules! impl_bitwise {
18    ($s_name:ident {$($s_fields:ident),*}, $t_name:ident, $fn_name:ident) => {
19        impl $t_name for $s_name {
20            type Output = Self;
21            fn $fn_name(self, rhs: Self) -> Self {
22                Self {
23                    $($s_fields: self.$s_fields.$fn_name(rhs.$s_fields),)*
24                }
25            }
26        }
27    };
28}
29#[macro_export]
30macro_rules! impl_all_bitwise {
31    ($s_name:ident {$($s_fields:ident,)*}) => {impl_all_bitwise!($s_name {$($s_fields),*});};
32    ($s_name:ident {$($s_fields:ident),*}) => {
33        impl_bitwise!($s_name {$($s_fields),*}, BitOr, bitor);
34        impl_bitwise!($s_name {$($s_fields),*}, BitXor, bitxor);
35        impl_bitwise!($s_name {$($s_fields),*}, BitAnd, bitand);
36        impl Not for $s_name {
37            type Output = Self;
38            fn not(self) -> Self {
39                Self {
40                    $($s_fields: self.$s_fields.not(),)*
41                }
42            }
43        }
44    };
45}
46
47/// The primary abstraction.
48/// Construct -> Extract -> Calculate -> Get Reply
49///
50/// Uses a generic for Metadata (meta).
51///
52/// Uses three type-states exposed as the aliases [CommentConstructed], [CommentExtracted], and [CommentCalculated].
53#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
54#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
55pub struct Comment<Meta, S> {
56    /// Metadata (generic)
57    pub meta: Meta,
58    /// Data for the current step
59    pub calculation_list: S,
60    /// If Some will prepend a "Hey {string}!" to the reply.
61    pub notify: Option<String>,
62    pub status: Status,
63    pub commands: Commands,
64    /// How long the reply may at most be
65    pub max_length: usize,
66    pub locale: String,
67}
68/// Base [Comment], contains the comment text, if it might have a calculation. Use [extract](Comment::extract).
69pub type CommentConstructed<Meta> = Comment<Meta, String>;
70/// Extracted [Comment], contains the calculations to be done. Use [calc](Comment::calc).
71pub type CommentExtracted<Meta> = Comment<Meta, Vec<CalculationJob>>;
72/// Calculated [Comment], contains the results along with how we go to them. Use [get_reply](Comment::get_reply).
73pub type CommentCalculated<Meta> = Comment<Meta, Vec<Calculation>>;
74
75#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
76#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
77#[non_exhaustive]
78pub struct Status {
79    pub already_replied_or_rejected: bool,
80    pub not_replied: bool,
81    pub number_too_big_to_calculate: bool,
82    pub no_factorial: bool,
83    pub reply_would_be_too_long: bool,
84    pub factorials_found: bool,
85    pub limit_hit: bool,
86}
87
88impl_all_bitwise!(Status {
89    already_replied_or_rejected,
90    not_replied,
91    number_too_big_to_calculate,
92    no_factorial,
93    reply_would_be_too_long,
94    factorials_found,
95    limit_hit,
96});
97#[allow(dead_code)]
98impl Status {
99    pub const NONE: Self = Self {
100        already_replied_or_rejected: false,
101        not_replied: false,
102        number_too_big_to_calculate: false,
103        no_factorial: false,
104        reply_would_be_too_long: false,
105        factorials_found: false,
106        limit_hit: false,
107    };
108    pub const ALREADY_REPLIED_OR_REJECTED: Self = Self {
109        already_replied_or_rejected: true,
110        ..Self::NONE
111    };
112    pub const NOT_REPLIED: Self = Self {
113        not_replied: true,
114        ..Self::NONE
115    };
116    pub const NUMBER_TOO_BIG_TO_CALCULATE: Self = Self {
117        number_too_big_to_calculate: true,
118        ..Self::NONE
119    };
120    pub const NO_FACTORIAL: Self = Self {
121        no_factorial: true,
122        ..Self::NONE
123    };
124    pub const REPLY_WOULD_BE_TOO_LONG: Self = Self {
125        reply_would_be_too_long: true,
126        ..Self::NONE
127    };
128    pub const FACTORIALS_FOUND: Self = Self {
129        factorials_found: true,
130        ..Self::NONE
131    };
132    pub const LIMIT_HIT: Self = Self {
133        limit_hit: true,
134        ..Self::NONE
135    };
136}
137
138#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
139#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
140#[non_exhaustive]
141pub struct Commands {
142    /// Turn all integers into scientific notiation if that makes them shorter.
143    #[cfg_attr(any(feature = "serde", test), serde(default))]
144    pub shorten: bool,
145    /// Return all the intermediate results for nested calculations.
146    #[cfg_attr(any(feature = "serde", test), serde(default))]
147    pub steps: bool,
148    /// Interpret multi-ops as nested ops.
149    #[cfg_attr(any(feature = "serde", test), serde(default))]
150    pub nested: bool,
151    /// Parse and calculate termials.
152    #[cfg_attr(any(feature = "serde", test), serde(default))]
153    pub termial: bool,
154    /// Disable the beginning note.
155    #[cfg_attr(any(feature = "serde", test), serde(default))]
156    pub no_note: bool,
157    /// Write out the number as a word if possible.
158    #[cfg_attr(any(feature = "serde", test), serde(default))]
159    pub write_out: bool,
160}
161impl_all_bitwise!(Commands {
162    shorten,
163    steps,
164    nested,
165    termial,
166    no_note,
167    write_out,
168});
169#[allow(dead_code)]
170impl Commands {
171    pub const NONE: Self = Self {
172        shorten: false,
173        steps: false,
174        nested: false,
175        termial: false,
176        no_note: false,
177        write_out: false,
178    };
179    pub const SHORTEN: Self = Self {
180        shorten: true,
181        ..Self::NONE
182    };
183    pub const STEPS: Self = Self {
184        steps: true,
185        ..Self::NONE
186    };
187    pub const NESTED: Self = Self {
188        nested: true,
189        ..Self::NONE
190    };
191    pub const TERMIAL: Self = Self {
192        termial: true,
193        ..Self::NONE
194    };
195    pub const NO_NOTE: Self = Self {
196        no_note: true,
197        ..Self::NONE
198    };
199    pub const WRITE_OUT: Self = Self {
200        write_out: true,
201        ..Self::NONE
202    };
203}
204
205impl Commands {
206    fn contains_command_format(text: &str, command: &str) -> bool {
207        let pattern1 = format!("\\[{command}\\]");
208        let pattern2 = format!("[{command}]");
209        let pattern3 = format!("!{command}");
210        text.contains(&pattern1) || text.contains(&pattern2) || text.contains(&pattern3)
211    }
212
213    pub fn from_comment_text(text: &str) -> Self {
214        Self {
215            shorten: Self::contains_command_format(text, "short")
216                || Self::contains_command_format(text, "shorten"),
217            steps: Self::contains_command_format(text, "steps")
218                || Self::contains_command_format(text, "all"),
219            nested: Self::contains_command_format(text, "nest")
220                || Self::contains_command_format(text, "nested"),
221            termial: Self::contains_command_format(text, "termial")
222                || Self::contains_command_format(text, "triangle"),
223            no_note: Self::contains_command_format(text, "no note")
224                || Self::contains_command_format(text, "no_note"),
225            write_out: Self::contains_command_format(text, "write_out")
226                || Self::contains_command_format(text, "write_num"),
227        }
228    }
229    pub fn overrides_from_comment_text(text: &str) -> Self {
230        Self {
231            shorten: !Self::contains_command_format(text, "long"),
232            steps: !(Self::contains_command_format(text, "no steps")
233                || Self::contains_command_format(text, "no_steps")),
234            nested: !(Self::contains_command_format(text, "no_nest")
235                || Self::contains_command_format(text, "multi")),
236            termial: !(Self::contains_command_format(text, "no termial")
237                || Self::contains_command_format(text, "no_termial")),
238            no_note: !Self::contains_command_format(text, "note"),
239            write_out: !(Self::contains_command_format(text, "dont_write_out")
240                || Self::contains_command_format(text, "normal_num")),
241        }
242    }
243}
244
245macro_rules! contains_comb {
246    // top level (advance both separately)
247    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
248        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end,$($end_rest),*]) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
249    };
250    // inner (advance only end)
251    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
252        $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
253    };
254    // top level (advance both separately) singular end (advance only start)
255    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
256        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
257    };
258    // top level (advance both separately) singular start (advance only end)
259    ($var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
260        $var.contains(concat!($start, $end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
261    };
262    // inner (advance only end) singular end (advance only start, so nothing)
263    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
264        $var.contains(concat!($start,$end))
265    };
266    // inner (advance only end) singular end (advance only end)
267    (@inner $var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
268        $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
269    };
270    // top level (advance both separately) singular start and end (no advance)
271    ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
272        $var.contains(concat!($start, $end))
273    };
274    // inner (advance only end) singular start and end (no advance)
275    (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
276        $var.contains(concat!($start,$end))
277    };
278}
279
280impl<Meta> CommentConstructed<Meta> {
281    /// Takes a raw comment, finds the factorials and commands, and packages it, also checks if it might have something to calculate.
282    pub fn new(
283        comment_text: &str,
284        meta: Meta,
285        pre_commands: Commands,
286        max_length: usize,
287        locale: &str,
288    ) -> Self {
289        let command_overrides = Commands::overrides_from_comment_text(comment_text);
290        let commands: Commands =
291            (Commands::from_comment_text(comment_text) | pre_commands) & command_overrides;
292
293        let mut status: Status = Default::default();
294
295        let text = if Self::might_have_factorial(comment_text) {
296            comment_text.to_owned()
297        } else {
298            status.no_factorial = true;
299            String::new()
300        };
301
302        Comment {
303            meta,
304            notify: None,
305            calculation_list: text,
306            status,
307            commands,
308            max_length,
309            locale: locale.to_owned(),
310        }
311    }
312
313    fn might_have_factorial(text: &str) -> bool {
314        contains_comb!(
315            text,
316            [
317                "0",
318                "1",
319                "2",
320                "3",
321                "4",
322                "5",
323                "6",
324                "7",
325                "8",
326                "9",
327                ")",
328                "e",
329                "pi",
330                "phi",
331                "tau",
332                "π",
333                "ɸ",
334                "τ",
335                "infinity",
336                "inf",
337                "∞\u{303}",
338                "∞"
339            ],
340            ["!", "?"]
341        ) || contains_comb!(
342            text,
343            ["!"],
344            [
345                "0",
346                "1",
347                "2",
348                "3",
349                "4",
350                "5",
351                "6",
352                "7",
353                "8",
354                "9",
355                "(",
356                "e",
357                "pi",
358                "phi",
359                "tau",
360                "π",
361                "ɸ",
362                "τ",
363                "infinity",
364                "inf",
365                "∞\u{303}",
366                "∞"
367            ]
368        )
369    }
370
371    /// Extracts the calculations using [parse](mod@crate::parse).
372    pub fn extract(self, consts: &Consts) -> CommentExtracted<Meta> {
373        let Comment {
374            meta,
375            calculation_list: comment_text,
376            notify,
377            mut status,
378            commands,
379            max_length,
380            locale,
381        } = self;
382        let mut pending_list: Vec<CalculationJob> = parse(
383            &comment_text,
384            commands.termial,
385            consts,
386            &consts
387                .locales
388                .get(&locale)
389                .unwrap_or(consts.locales.get(&consts.default_locale).unwrap())
390                .format
391                .number_format,
392        );
393
394        if commands.nested {
395            for calc in &mut pending_list {
396                Self::multi_to_nested(calc);
397            }
398        }
399
400        if pending_list.is_empty() {
401            status.no_factorial = true;
402        }
403
404        Comment {
405            meta,
406            calculation_list: pending_list,
407            notify,
408            status,
409            commands,
410            max_length,
411            locale,
412        }
413    }
414
415    fn multi_to_nested(mut calc: &mut CalculationJob) {
416        loop {
417            let level = calc.level.clamp(-1, 1);
418            let depth = calc.level.abs();
419            calc.level = level;
420            for _ in 1..depth {
421                let base = std::mem::replace(
422                    &mut calc.base,
423                    CalculationBase::Num(
424                        crate::calculation_results::CalculationResult::ComplexInfinity,
425                    ),
426                );
427                let new_base = CalculationBase::Calc(Box::new(CalculationJob {
428                    base,
429                    level,
430                    negative: 0,
431                }));
432                let _ = std::mem::replace(&mut calc.base, new_base);
433            }
434            let CalculationBase::Calc(next) = &mut calc.base else {
435                return;
436            };
437            calc = next;
438        }
439    }
440
441    /// Constructs an empty comment with [Status] already_replied_or_rejected set.
442    pub fn new_already_replied(meta: Meta, max_length: usize, locale: &str) -> Self {
443        let text = String::new();
444        let status: Status = Status {
445            already_replied_or_rejected: true,
446            ..Default::default()
447        };
448        let commands: Commands = Default::default();
449
450        Comment {
451            meta,
452            notify: None,
453            calculation_list: text,
454            status,
455            commands,
456            max_length,
457            locale: locale.to_owned(),
458        }
459    }
460}
461impl<Meta, S> Comment<Meta, S> {
462    pub fn add_status(&mut self, status: Status) {
463        self.status = self.status | status;
464    }
465}
466impl<Meta> CommentExtracted<Meta> {
467    /// Does the calculations using [calculation_tasks](crate::calculation_tasks).
468    pub fn calc(self, consts: &Consts) -> CommentCalculated<Meta> {
469        let Comment {
470            meta,
471            calculation_list: pending_list,
472            notify,
473            mut status,
474            commands,
475            max_length,
476            locale,
477        } = self;
478        let mut calculation_list: Vec<Calculation> = pending_list
479            .into_iter()
480            .flat_map(|calc| calc.execute(commands.steps, consts))
481            .filter_map(|x| {
482                if x.is_none() {
483                    status.number_too_big_to_calculate = true;
484                };
485                x
486            })
487            .collect();
488
489        calculation_list.sort();
490        calculation_list.dedup();
491        calculation_list.sort_by_key(|x| x.steps.len());
492
493        if calculation_list.is_empty() {
494            status.no_factorial = true;
495        } else {
496            status.factorials_found = true;
497        }
498        Comment {
499            meta,
500            calculation_list,
501            notify,
502            status,
503            commands,
504            max_length,
505            locale,
506        }
507    }
508}
509impl<Meta> CommentCalculated<Meta> {
510    /// Does the formatting for the reply using [calculation_result](crate::calculation_results).
511    pub fn get_reply(&self, consts: &Consts) -> String {
512        let mut fell_back = false;
513        let locale = consts.locales.get(&self.locale).unwrap_or_else(|| {
514            fell_back = true;
515            consts.locales.get(&consts.default_locale).unwrap()
516        });
517        let mut note = self
518            .notify
519            .as_ref()
520            .map(|user| locale.notes.mention.replace("{mention}", user) + "\n\n")
521            .unwrap_or_default();
522
523        if fell_back {
524            let _ = note.write_str("Sorry, I currently don't speak ");
525            let _ = note.write_str(&self.locale);
526            let _ = note.write_str(". Maybe you could [teach me](https://github.com/tolik518/factorion-bot/blob/master/CONTRIBUTING.md#translation)? \n\n");
527        }
528
529        let too_big_number = Integer::u64_pow_u64(10, self.max_length as u64).complete();
530        let too_big_number = &too_big_number;
531
532        // Add Note
533        let multiple = self.calculation_list.len() > 1;
534        if !self.commands.no_note {
535            if self.status.limit_hit {
536                let _ = note.write_str(
537                    locale
538                        .notes
539                        .limit_hit
540                        .as_ref()
541                        .map(AsRef::as_ref)
542                        .unwrap_or(
543                            "I have repeated myself enough, I won't do that calculation again.",
544                        ),
545                );
546                let _ = note.write_str("\n\n");
547            } else if self
548                .calculation_list
549                .iter()
550                .any(Calculation::is_digit_tower)
551            {
552                if multiple {
553                    let _ = note.write_str(&locale.notes.tower_mult);
554                    let _ = note.write_str("\n\n");
555                } else {
556                    let _ = note.write_str(&locale.notes.tower);
557                    let _ = note.write_str("\n\n");
558                }
559            } else if self
560                .calculation_list
561                .iter()
562                .any(Calculation::is_aproximate_digits)
563            {
564                if multiple {
565                    let _ = note.write_str(&locale.notes.digits_mult);
566                    let _ = note.write_str("\n\n");
567                } else {
568                    let _ = note.write_str(&locale.notes.digits);
569                    let _ = note.write_str("\n\n");
570                }
571            } else if self
572                .calculation_list
573                .iter()
574                .any(Calculation::is_approximate)
575            {
576                if multiple {
577                    let _ = note.write_str(&locale.notes.approx_mult);
578                    let _ = note.write_str("\n\n");
579                } else {
580                    let _ = note.write_str(&locale.notes.approx);
581                    let _ = note.write_str("\n\n");
582                }
583            } else if self.calculation_list.iter().any(Calculation::is_rounded) {
584                if multiple {
585                    let _ = note.write_str(&locale.notes.round_mult);
586                    let _ = note.write_str("\n\n");
587                } else {
588                    let _ = note.write_str(&locale.notes.round);
589                    let _ = note.write_str("\n\n");
590                }
591            } else if self
592                .calculation_list
593                .iter()
594                .any(|c| c.is_too_long(too_big_number))
595                && !(self.commands.write_out
596                    && self
597                        .calculation_list
598                        .iter()
599                        .all(|c| c.can_write_out(consts.float_precision)))
600            {
601                if multiple {
602                    let _ = note.write_str(&locale.notes.too_big_mult);
603                    let _ = note.write_str("\n\n");
604                } else {
605                    let _ = note.write_str(&locale.notes.too_big);
606                    let _ = note.write_str("\n\n");
607                }
608            } else if self.commands.write_out && self.locale != "en" {
609                let _ =
610                    note.write_str("I can only write out numbers in english, so I will do that.");
611                let _ = note.write_str("\n\n");
612            }
613        }
614
615        // Add Factorials
616        let mut reply = self
617            .calculation_list
618            .iter()
619            .fold(note.clone(), |mut acc, factorial| {
620                let _ = factorial.format(
621                    &mut acc,
622                    FormatOptions {
623                        force_shorten: self.commands.shorten,
624                        write_out: self.commands.write_out,
625                        ..FormatOptions::NONE
626                    },
627                    too_big_number,
628                    consts,
629                    &locale.format,
630                );
631                acc
632            });
633
634        // If the reply was too long try force shortening all factorials
635        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length
636            && !self.commands.shorten
637            && !self
638                .calculation_list
639                .iter()
640                .all(|fact| fact.is_too_long(too_big_number))
641        {
642            if note.is_empty() && !self.commands.no_note {
643                if multiple {
644                    let _ = note.write_str(&locale.notes.too_big_mult);
645                } else {
646                    let _ = note.write_str(&locale.notes.too_big);
647                }
648                let _ = note.write_str("\n\n");
649            };
650            reply = self
651                .calculation_list
652                .iter()
653                .fold(note, |mut acc, factorial| {
654                    let _ = factorial.format(
655                        &mut acc,
656                        FormatOptions {
657                            write_out: self.commands.write_out,
658                            ..FormatOptions::FORCE_SHORTEN
659                        },
660                        too_big_number,
661                        consts,
662                        &locale.format,
663                    );
664                    acc
665                });
666        }
667
668        let note = if !self.commands.no_note {
669            locale.notes.tetration.clone().into_owned() + "\n\n"
670        } else {
671            String::new()
672        };
673        // If the reply was too long try agressive shortening all factorials
674        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length && !self.commands.steps
675        {
676            reply = self
677                .calculation_list
678                .iter()
679                .fold(note, |mut acc, factorial| {
680                    let _ = factorial.format(
681                        &mut acc,
682                        FormatOptions {
683                            write_out: self.commands.write_out,
684                            ..{ FormatOptions::FORCE_SHORTEN | FormatOptions::AGRESSIVE_SHORTEN }
685                        },
686                        too_big_number,
687                        consts,
688                        &locale.format,
689                    );
690                    acc
691                });
692        }
693
694        let note = if !self.commands.no_note {
695            locale.notes.remove.clone().into_owned() + "\n\n"
696        } else {
697            String::new()
698        };
699        // Remove factorials until we can fit them in a comment
700        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length {
701            let mut factorial_list: Vec<String> = self
702                .calculation_list
703                .iter()
704                .map(|fact| {
705                    let mut res = String::new();
706                    let _ = fact.format(
707                        &mut res,
708                        FormatOptions {
709                            agressive_shorten: !self.commands.steps,
710                            write_out: self.commands.write_out,
711                            ..FormatOptions::FORCE_SHORTEN
712                        },
713                        too_big_number,
714                        consts,
715                        &locale.format,
716                    );
717                    res
718                })
719                .collect();
720            'drop_last: {
721                while note.len()
722                    + factorial_list.iter().map(|s| s.len()).sum::<usize>()
723                    + locale.bot_disclaimer.len()
724                    + 16
725                    > self.max_length
726                {
727                    // remove last factorial (probably the biggest)
728                    factorial_list.pop();
729                    if factorial_list.is_empty() {
730                        reply = locale.notes.no_post.to_string();
731                        break 'drop_last;
732                    }
733                }
734                reply = factorial_list.iter().fold(note, |mut acc, factorial| {
735                    let _ = acc.write_str(factorial);
736                    acc
737                });
738            }
739        }
740        if !locale.bot_disclaimer.is_empty() {
741            reply.push_str("\n*^(");
742            reply.push_str(&locale.bot_disclaimer);
743            reply.push_str(")*");
744        }
745        reply
746    }
747}
748
749#[cfg(test)]
750mod tests {
751    use crate::{
752        calculation_results::Number,
753        calculation_tasks::{CalculationBase, CalculationJob},
754        locale::NumFormat,
755    };
756
757    const MAX_LENGTH: usize = 10_000;
758
759    use super::*;
760
761    type Comment<S> = super::Comment<(), S>;
762
763    #[test]
764    fn test_extraction_dedup() {
765        let consts = Consts::default();
766        let jobs = parse(
767            "24! -24! 2!? (2!?)!",
768            true,
769            &consts,
770            &NumFormat { decimal: '.' },
771        );
772        assert_eq!(
773            jobs,
774            [
775                CalculationJob {
776                    base: CalculationBase::Num(Number::Exact(24.into())),
777                    level: 1,
778                    negative: 0
779                },
780                CalculationJob {
781                    base: CalculationBase::Num(Number::Exact(24.into())),
782                    level: 1,
783                    negative: 1
784                },
785                CalculationJob {
786                    base: CalculationBase::Calc(Box::new(CalculationJob {
787                        base: CalculationBase::Num(Number::Exact(2.into())),
788                        level: 1,
789                        negative: 0
790                    })),
791                    level: -1,
792                    negative: 0
793                },
794                CalculationJob {
795                    base: CalculationBase::Calc(Box::new(CalculationJob {
796                        base: CalculationBase::Calc(Box::new(CalculationJob {
797                            base: CalculationBase::Num(Number::Exact(2.into())),
798                            level: 1,
799                            negative: 0
800                        })),
801                        level: -1,
802                        negative: 0
803                    })),
804                    level: 1,
805                    negative: 0
806                }
807            ]
808        );
809    }
810
811    #[test]
812    fn test_commands_from_comment_text() {
813        let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note !nested");
814        assert!(cmd1.shorten);
815        assert!(cmd1.steps);
816        assert!(cmd1.termial);
817        assert!(cmd1.no_note);
818        assert!(cmd1.nested);
819        let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note] [nest]");
820        assert!(cmd2.shorten);
821        assert!(cmd2.steps);
822        assert!(cmd2.termial);
823        assert!(cmd2.no_note);
824        assert!(cmd2.nested);
825        let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\] \[nest\]";
826        let cmd3 = Commands::from_comment_text(comment);
827        assert!(cmd3.shorten);
828        assert!(cmd3.steps);
829        assert!(cmd3.termial);
830        assert!(cmd3.no_note);
831        assert!(cmd3.nested);
832        let cmd4 = Commands::from_comment_text("shorten all triangle no_note nest");
833        assert!(!cmd4.shorten);
834        assert!(!cmd4.steps);
835        assert!(!cmd4.termial);
836        assert!(!cmd4.no_note);
837        assert!(!cmd4.nested);
838    }
839
840    #[test]
841    fn test_commands_overrides_from_comment_text() {
842        let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note multi");
843        assert!(cmd1.shorten);
844        assert!(cmd1.steps);
845        assert!(cmd1.termial);
846        assert!(cmd1.no_note);
847        assert!(cmd1.nested);
848    }
849
850    #[test]
851    fn test_might_have_factorial() {
852        assert!(Comment::might_have_factorial("5!"));
853        assert!(Comment::might_have_factorial("3?"));
854        assert!(!Comment::might_have_factorial("!?"));
855    }
856
857    #[test]
858    fn test_new_already_replied() {
859        let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
860        assert_eq!(comment.calculation_list, "");
861        assert!(comment.status.already_replied_or_rejected);
862    }
863
864    #[test]
865    fn test_locale_fallback_note() {
866        let consts = Consts::default();
867        let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
868            .extract(&consts)
869            .calc(&consts);
870        let reply = comment.get_reply(&consts);
871        assert_eq!(
872            reply,
873            "Sorry, I currently don't speak n/a. Maybe you could [teach me](https://github.com/tolik518/factorion-bot/blob/master/CONTRIBUTING.md#translation)? \n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*"
874        );
875    }
876
877    #[test]
878    fn test_limit_hit_note() {
879        let consts = Consts::default();
880        let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
881            .extract(&consts)
882            .calc(&consts);
883        comment.add_status(Status::LIMIT_HIT);
884        let reply = comment.get_reply(&consts);
885        assert_eq!(
886            reply,
887            "I have repeated myself enough, I won't do that calculation again.\n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*"
888        );
889    }
890
891    #[test]
892    fn test_write_out_unsupported_note() {
893        let consts = Consts::default();
894        let comment = Comment::new("1!", (), Commands::WRITE_OUT, MAX_LENGTH, "de")
895            .extract(&consts)
896            .calc(&consts);
897        let reply = comment.get_reply(&consts);
898        assert_eq!(
899            reply,
900            "I can only write out numbers in english, so I will do that.\n\nFakultät von one ist one \n\n\n*^(Dieser Kommentar wurde automatisch geschrieben | [Quelltext](http://f.r0.fyi))*"
901        );
902    }
903}