1#[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#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
52#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
53pub struct Comment<Meta, S> {
54 pub meta: Meta,
56 pub calculation_list: S,
58 pub notify: Option<String>,
60 pub status: Status,
61 pub commands: Commands,
62 pub max_length: usize,
64 pub locale: String,
65}
66pub type CommentConstructed<Meta> = Comment<Meta, String>;
68pub type CommentExtracted<Meta> = Comment<Meta, Vec<CalculationJob>>;
70pub 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 #[cfg_attr(any(feature = "serde", test), serde(default))]
142 pub shorten: bool,
143 #[cfg_attr(any(feature = "serde", test), serde(default))]
145 pub steps: bool,
146 #[cfg_attr(any(feature = "serde", test), serde(default))]
148 pub nested: bool,
149 #[cfg_attr(any(feature = "serde", test), serde(default))]
151 pub termial: bool,
152 #[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 ($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 $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 ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
241 $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
242 };
243 ($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 $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
249 $var.contains(concat!($start,$end))
250 };
251 (@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 ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
257 $var.contains(concat!($start, $end))
258 };
259 (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
261 $var.contains(concat!($start,$end))
262 };
263}
264
265impl<Meta> CommentConstructed<Meta> {
266 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 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 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 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 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 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 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 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 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 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 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}