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