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::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    pub shorten: bool,
142    /// Return all the intermediate results for nested calculations.
143    pub steps: bool,
144    /// Parse and calculate termials.
145    pub termial: bool,
146    /// Disable the beginning note.
147    pub no_note: bool,
148    pub post_only: bool,
149}
150impl_all_bitwise!(Commands {
151    shorten,
152    steps,
153    termial,
154    no_note,
155    post_only,
156});
157#[allow(dead_code)]
158impl Commands {
159    pub const NONE: Self = Self {
160        shorten: false,
161        steps: false,
162        termial: false,
163        no_note: false,
164        post_only: false,
165    };
166    pub const SHORTEN: Self = Self {
167        shorten: true,
168        ..Self::NONE
169    };
170    pub const STEPS: Self = Self {
171        steps: true,
172        ..Self::NONE
173    };
174    pub const TERMIAL: Self = Self {
175        termial: true,
176        ..Self::NONE
177    };
178    pub const NO_NOTE: Self = Self {
179        no_note: true,
180        ..Self::NONE
181    };
182    pub const POST_ONLY: Self = Self {
183        post_only: true,
184        ..Self::NONE
185    };
186}
187
188impl Commands {
189    fn contains_command_format(text: &str, command: &str) -> bool {
190        let pattern1 = format!("\\[{command}\\]");
191        let pattern2 = format!("[{command}]");
192        let pattern3 = format!("!{command}");
193        text.contains(&pattern1) || text.contains(&pattern2) || text.contains(&pattern3)
194    }
195
196    pub fn from_comment_text(text: &str) -> Self {
197        Self {
198            shorten: Self::contains_command_format(text, "short")
199                || Self::contains_command_format(text, "shorten"),
200            steps: Self::contains_command_format(text, "steps")
201                || Self::contains_command_format(text, "all"),
202            termial: Self::contains_command_format(text, "termial")
203                || Self::contains_command_format(text, "triangle"),
204            no_note: Self::contains_command_format(text, "no note")
205                || Self::contains_command_format(text, "no_note"),
206            post_only: false,
207        }
208    }
209    pub fn overrides_from_comment_text(text: &str) -> Self {
210        Self {
211            shorten: !Self::contains_command_format(text, "long"),
212            steps: !(Self::contains_command_format(text, "no steps")
213                | Self::contains_command_format(text, "no_steps")),
214            termial: !(Self::contains_command_format(text, "no termial")
215                | Self::contains_command_format(text, "no_termial")),
216            no_note: !Self::contains_command_format(text, "note"),
217            post_only: true,
218        }
219    }
220}
221
222macro_rules! contains_comb {
223    // top level (advance both separately)
224    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
225        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end,$($end_rest),*]) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
226    };
227    // inner (advance only end)
228    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
229        $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
230    };
231    // top level (advance both separately) singular end (advance only start)
232    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
233        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
234    };
235    // top level (advance both separately) singular start (advance only end)
236    ($var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
237        $var.contains(concat!($start, $end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
238    };
239    // inner (advance only end) singular end (advance only start, so nothing)
240    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
241        $var.contains(concat!($start,$end))
242    };
243    // inner (advance only end) singular end (advance only end)
244    (@inner $var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
245        $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
246    };
247    // top level (advance both separately) singular start and end (no advance)
248    ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
249        $var.contains(concat!($start, $end))
250    };
251    // inner (advance only end) singular start and end (no advance)
252    (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
253        $var.contains(concat!($start,$end))
254    };
255}
256
257impl<Meta> CommentConstructed<Meta> {
258    /// Takes a raw comment, finds the factorials and commands, and packages it, also checks if it might have something to calculate.
259    pub fn new(
260        comment_text: &str,
261        meta: Meta,
262        pre_commands: Commands,
263        max_length: usize,
264        locale: &str,
265    ) -> Self {
266        let command_overrides = Commands::overrides_from_comment_text(comment_text);
267        let commands: Commands =
268            (Commands::from_comment_text(comment_text) | pre_commands) & command_overrides;
269
270        let mut status: Status = Default::default();
271
272        let text = if Self::might_have_factorial(comment_text) {
273            comment_text.to_owned()
274        } else {
275            status.no_factorial = true;
276            String::new()
277        };
278
279        Comment {
280            meta,
281            notify: None,
282            calculation_list: text,
283            status,
284            commands,
285            max_length,
286            locale: locale.to_owned(),
287        }
288    }
289
290    fn might_have_factorial(text: &str) -> bool {
291        contains_comb!(
292            text,
293            [
294                "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ")", "e", "pi", "phi", "tau",
295                "π", "ɸ", "τ"
296            ],
297            ["!", "?"]
298        ) || contains_comb!(
299            text,
300            ["!"],
301            [
302                "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "(", "e", "pi", "phi", "tau",
303                "π", "ɸ", "τ"
304            ]
305        )
306    }
307
308    /// Extracts the calculations using [parse](mod@crate::parse).
309    pub fn extract(self, consts: &Consts) -> CommentExtracted<Meta> {
310        let Comment {
311            meta,
312            calculation_list: comment_text,
313            notify,
314            mut status,
315            commands,
316            max_length,
317            locale,
318        } = self;
319        let pending_list: Vec<CalculationJob> = parse(
320            &comment_text,
321            commands.termial,
322            consts,
323            &consts
324                .locales
325                .get(&locale)
326                .unwrap_or(consts.locales.get(&consts.default_locale).unwrap())
327                .format()
328                .number_format(),
329        );
330
331        if pending_list.is_empty() {
332            status.no_factorial = true;
333        }
334
335        Comment {
336            meta,
337            calculation_list: pending_list,
338            notify,
339            status,
340            commands,
341            max_length,
342            locale,
343        }
344    }
345
346    /// Constructs an empty comment with [Status] already_replied_or_rejected set.
347    pub fn new_already_replied(meta: Meta, max_length: usize, locale: &str) -> Self {
348        let text = String::new();
349        let status: Status = Status {
350            already_replied_or_rejected: true,
351            ..Default::default()
352        };
353        let commands: Commands = Default::default();
354
355        Comment {
356            meta,
357            notify: None,
358            calculation_list: text,
359            status,
360            commands,
361            max_length,
362            locale: locale.to_owned(),
363        }
364    }
365}
366impl<Meta, S> Comment<Meta, S> {
367    pub fn add_status(&mut self, status: Status) {
368        self.status = self.status | status;
369    }
370}
371impl<Meta> CommentExtracted<Meta> {
372    /// Does the calculations using [calculation_tasks](crate::calculation_tasks).
373    pub fn calc(self, consts: &Consts) -> CommentCalculated<Meta> {
374        let Comment {
375            meta,
376            calculation_list: pending_list,
377            notify,
378            mut status,
379            commands,
380            max_length,
381            locale,
382        } = self;
383        let mut calculation_list: Vec<Calculation> = pending_list
384            .into_iter()
385            .flat_map(|calc| calc.execute(commands.steps, consts))
386            .filter_map(|x| {
387                if x.is_none() {
388                    status.number_too_big_to_calculate = true;
389                };
390                x
391            })
392            .collect();
393
394        calculation_list.sort();
395        calculation_list.dedup();
396        calculation_list.sort_by_key(|x| x.steps.len());
397
398        if calculation_list.is_empty() {
399            status.no_factorial = true;
400        } else {
401            status.factorials_found = true;
402        }
403        Comment {
404            meta,
405            calculation_list,
406            notify,
407            status,
408            commands,
409            max_length,
410            locale,
411        }
412    }
413}
414impl<Meta> CommentCalculated<Meta> {
415    /// Does the formatting for the reply using [calculation_result](crate::calculation_results).
416    pub fn get_reply(&self, consts: &Consts) -> String {
417        let mut fell_back = false;
418        let locale = consts.locales.get(&self.locale).unwrap_or_else(|| {
419            fell_back = true;
420            consts.locales.get(&consts.default_locale).unwrap()
421        });
422        let mut note = self
423            .notify
424            .as_ref()
425            .map(|user| locale.notes().mention().replace("{mention}", user) + "\n\n")
426            .unwrap_or_default();
427
428        if fell_back {
429            let _ = note.write_str("Sorry, I currently don't speak ");
430            let _ = note.write_str(&self.locale);
431            let _ = note.write_str(". Maybe you could [teach me](https://github.com/tolik518/factorion-bot/blob/master/CONTRIBUTING.md#translation)? \n\n");
432        }
433
434        let too_big_number = Integer::u64_pow_u64(10, self.max_length as u64).complete();
435        let too_big_number = &too_big_number;
436
437        // Add Note
438        let multiple = self.calculation_list.len() > 1;
439        if !self.commands.no_note {
440            if self.status.limit_hit {
441                let _ = note.write_str(locale.notes().limit_hit().map(AsRef::as_ref).unwrap_or(
442                    "I have repeated myself enough, I won't do that calculation again.",
443                ));
444                let _ = note.write_str("\n\n");
445            } else if self
446                .calculation_list
447                .iter()
448                .any(Calculation::is_digit_tower)
449            {
450                if multiple {
451                    let _ = note.write_str(locale.notes().tower_mult());
452                    let _ = note.write_str("\n\n");
453                } else {
454                    let _ = note.write_str(locale.notes().tower());
455                    let _ = note.write_str("\n\n");
456                }
457            } else if self
458                .calculation_list
459                .iter()
460                .any(Calculation::is_aproximate_digits)
461            {
462                if multiple {
463                    let _ = note.write_str(locale.notes().digits_mult());
464                    let _ = note.write_str("\n\n");
465                } else {
466                    let _ = note.write_str(locale.notes().digits());
467                    let _ = note.write_str("\n\n");
468                }
469            } else if self
470                .calculation_list
471                .iter()
472                .any(Calculation::is_approximate)
473            {
474                if multiple {
475                    let _ = note.write_str(locale.notes().approx_mult());
476                    let _ = note.write_str("\n\n");
477                } else {
478                    let _ = note.write_str(locale.notes().approx());
479                    let _ = note.write_str("\n\n");
480                }
481            } else if self.calculation_list.iter().any(Calculation::is_rounded) {
482                if multiple {
483                    let _ = note.write_str(locale.notes().round_mult());
484                    let _ = note.write_str("\n\n");
485                } else {
486                    let _ = note.write_str(locale.notes().round());
487                    let _ = note.write_str("\n\n");
488                }
489            } else if self
490                .calculation_list
491                .iter()
492                .any(|c| c.is_too_long(too_big_number))
493            {
494                if multiple {
495                    let _ = note.write_str(locale.notes().too_big_mult());
496                    let _ = note.write_str("\n\n");
497                } else {
498                    let _ = note.write_str(locale.notes().too_big());
499                    let _ = note.write_str("\n\n");
500                }
501            }
502        }
503
504        // Add Factorials
505        let mut reply = self
506            .calculation_list
507            .iter()
508            .fold(note.clone(), |mut acc, factorial| {
509                let _ = factorial.format(
510                    &mut acc,
511                    self.commands.shorten,
512                    false,
513                    too_big_number,
514                    consts,
515                    &locale.format(),
516                );
517                acc
518            });
519
520        // If the reply was too long try force shortening all factorials
521        if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length
522            && !self.commands.shorten
523            && !self
524                .calculation_list
525                .iter()
526                .all(|fact| fact.is_too_long(too_big_number))
527        {
528            if note.is_empty() && !self.commands.no_note {
529                let _ = note.write_str(locale.notes().remove());
530            };
531            reply = self
532                .calculation_list
533                .iter()
534                .fold(note, |mut acc, factorial| {
535                    let _ = factorial.format(
536                        &mut acc,
537                        true,
538                        false,
539                        too_big_number,
540                        consts,
541                        &locale.format(),
542                    );
543                    acc
544                });
545        }
546
547        // Remove factorials until we can fit them in a comment
548        if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length {
549            let note = locale.notes().remove().clone().into_owned() + "\n\n";
550            let mut factorial_list: Vec<String> = self
551                .calculation_list
552                .iter()
553                .map(|fact| {
554                    let mut res = String::new();
555                    let _ = fact.format(
556                        &mut res,
557                        true,
558                        false,
559                        too_big_number,
560                        consts,
561                        &locale.format(),
562                    );
563                    res
564                })
565                .collect();
566            'drop_last: {
567                while note.len()
568                    + factorial_list.iter().map(|s| s.len()).sum::<usize>()
569                    + locale.bot_disclaimer().len()
570                    + 16
571                    > self.max_length
572                {
573                    // remove last factorial (probably the biggest)
574                    factorial_list.pop();
575                    if factorial_list.is_empty() {
576                        if self.calculation_list.len() == 1 {
577                            let note = locale.notes().tetration().clone().into_owned() + "\n\n";
578                            reply =
579                                self.calculation_list
580                                    .iter()
581                                    .fold(note, |mut acc, factorial| {
582                                        let _ = factorial.format(
583                                            &mut acc,
584                                            true,
585                                            true,
586                                            too_big_number,
587                                            consts,
588                                            &locale.format(),
589                                        );
590                                        acc
591                                    });
592                            if reply.len() <= self.max_length {
593                                break 'drop_last;
594                            }
595                        }
596                        reply = locale.notes().no_post().to_string();
597                        break 'drop_last;
598                    }
599                }
600                reply = factorial_list
601                    .iter()
602                    .fold(note, |acc, factorial| format!("{acc}{factorial}"));
603            }
604        }
605        if !locale.bot_disclaimer().is_empty() {
606            reply.push_str("\n*^(");
607            reply.push_str(locale.bot_disclaimer());
608            reply.push_str(")*");
609        }
610        reply
611    }
612}
613
614#[cfg(test)]
615mod tests {
616    use crate::{
617        calculation_results::Number,
618        calculation_tasks::{CalculationBase, CalculationJob},
619        locale::NumFormat,
620    };
621
622    const MAX_LENGTH: usize = 10_000;
623
624    use super::*;
625
626    type Comment<S> = super::Comment<(), S>;
627
628    #[test]
629    fn test_extraction_dedup() {
630        let consts = Consts::default();
631        let jobs = parse(
632            "24! -24! 2!? (2!?)!",
633            true,
634            &consts,
635            &NumFormat::V1(&crate::locale::v1::NumFormat { decimal: '.' }),
636        );
637        assert_eq!(
638            jobs,
639            [
640                CalculationJob {
641                    base: CalculationBase::Num(Number::Exact(24.into())),
642                    level: 1,
643                    negative: 0
644                },
645                CalculationJob {
646                    base: CalculationBase::Num(Number::Exact(24.into())),
647                    level: 1,
648                    negative: 1
649                },
650                CalculationJob {
651                    base: CalculationBase::Calc(Box::new(CalculationJob {
652                        base: CalculationBase::Num(Number::Exact(2.into())),
653                        level: 1,
654                        negative: 0
655                    })),
656                    level: -1,
657                    negative: 0
658                },
659                CalculationJob {
660                    base: CalculationBase::Calc(Box::new(CalculationJob {
661                        base: CalculationBase::Calc(Box::new(CalculationJob {
662                            base: CalculationBase::Num(Number::Exact(2.into())),
663                            level: 1,
664                            negative: 0
665                        })),
666                        level: -1,
667                        negative: 0
668                    })),
669                    level: 1,
670                    negative: 0
671                }
672            ]
673        );
674    }
675
676    #[test]
677    fn test_commands_from_comment_text() {
678        let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note");
679        assert!(cmd1.shorten);
680        assert!(cmd1.steps);
681        assert!(cmd1.termial);
682        assert!(cmd1.no_note);
683        assert!(!cmd1.post_only);
684        let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note]");
685        assert!(cmd2.shorten);
686        assert!(cmd2.steps);
687        assert!(cmd2.termial);
688        assert!(cmd2.no_note);
689        assert!(!cmd2.post_only);
690        let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\]";
691        let cmd3 = Commands::from_comment_text(comment);
692        assert!(cmd3.shorten);
693        assert!(cmd3.steps);
694        assert!(cmd3.termial);
695        assert!(cmd3.no_note);
696        assert!(!cmd3.post_only);
697        let cmd4 = Commands::from_comment_text("shorten all triangle no_note");
698        assert!(!cmd4.shorten);
699        assert!(!cmd4.steps);
700        assert!(!cmd4.termial);
701        assert!(!cmd4.no_note);
702        assert!(!cmd4.post_only);
703    }
704
705    #[test]
706    fn test_commands_overrides_from_comment_text() {
707        let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note");
708        assert!(cmd1.shorten);
709        assert!(cmd1.steps);
710        assert!(cmd1.termial);
711        assert!(cmd1.no_note);
712        assert!(cmd1.post_only);
713    }
714
715    #[test]
716    fn test_might_have_factorial() {
717        assert!(Comment::might_have_factorial("5!"));
718        assert!(Comment::might_have_factorial("3?"));
719        assert!(!Comment::might_have_factorial("!?"));
720    }
721
722    #[test]
723    fn test_new_already_replied() {
724        let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
725        assert_eq!(comment.calculation_list, "");
726        assert!(comment.status.already_replied_or_rejected);
727    }
728
729    #[test]
730    fn test_locale_fallback_note() {
731        let consts = Consts::default();
732        let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
733            .extract(&consts)
734            .calc(&consts);
735        let reply = comment.get_reply(&consts);
736        assert_eq!(
737            reply,
738            "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.)*"
739        );
740    }
741
742    #[test]
743    fn test_limit_hit_note() {
744        let consts = Consts::default();
745        let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
746            .extract(&consts)
747            .calc(&consts);
748        comment.add_status(Status::LIMIT_HIT);
749        let reply = comment.get_reply(&consts);
750        assert_eq!(
751            reply,
752            "I have repeated myself enough, I won't do that calculation again.\n\n\n*^(This action was performed by a bot.)*"
753        );
754    }
755}