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::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    /// Parse and calculate termials.
147    #[cfg_attr(any(feature = "serde", test), serde(default))]
148    pub termial: bool,
149    /// Disable the beginning note.
150    #[cfg_attr(any(feature = "serde", test), serde(default))]
151    pub no_note: bool,
152}
153impl_all_bitwise!(Commands {
154    shorten,
155    steps,
156    termial,
157    no_note,
158});
159#[allow(dead_code)]
160impl Commands {
161    pub const NONE: Self = Self {
162        shorten: false,
163        steps: false,
164        termial: false,
165        no_note: false,
166    };
167    pub const SHORTEN: Self = Self {
168        shorten: true,
169        ..Self::NONE
170    };
171    pub const STEPS: Self = Self {
172        steps: true,
173        ..Self::NONE
174    };
175    pub const TERMIAL: Self = Self {
176        termial: true,
177        ..Self::NONE
178    };
179    pub const NO_NOTE: Self = Self {
180        no_note: true,
181        ..Self::NONE
182    };
183}
184
185impl Commands {
186    fn contains_command_format(text: &str, command: &str) -> bool {
187        let pattern1 = format!("\\[{command}\\]");
188        let pattern2 = format!("[{command}]");
189        let pattern3 = format!("!{command}");
190        text.contains(&pattern1) || text.contains(&pattern2) || text.contains(&pattern3)
191    }
192
193    pub fn from_comment_text(text: &str) -> Self {
194        Self {
195            shorten: Self::contains_command_format(text, "short")
196                || Self::contains_command_format(text, "shorten"),
197            steps: Self::contains_command_format(text, "steps")
198                || Self::contains_command_format(text, "all"),
199            termial: Self::contains_command_format(text, "termial")
200                || Self::contains_command_format(text, "triangle"),
201            no_note: Self::contains_command_format(text, "no note")
202                || Self::contains_command_format(text, "no_note"),
203        }
204    }
205    pub fn overrides_from_comment_text(text: &str) -> Self {
206        Self {
207            shorten: !Self::contains_command_format(text, "long"),
208            steps: !(Self::contains_command_format(text, "no steps")
209                | Self::contains_command_format(text, "no_steps")),
210            termial: !(Self::contains_command_format(text, "no termial")
211                | Self::contains_command_format(text, "no_termial")),
212            no_note: !Self::contains_command_format(text, "note"),
213        }
214    }
215}
216
217macro_rules! contains_comb {
218    // top level (advance both separately)
219    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
220        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end,$($end_rest),*]) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
221    };
222    // inner (advance only end)
223    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
224        $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
225    };
226    // top level (advance both separately) singular end (advance only start)
227    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
228        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
229    };
230    // top level (advance both separately) singular start (advance only end)
231    ($var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
232        $var.contains(concat!($start, $end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
233    };
234    // inner (advance only end) singular end (advance only start, so nothing)
235    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
236        $var.contains(concat!($start,$end))
237    };
238    // inner (advance only end) singular end (advance only end)
239    (@inner $var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
240        $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
241    };
242    // top level (advance both separately) singular start and end (no advance)
243    ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
244        $var.contains(concat!($start, $end))
245    };
246    // inner (advance only end) singular start and end (no advance)
247    (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
248        $var.contains(concat!($start,$end))
249    };
250}
251
252impl<Meta> CommentConstructed<Meta> {
253    /// Takes a raw comment, finds the factorials and commands, and packages it, also checks if it might have something to calculate.
254    pub fn new(
255        comment_text: &str,
256        meta: Meta,
257        pre_commands: Commands,
258        max_length: usize,
259        locale: &str,
260    ) -> Self {
261        let command_overrides = Commands::overrides_from_comment_text(comment_text);
262        let commands: Commands =
263            (Commands::from_comment_text(comment_text) | pre_commands) & command_overrides;
264
265        let mut status: Status = Default::default();
266
267        let text = if Self::might_have_factorial(comment_text) {
268            comment_text.to_owned()
269        } else {
270            status.no_factorial = true;
271            String::new()
272        };
273
274        Comment {
275            meta,
276            notify: None,
277            calculation_list: text,
278            status,
279            commands,
280            max_length,
281            locale: locale.to_owned(),
282        }
283    }
284
285    fn might_have_factorial(text: &str) -> bool {
286        contains_comb!(
287            text,
288            [
289                "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ")", "e", "pi", "phi", "tau",
290                "π", "ɸ", "τ"
291            ],
292            ["!", "?"]
293        ) || contains_comb!(
294            text,
295            ["!"],
296            [
297                "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "(", "e", "pi", "phi", "tau",
298                "π", "ɸ", "τ"
299            ]
300        )
301    }
302
303    /// Extracts the calculations using [parse](mod@crate::parse).
304    pub fn extract(self, consts: &Consts) -> CommentExtracted<Meta> {
305        let Comment {
306            meta,
307            calculation_list: comment_text,
308            notify,
309            mut status,
310            commands,
311            max_length,
312            locale,
313        } = self;
314        let pending_list: Vec<CalculationJob> = parse(
315            &comment_text,
316            commands.termial,
317            consts,
318            &consts
319                .locales
320                .get(&locale)
321                .unwrap_or(consts.locales.get(&consts.default_locale).unwrap())
322                .format()
323                .number_format(),
324        );
325
326        if pending_list.is_empty() {
327            status.no_factorial = true;
328        }
329
330        Comment {
331            meta,
332            calculation_list: pending_list,
333            notify,
334            status,
335            commands,
336            max_length,
337            locale,
338        }
339    }
340
341    /// Constructs an empty comment with [Status] already_replied_or_rejected set.
342    pub fn new_already_replied(meta: Meta, max_length: usize, locale: &str) -> Self {
343        let text = String::new();
344        let status: Status = Status {
345            already_replied_or_rejected: true,
346            ..Default::default()
347        };
348        let commands: Commands = Default::default();
349
350        Comment {
351            meta,
352            notify: None,
353            calculation_list: text,
354            status,
355            commands,
356            max_length,
357            locale: locale.to_owned(),
358        }
359    }
360}
361impl<Meta, S> Comment<Meta, S> {
362    pub fn add_status(&mut self, status: Status) {
363        self.status = self.status | status;
364    }
365}
366impl<Meta> CommentExtracted<Meta> {
367    /// Does the calculations using [calculation_tasks](crate::calculation_tasks).
368    pub fn calc(self, consts: &Consts) -> CommentCalculated<Meta> {
369        let Comment {
370            meta,
371            calculation_list: pending_list,
372            notify,
373            mut status,
374            commands,
375            max_length,
376            locale,
377        } = self;
378        let mut calculation_list: Vec<Calculation> = pending_list
379            .into_iter()
380            .flat_map(|calc| calc.execute(commands.steps, consts))
381            .filter_map(|x| {
382                if x.is_none() {
383                    status.number_too_big_to_calculate = true;
384                };
385                x
386            })
387            .collect();
388
389        calculation_list.sort();
390        calculation_list.dedup();
391        calculation_list.sort_by_key(|x| x.steps.len());
392
393        if calculation_list.is_empty() {
394            status.no_factorial = true;
395        } else {
396            status.factorials_found = true;
397        }
398        Comment {
399            meta,
400            calculation_list,
401            notify,
402            status,
403            commands,
404            max_length,
405            locale,
406        }
407    }
408}
409impl<Meta> CommentCalculated<Meta> {
410    /// Does the formatting for the reply using [calculation_result](crate::calculation_results).
411    pub fn get_reply(&self, consts: &Consts) -> String {
412        let mut fell_back = false;
413        let locale = consts.locales.get(&self.locale).unwrap_or_else(|| {
414            fell_back = true;
415            consts.locales.get(&consts.default_locale).unwrap()
416        });
417        let mut note = self
418            .notify
419            .as_ref()
420            .map(|user| locale.notes().mention().replace("{mention}", user) + "\n\n")
421            .unwrap_or_default();
422
423        if fell_back {
424            let _ = note.write_str("Sorry, I currently don't speak ");
425            let _ = note.write_str(&self.locale);
426            let _ = note.write_str(". Maybe you could [teach me](https://github.com/tolik518/factorion-bot/blob/master/CONTRIBUTING.md#translation)? \n\n");
427        }
428
429        let too_big_number = Integer::u64_pow_u64(10, self.max_length as u64).complete();
430        let too_big_number = &too_big_number;
431
432        // Add Note
433        let multiple = self.calculation_list.len() > 1;
434        if !self.commands.no_note {
435            if self.status.limit_hit {
436                let _ = note.write_str(locale.notes().limit_hit().map(AsRef::as_ref).unwrap_or(
437                    "I have repeated myself enough, I won't do that calculation again.",
438                ));
439                let _ = note.write_str("\n\n");
440            } else if self
441                .calculation_list
442                .iter()
443                .any(Calculation::is_digit_tower)
444            {
445                if multiple {
446                    let _ = note.write_str(locale.notes().tower_mult());
447                    let _ = note.write_str("\n\n");
448                } else {
449                    let _ = note.write_str(locale.notes().tower());
450                    let _ = note.write_str("\n\n");
451                }
452            } else if self
453                .calculation_list
454                .iter()
455                .any(Calculation::is_aproximate_digits)
456            {
457                if multiple {
458                    let _ = note.write_str(locale.notes().digits_mult());
459                    let _ = note.write_str("\n\n");
460                } else {
461                    let _ = note.write_str(locale.notes().digits());
462                    let _ = note.write_str("\n\n");
463                }
464            } else if self
465                .calculation_list
466                .iter()
467                .any(Calculation::is_approximate)
468            {
469                if multiple {
470                    let _ = note.write_str(locale.notes().approx_mult());
471                    let _ = note.write_str("\n\n");
472                } else {
473                    let _ = note.write_str(locale.notes().approx());
474                    let _ = note.write_str("\n\n");
475                }
476            } else if self.calculation_list.iter().any(Calculation::is_rounded) {
477                if multiple {
478                    let _ = note.write_str(locale.notes().round_mult());
479                    let _ = note.write_str("\n\n");
480                } else {
481                    let _ = note.write_str(locale.notes().round());
482                    let _ = note.write_str("\n\n");
483                }
484            } else if self
485                .calculation_list
486                .iter()
487                .any(|c| c.is_too_long(too_big_number))
488            {
489                if multiple {
490                    let _ = note.write_str(locale.notes().too_big_mult());
491                    let _ = note.write_str("\n\n");
492                } else {
493                    let _ = note.write_str(locale.notes().too_big());
494                    let _ = note.write_str("\n\n");
495                }
496            }
497        }
498
499        // Add Factorials
500        let mut reply = self
501            .calculation_list
502            .iter()
503            .fold(note.clone(), |mut acc, factorial| {
504                let _ = factorial.format(
505                    &mut acc,
506                    self.commands.shorten,
507                    false,
508                    too_big_number,
509                    consts,
510                    &locale.format(),
511                );
512                acc
513            });
514
515        // If the reply was too long try force shortening all factorials
516        if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length
517            && !self.commands.shorten
518            && !self
519                .calculation_list
520                .iter()
521                .all(|fact| fact.is_too_long(too_big_number))
522        {
523            if note.is_empty() && !self.commands.no_note {
524                if multiple {
525                    let _ = note.write_str(locale.notes().too_big_mult());
526                } else {
527                    let _ = note.write_str(locale.notes().too_big());
528                }
529                let _ = note.write_str("\n\n");
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        let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note]");
684        assert!(cmd2.shorten);
685        assert!(cmd2.steps);
686        assert!(cmd2.termial);
687        assert!(cmd2.no_note);
688        let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\]";
689        let cmd3 = Commands::from_comment_text(comment);
690        assert!(cmd3.shorten);
691        assert!(cmd3.steps);
692        assert!(cmd3.termial);
693        assert!(cmd3.no_note);
694        let cmd4 = Commands::from_comment_text("shorten all triangle no_note");
695        assert!(!cmd4.shorten);
696        assert!(!cmd4.steps);
697        assert!(!cmd4.termial);
698        assert!(!cmd4.no_note);
699    }
700
701    #[test]
702    fn test_commands_overrides_from_comment_text() {
703        let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note");
704        assert!(cmd1.shorten);
705        assert!(cmd1.steps);
706        assert!(cmd1.termial);
707        assert!(cmd1.no_note);
708    }
709
710    #[test]
711    fn test_might_have_factorial() {
712        assert!(Comment::might_have_factorial("5!"));
713        assert!(Comment::might_have_factorial("3?"));
714        assert!(!Comment::might_have_factorial("!?"));
715    }
716
717    #[test]
718    fn test_new_already_replied() {
719        let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
720        assert_eq!(comment.calculation_list, "");
721        assert!(comment.status.already_replied_or_rejected);
722    }
723
724    #[test]
725    fn test_locale_fallback_note() {
726        let consts = Consts::default();
727        let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
728            .extract(&consts)
729            .calc(&consts);
730        let reply = comment.get_reply(&consts);
731        assert_eq!(
732            reply,
733            "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))*"
734        );
735    }
736
737    #[test]
738    fn test_limit_hit_note() {
739        let consts = Consts::default();
740        let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
741            .extract(&consts)
742            .calc(&consts);
743        comment.add_status(Status::LIMIT_HIT);
744        let reply = comment.get_reply(&consts);
745        assert_eq!(
746            reply,
747            "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))*"
748        );
749    }
750}