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, Number};
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(locale.notes().limit_hit().map(AsRef::as_ref).unwrap_or(
537                    "I have repeated myself enough, I won't do that calculation again.",
538                ));
539                let _ = note.write_str("\n\n");
540            } else if self
541                .calculation_list
542                .iter()
543                .any(Calculation::is_digit_tower)
544            {
545                if multiple {
546                    let _ = note.write_str(locale.notes().tower_mult());
547                    let _ = note.write_str("\n\n");
548                } else {
549                    let _ = note.write_str(locale.notes().tower());
550                    let _ = note.write_str("\n\n");
551                }
552            } else if self
553                .calculation_list
554                .iter()
555                .any(Calculation::is_aproximate_digits)
556            {
557                if multiple {
558                    let _ = note.write_str(locale.notes().digits_mult());
559                    let _ = note.write_str("\n\n");
560                } else {
561                    let _ = note.write_str(locale.notes().digits());
562                    let _ = note.write_str("\n\n");
563                }
564            } else if self
565                .calculation_list
566                .iter()
567                .any(Calculation::is_approximate)
568            {
569                if multiple {
570                    let _ = note.write_str(locale.notes().approx_mult());
571                    let _ = note.write_str("\n\n");
572                } else {
573                    let _ = note.write_str(locale.notes().approx());
574                    let _ = note.write_str("\n\n");
575                }
576            } else if self.calculation_list.iter().any(Calculation::is_rounded) {
577                if multiple {
578                    let _ = note.write_str(locale.notes().round_mult());
579                    let _ = note.write_str("\n\n");
580                } else {
581                    let _ = note.write_str(locale.notes().round());
582                    let _ = note.write_str("\n\n");
583                }
584            } else if self
585                .calculation_list
586                .iter()
587                .any(|c| c.is_too_long(too_big_number))
588                && !(self.commands.write_out
589                    && self
590                        .calculation_list
591                        .iter()
592                        .all(|c| c.can_write_out(consts.float_precision)))
593            {
594                if multiple {
595                    let _ = note.write_str(locale.notes().too_big_mult());
596                    let _ = note.write_str("\n\n");
597                } else {
598                    let _ = note.write_str(locale.notes().too_big());
599                    let _ = note.write_str("\n\n");
600                }
601            } else if self.commands.write_out && self.locale != "en" {
602                let _ =
603                    note.write_str("I can only write out numbers in english, so I will do that.");
604                let _ = note.write_str("\n\n");
605            }
606        }
607
608        // Add Factorials
609        let mut reply = self
610            .calculation_list
611            .iter()
612            .fold(note.clone(), |mut acc, factorial| {
613                let _ = factorial.format(
614                    &mut acc,
615                    FormatOptions {
616                        force_shorten: self.commands.shorten,
617                        write_out: self.commands.write_out,
618                        ..FormatOptions::NONE
619                    },
620                    too_big_number,
621                    consts,
622                    &locale.format(),
623                );
624                acc
625            });
626
627        // If the reply was too long try force shortening all factorials
628        if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length
629            && !self.commands.shorten
630            && !self
631                .calculation_list
632                .iter()
633                .all(|fact| fact.is_too_long(too_big_number))
634        {
635            if note.is_empty() && !self.commands.no_note {
636                if multiple {
637                    let _ = note.write_str(locale.notes().too_big_mult());
638                } else {
639                    let _ = note.write_str(locale.notes().too_big());
640                }
641                let _ = note.write_str("\n\n");
642            };
643            reply = self
644                .calculation_list
645                .iter()
646                .fold(note, |mut acc, factorial| {
647                    let _ = factorial.format(
648                        &mut acc,
649                        FormatOptions {
650                            write_out: self.commands.write_out,
651                            ..FormatOptions::FORCE_SHORTEN
652                        },
653                        too_big_number,
654                        consts,
655                        &locale.format(),
656                    );
657                    acc
658                });
659        }
660
661        let note = if !self.commands.no_note {
662            locale.notes().tetration().clone().into_owned() + "\n\n"
663        } else {
664            String::new()
665        };
666        // If the reply was too long try agressive shortening all factorials
667        if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length
668            && !self.commands.steps
669        {
670            reply = self
671                .calculation_list
672                .iter()
673                .fold(note, |mut acc, factorial| {
674                    let _ = factorial.format(
675                        &mut acc,
676                        FormatOptions {
677                            write_out: self.commands.write_out,
678                            ..{ FormatOptions::FORCE_SHORTEN | FormatOptions::AGRESSIVE_SHORTEN }
679                        },
680                        too_big_number,
681                        consts,
682                        &locale.format(),
683                    );
684                    acc
685                });
686        }
687
688        let note = if !self.commands.no_note {
689            locale.notes().remove().clone().into_owned() + "\n\n"
690        } else {
691            String::new()
692        };
693        // Remove factorials until we can fit them in a comment
694        if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length {
695            let mut factorial_list: Vec<String> = self
696                .calculation_list
697                .iter()
698                .map(|fact| {
699                    let mut res = String::new();
700                    let _ = fact.format(
701                        &mut res,
702                        FormatOptions {
703                            agressive_shorten: !self.commands.steps,
704                            write_out: self.commands.write_out,
705                            ..FormatOptions::FORCE_SHORTEN
706                        },
707                        too_big_number,
708                        consts,
709                        &locale.format(),
710                    );
711                    res
712                })
713                .collect();
714            'drop_last: {
715                while note.len()
716                    + factorial_list.iter().map(|s| s.len()).sum::<usize>()
717                    + locale.bot_disclaimer().len()
718                    + 16
719                    > self.max_length
720                {
721                    // remove last factorial (probably the biggest)
722                    factorial_list.pop();
723                    if factorial_list.is_empty() {
724                        reply = locale.notes().no_post().to_string();
725                        break 'drop_last;
726                    }
727                }
728                reply = factorial_list.iter().fold(note, |mut acc, factorial| {
729                    let _ = acc.write_str(&factorial);
730                    acc
731                });
732            }
733        }
734        if !locale.bot_disclaimer().is_empty() {
735            reply.push_str("\n*^(");
736            reply.push_str(locale.bot_disclaimer());
737            reply.push_str(")*");
738        }
739        reply
740    }
741}
742
743#[cfg(test)]
744mod tests {
745    use crate::{
746        calculation_results::Number,
747        calculation_tasks::{CalculationBase, CalculationJob},
748        locale::NumFormat,
749    };
750
751    const MAX_LENGTH: usize = 10_000;
752
753    use super::*;
754
755    type Comment<S> = super::Comment<(), S>;
756
757    #[test]
758    fn test_extraction_dedup() {
759        let consts = Consts::default();
760        let jobs = parse(
761            "24! -24! 2!? (2!?)!",
762            true,
763            &consts,
764            &NumFormat::V1(&crate::locale::v1::NumFormat { decimal: '.' }),
765        );
766        assert_eq!(
767            jobs,
768            [
769                CalculationJob {
770                    base: CalculationBase::Num(Number::Exact(24.into())),
771                    level: 1,
772                    negative: 0
773                },
774                CalculationJob {
775                    base: CalculationBase::Num(Number::Exact(24.into())),
776                    level: 1,
777                    negative: 1
778                },
779                CalculationJob {
780                    base: CalculationBase::Calc(Box::new(CalculationJob {
781                        base: CalculationBase::Num(Number::Exact(2.into())),
782                        level: 1,
783                        negative: 0
784                    })),
785                    level: -1,
786                    negative: 0
787                },
788                CalculationJob {
789                    base: CalculationBase::Calc(Box::new(CalculationJob {
790                        base: CalculationBase::Calc(Box::new(CalculationJob {
791                            base: CalculationBase::Num(Number::Exact(2.into())),
792                            level: 1,
793                            negative: 0
794                        })),
795                        level: -1,
796                        negative: 0
797                    })),
798                    level: 1,
799                    negative: 0
800                }
801            ]
802        );
803    }
804
805    #[test]
806    fn test_commands_from_comment_text() {
807        let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note !nested");
808        assert!(cmd1.shorten);
809        assert!(cmd1.steps);
810        assert!(cmd1.termial);
811        assert!(cmd1.no_note);
812        assert!(cmd1.nested);
813        let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note] [nest]");
814        assert!(cmd2.shorten);
815        assert!(cmd2.steps);
816        assert!(cmd2.termial);
817        assert!(cmd2.no_note);
818        assert!(cmd2.nested);
819        let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\] \[nest\]";
820        let cmd3 = Commands::from_comment_text(comment);
821        assert!(cmd3.shorten);
822        assert!(cmd3.steps);
823        assert!(cmd3.termial);
824        assert!(cmd3.no_note);
825        assert!(cmd3.nested);
826        let cmd4 = Commands::from_comment_text("shorten all triangle no_note nest");
827        assert!(!cmd4.shorten);
828        assert!(!cmd4.steps);
829        assert!(!cmd4.termial);
830        assert!(!cmd4.no_note);
831        assert!(!cmd4.nested);
832    }
833
834    #[test]
835    fn test_commands_overrides_from_comment_text() {
836        let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note multi");
837        assert!(cmd1.shorten);
838        assert!(cmd1.steps);
839        assert!(cmd1.termial);
840        assert!(cmd1.no_note);
841        assert!(cmd1.nested);
842    }
843
844    #[test]
845    fn test_might_have_factorial() {
846        assert!(Comment::might_have_factorial("5!"));
847        assert!(Comment::might_have_factorial("3?"));
848        assert!(!Comment::might_have_factorial("!?"));
849    }
850
851    #[test]
852    fn test_new_already_replied() {
853        let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
854        assert_eq!(comment.calculation_list, "");
855        assert!(comment.status.already_replied_or_rejected);
856    }
857
858    #[test]
859    fn test_locale_fallback_note() {
860        let consts = Consts::default();
861        let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
862            .extract(&consts)
863            .calc(&consts);
864        let reply = comment.get_reply(&consts);
865        assert_eq!(
866            reply,
867            "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))*"
868        );
869    }
870
871    #[test]
872    fn test_limit_hit_note() {
873        let consts = Consts::default();
874        let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
875            .extract(&consts)
876            .calc(&consts);
877        comment.add_status(Status::LIMIT_HIT);
878        let reply = comment.get_reply(&consts);
879        assert_eq!(
880            reply,
881            "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))*"
882        );
883    }
884
885    #[test]
886    fn test_write_out_unsupported_note() {
887        let consts = Consts::default();
888        let comment = Comment::new("1!", (), Commands::WRITE_OUT, MAX_LENGTH, "de")
889            .extract(&consts)
890            .calc(&consts);
891        let reply = comment.get_reply(&consts);
892        assert_eq!(
893            reply,
894            "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))*"
895        );
896    }
897}