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", "1", "2", "3", "4", "5", "6", "7", "8", "9", ")", "e", "pi", "phi", "tau",
303                "π", "ɸ", "τ"
304            ],
305            ["!", "?"]
306        ) || contains_comb!(
307            text,
308            ["!"],
309            [
310                "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "(", "e", "pi", "phi", "tau",
311                "π", "ɸ", "τ"
312            ]
313        )
314    }
315
316    /// Extracts the calculations using [parse](mod@crate::parse).
317    pub fn extract(self, consts: &Consts) -> CommentExtracted<Meta> {
318        let Comment {
319            meta,
320            calculation_list: comment_text,
321            notify,
322            mut status,
323            commands,
324            max_length,
325            locale,
326        } = self;
327        let mut pending_list: Vec<CalculationJob> = parse(
328            &comment_text,
329            commands.termial,
330            consts,
331            &consts
332                .locales
333                .get(&locale)
334                .unwrap_or(consts.locales.get(&consts.default_locale).unwrap())
335                .format()
336                .number_format(),
337        );
338
339        if commands.nested {
340            for calc in &mut pending_list {
341                Self::multi_to_nested(calc);
342            }
343        }
344
345        if pending_list.is_empty() {
346            status.no_factorial = true;
347        }
348
349        Comment {
350            meta,
351            calculation_list: pending_list,
352            notify,
353            status,
354            commands,
355            max_length,
356            locale,
357        }
358    }
359
360    fn multi_to_nested(mut calc: &mut CalculationJob) {
361        loop {
362            let level = calc.level.clamp(-1, 1);
363            let depth = calc.level.abs();
364            calc.level = level;
365            for _ in 1..depth {
366                let base = std::mem::replace(
367                    &mut calc.base,
368                    CalculationBase::Num(
369                        crate::calculation_results::CalculationResult::ComplexInfinity,
370                    ),
371                );
372                let new_base = CalculationBase::Calc(Box::new(CalculationJob {
373                    base,
374                    level,
375                    negative: 0,
376                }));
377                let _ = std::mem::replace(&mut calc.base, new_base);
378            }
379            let CalculationBase::Calc(next) = &mut calc.base else {
380                return;
381            };
382            calc = next;
383        }
384    }
385
386    /// Constructs an empty comment with [Status] already_replied_or_rejected set.
387    pub fn new_already_replied(meta: Meta, max_length: usize, locale: &str) -> Self {
388        let text = String::new();
389        let status: Status = Status {
390            already_replied_or_rejected: true,
391            ..Default::default()
392        };
393        let commands: Commands = Default::default();
394
395        Comment {
396            meta,
397            notify: None,
398            calculation_list: text,
399            status,
400            commands,
401            max_length,
402            locale: locale.to_owned(),
403        }
404    }
405}
406impl<Meta, S> Comment<Meta, S> {
407    pub fn add_status(&mut self, status: Status) {
408        self.status = self.status | status;
409    }
410}
411impl<Meta> CommentExtracted<Meta> {
412    /// Does the calculations using [calculation_tasks](crate::calculation_tasks).
413    pub fn calc(self, consts: &Consts) -> CommentCalculated<Meta> {
414        let Comment {
415            meta,
416            calculation_list: pending_list,
417            notify,
418            mut status,
419            commands,
420            max_length,
421            locale,
422        } = self;
423        let mut calculation_list: Vec<Calculation> = pending_list
424            .into_iter()
425            .flat_map(|calc| calc.execute(commands.steps, consts))
426            .filter_map(|x| {
427                if x.is_none() {
428                    status.number_too_big_to_calculate = true;
429                };
430                x
431            })
432            .collect();
433
434        calculation_list.sort();
435        calculation_list.dedup();
436        calculation_list.sort_by_key(|x| x.steps.len());
437
438        if calculation_list.is_empty() {
439            status.no_factorial = true;
440        } else {
441            status.factorials_found = true;
442        }
443        Comment {
444            meta,
445            calculation_list,
446            notify,
447            status,
448            commands,
449            max_length,
450            locale,
451        }
452    }
453}
454impl<Meta> CommentCalculated<Meta> {
455    /// Does the formatting for the reply using [calculation_result](crate::calculation_results).
456    pub fn get_reply(&self, consts: &Consts) -> String {
457        let mut fell_back = false;
458        let locale = consts.locales.get(&self.locale).unwrap_or_else(|| {
459            fell_back = true;
460            consts.locales.get(&consts.default_locale).unwrap()
461        });
462        let mut note = self
463            .notify
464            .as_ref()
465            .map(|user| locale.notes().mention().replace("{mention}", user) + "\n\n")
466            .unwrap_or_default();
467
468        if fell_back {
469            let _ = note.write_str("Sorry, I currently don't speak ");
470            let _ = note.write_str(&self.locale);
471            let _ = note.write_str(". Maybe you could [teach me](https://github.com/tolik518/factorion-bot/blob/master/CONTRIBUTING.md#translation)? \n\n");
472        }
473
474        let too_big_number = Integer::u64_pow_u64(10, self.max_length as u64).complete();
475        let too_big_number = &too_big_number;
476
477        // Add Note
478        let multiple = self.calculation_list.len() > 1;
479        if !self.commands.no_note {
480            if self.status.limit_hit {
481                let _ = note.write_str(locale.notes().limit_hit().map(AsRef::as_ref).unwrap_or(
482                    "I have repeated myself enough, I won't do that calculation again.",
483                ));
484                let _ = note.write_str("\n\n");
485            } else if self
486                .calculation_list
487                .iter()
488                .any(Calculation::is_digit_tower)
489            {
490                if multiple {
491                    let _ = note.write_str(locale.notes().tower_mult());
492                    let _ = note.write_str("\n\n");
493                } else {
494                    let _ = note.write_str(locale.notes().tower());
495                    let _ = note.write_str("\n\n");
496                }
497            } else if self
498                .calculation_list
499                .iter()
500                .any(Calculation::is_aproximate_digits)
501            {
502                if multiple {
503                    let _ = note.write_str(locale.notes().digits_mult());
504                    let _ = note.write_str("\n\n");
505                } else {
506                    let _ = note.write_str(locale.notes().digits());
507                    let _ = note.write_str("\n\n");
508                }
509            } else if self
510                .calculation_list
511                .iter()
512                .any(Calculation::is_approximate)
513            {
514                if multiple {
515                    let _ = note.write_str(locale.notes().approx_mult());
516                    let _ = note.write_str("\n\n");
517                } else {
518                    let _ = note.write_str(locale.notes().approx());
519                    let _ = note.write_str("\n\n");
520                }
521            } else if self.calculation_list.iter().any(Calculation::is_rounded) {
522                if multiple {
523                    let _ = note.write_str(locale.notes().round_mult());
524                    let _ = note.write_str("\n\n");
525                } else {
526                    let _ = note.write_str(locale.notes().round());
527                    let _ = note.write_str("\n\n");
528                }
529            } else if self
530                .calculation_list
531                .iter()
532                .any(|c| c.is_too_long(too_big_number))
533            {
534                if multiple {
535                    let _ = note.write_str(locale.notes().too_big_mult());
536                    let _ = note.write_str("\n\n");
537                } else {
538                    let _ = note.write_str(locale.notes().too_big());
539                    let _ = note.write_str("\n\n");
540                }
541            }
542        }
543
544        // Add Factorials
545        let mut reply = self
546            .calculation_list
547            .iter()
548            .fold(note.clone(), |mut acc, factorial| {
549                let _ = factorial.format(
550                    &mut acc,
551                    self.commands.shorten,
552                    false,
553                    too_big_number,
554                    consts,
555                    &locale.format(),
556                );
557                acc
558            });
559
560        // If the reply was too long try force shortening all factorials
561        if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length
562            && !self.commands.shorten
563            && !self
564                .calculation_list
565                .iter()
566                .all(|fact| fact.is_too_long(too_big_number))
567        {
568            if note.is_empty() && !self.commands.no_note {
569                if multiple {
570                    let _ = note.write_str(locale.notes().too_big_mult());
571                } else {
572                    let _ = note.write_str(locale.notes().too_big());
573                }
574                let _ = note.write_str("\n\n");
575            };
576            reply = self
577                .calculation_list
578                .iter()
579                .fold(note, |mut acc, factorial| {
580                    let _ = factorial.format(
581                        &mut acc,
582                        true,
583                        false,
584                        too_big_number,
585                        consts,
586                        &locale.format(),
587                    );
588                    acc
589                });
590        }
591
592        // Remove factorials until we can fit them in a comment
593        if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length {
594            let note = locale.notes().remove().clone().into_owned() + "\n\n";
595            let mut factorial_list: Vec<String> = self
596                .calculation_list
597                .iter()
598                .map(|fact| {
599                    let mut res = String::new();
600                    let _ = fact.format(
601                        &mut res,
602                        true,
603                        false,
604                        too_big_number,
605                        consts,
606                        &locale.format(),
607                    );
608                    res
609                })
610                .collect();
611            'drop_last: {
612                while note.len()
613                    + factorial_list.iter().map(|s| s.len()).sum::<usize>()
614                    + locale.bot_disclaimer().len()
615                    + 16
616                    > self.max_length
617                {
618                    // remove last factorial (probably the biggest)
619                    factorial_list.pop();
620                    if factorial_list.is_empty() {
621                        if self.calculation_list.len() == 1 {
622                            let note = locale.notes().tetration().clone().into_owned() + "\n\n";
623                            reply =
624                                self.calculation_list
625                                    .iter()
626                                    .fold(note, |mut acc, factorial| {
627                                        let _ = factorial.format(
628                                            &mut acc,
629                                            true,
630                                            true,
631                                            too_big_number,
632                                            consts,
633                                            &locale.format(),
634                                        );
635                                        acc
636                                    });
637                            if reply.len() <= self.max_length {
638                                break 'drop_last;
639                            }
640                        }
641                        reply = locale.notes().no_post().to_string();
642                        break 'drop_last;
643                    }
644                }
645                reply = factorial_list
646                    .iter()
647                    .fold(note, |acc, factorial| format!("{acc}{factorial}"));
648            }
649        }
650        if !locale.bot_disclaimer().is_empty() {
651            reply.push_str("\n*^(");
652            reply.push_str(locale.bot_disclaimer());
653            reply.push_str(")*");
654        }
655        reply
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use crate::{
662        calculation_results::Number,
663        calculation_tasks::{CalculationBase, CalculationJob},
664        locale::NumFormat,
665    };
666
667    const MAX_LENGTH: usize = 10_000;
668
669    use super::*;
670
671    type Comment<S> = super::Comment<(), S>;
672
673    #[test]
674    fn test_extraction_dedup() {
675        let consts = Consts::default();
676        let jobs = parse(
677            "24! -24! 2!? (2!?)!",
678            true,
679            &consts,
680            &NumFormat::V1(&crate::locale::v1::NumFormat { decimal: '.' }),
681        );
682        assert_eq!(
683            jobs,
684            [
685                CalculationJob {
686                    base: CalculationBase::Num(Number::Exact(24.into())),
687                    level: 1,
688                    negative: 0
689                },
690                CalculationJob {
691                    base: CalculationBase::Num(Number::Exact(24.into())),
692                    level: 1,
693                    negative: 1
694                },
695                CalculationJob {
696                    base: CalculationBase::Calc(Box::new(CalculationJob {
697                        base: CalculationBase::Num(Number::Exact(2.into())),
698                        level: 1,
699                        negative: 0
700                    })),
701                    level: -1,
702                    negative: 0
703                },
704                CalculationJob {
705                    base: CalculationBase::Calc(Box::new(CalculationJob {
706                        base: CalculationBase::Calc(Box::new(CalculationJob {
707                            base: CalculationBase::Num(Number::Exact(2.into())),
708                            level: 1,
709                            negative: 0
710                        })),
711                        level: -1,
712                        negative: 0
713                    })),
714                    level: 1,
715                    negative: 0
716                }
717            ]
718        );
719    }
720
721    #[test]
722    fn test_commands_from_comment_text() {
723        let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note !nested");
724        assert!(cmd1.shorten);
725        assert!(cmd1.steps);
726        assert!(cmd1.termial);
727        assert!(cmd1.no_note);
728        assert!(cmd1.nested);
729        let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note] [nest]");
730        assert!(cmd2.shorten);
731        assert!(cmd2.steps);
732        assert!(cmd2.termial);
733        assert!(cmd2.no_note);
734        assert!(cmd2.nested);
735        let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\] \[nest\]";
736        let cmd3 = Commands::from_comment_text(comment);
737        assert!(cmd3.shorten);
738        assert!(cmd3.steps);
739        assert!(cmd3.termial);
740        assert!(cmd3.no_note);
741        assert!(cmd3.nested);
742        let cmd4 = Commands::from_comment_text("shorten all triangle no_note nest");
743        assert!(!cmd4.shorten);
744        assert!(!cmd4.steps);
745        assert!(!cmd4.termial);
746        assert!(!cmd4.no_note);
747        assert!(!cmd4.nested);
748    }
749
750    #[test]
751    fn test_commands_overrides_from_comment_text() {
752        let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note multi");
753        assert!(cmd1.shorten);
754        assert!(cmd1.steps);
755        assert!(cmd1.termial);
756        assert!(cmd1.no_note);
757        assert!(cmd1.nested);
758    }
759
760    #[test]
761    fn test_might_have_factorial() {
762        assert!(Comment::might_have_factorial("5!"));
763        assert!(Comment::might_have_factorial("3?"));
764        assert!(!Comment::might_have_factorial("!?"));
765    }
766
767    #[test]
768    fn test_new_already_replied() {
769        let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
770        assert_eq!(comment.calculation_list, "");
771        assert!(comment.status.already_replied_or_rejected);
772    }
773
774    #[test]
775    fn test_locale_fallback_note() {
776        let consts = Consts::default();
777        let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
778            .extract(&consts)
779            .calc(&consts);
780        let reply = comment.get_reply(&consts);
781        assert_eq!(
782            reply,
783            "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))*"
784        );
785    }
786
787    #[test]
788    fn test_limit_hit_note() {
789        let consts = Consts::default();
790        let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
791            .extract(&consts)
792            .calc(&consts);
793        comment.add_status(Status::LIMIT_HIT);
794        let reply = comment.get_reply(&consts);
795        assert_eq!(
796            reply,
797            "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))*"
798        );
799    }
800}