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 let note = locale.notes().remove().clone().into_owned() + "\n\n";
635 let mut factorial_list: Vec<String> = self
636 .calculation_list
637 .iter()
638 .map(|fact| {
639 let mut res = String::new();
640 let _ = fact.format(
641 &mut res,
642 true,
643 false,
644 too_big_number,
645 consts,
646 &locale.format(),
647 );
648 res
649 })
650 .collect();
651 'drop_last: {
652 while note.len()
653 + factorial_list.iter().map(|s| s.len()).sum::<usize>()
654 + locale.bot_disclaimer().len()
655 + 16
656 > self.max_length
657 {
658 factorial_list.pop();
660 if factorial_list.is_empty() {
661 if self.calculation_list.len() == 1 {
662 let note = locale.notes().tetration().clone().into_owned() + "\n\n";
663 reply =
664 self.calculation_list
665 .iter()
666 .fold(note, |mut acc, factorial| {
667 let _ = factorial.format(
668 &mut acc,
669 true,
670 true,
671 too_big_number,
672 consts,
673 &locale.format(),
674 );
675 acc
676 });
677 if reply.len() <= self.max_length {
678 break 'drop_last;
679 }
680 }
681 reply = locale.notes().no_post().to_string();
682 break 'drop_last;
683 }
684 }
685 reply = factorial_list
686 .iter()
687 .fold(note, |acc, factorial| format!("{acc}{factorial}"));
688 }
689 }
690 if !locale.bot_disclaimer().is_empty() {
691 reply.push_str("\n*^(");
692 reply.push_str(locale.bot_disclaimer());
693 reply.push_str(")*");
694 }
695 reply
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use crate::{
702 calculation_results::Number,
703 calculation_tasks::{CalculationBase, CalculationJob},
704 locale::NumFormat,
705 };
706
707 const MAX_LENGTH: usize = 10_000;
708
709 use super::*;
710
711 type Comment<S> = super::Comment<(), S>;
712
713 #[test]
714 fn test_extraction_dedup() {
715 let consts = Consts::default();
716 let jobs = parse(
717 "24! -24! 2!? (2!?)!",
718 true,
719 &consts,
720 &NumFormat::V1(&crate::locale::v1::NumFormat { decimal: '.' }),
721 );
722 assert_eq!(
723 jobs,
724 [
725 CalculationJob {
726 base: CalculationBase::Num(Number::Exact(24.into())),
727 level: 1,
728 negative: 0
729 },
730 CalculationJob {
731 base: CalculationBase::Num(Number::Exact(24.into())),
732 level: 1,
733 negative: 1
734 },
735 CalculationJob {
736 base: CalculationBase::Calc(Box::new(CalculationJob {
737 base: CalculationBase::Num(Number::Exact(2.into())),
738 level: 1,
739 negative: 0
740 })),
741 level: -1,
742 negative: 0
743 },
744 CalculationJob {
745 base: CalculationBase::Calc(Box::new(CalculationJob {
746 base: CalculationBase::Calc(Box::new(CalculationJob {
747 base: CalculationBase::Num(Number::Exact(2.into())),
748 level: 1,
749 negative: 0
750 })),
751 level: -1,
752 negative: 0
753 })),
754 level: 1,
755 negative: 0
756 }
757 ]
758 );
759 }
760
761 #[test]
762 fn test_commands_from_comment_text() {
763 let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note !nested");
764 assert!(cmd1.shorten);
765 assert!(cmd1.steps);
766 assert!(cmd1.termial);
767 assert!(cmd1.no_note);
768 assert!(cmd1.nested);
769 let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note] [nest]");
770 assert!(cmd2.shorten);
771 assert!(cmd2.steps);
772 assert!(cmd2.termial);
773 assert!(cmd2.no_note);
774 assert!(cmd2.nested);
775 let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\] \[nest\]";
776 let cmd3 = Commands::from_comment_text(comment);
777 assert!(cmd3.shorten);
778 assert!(cmd3.steps);
779 assert!(cmd3.termial);
780 assert!(cmd3.no_note);
781 assert!(cmd3.nested);
782 let cmd4 = Commands::from_comment_text("shorten all triangle no_note nest");
783 assert!(!cmd4.shorten);
784 assert!(!cmd4.steps);
785 assert!(!cmd4.termial);
786 assert!(!cmd4.no_note);
787 assert!(!cmd4.nested);
788 }
789
790 #[test]
791 fn test_commands_overrides_from_comment_text() {
792 let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note multi");
793 assert!(cmd1.shorten);
794 assert!(cmd1.steps);
795 assert!(cmd1.termial);
796 assert!(cmd1.no_note);
797 assert!(cmd1.nested);
798 }
799
800 #[test]
801 fn test_might_have_factorial() {
802 assert!(Comment::might_have_factorial("5!"));
803 assert!(Comment::might_have_factorial("3?"));
804 assert!(!Comment::might_have_factorial("!?"));
805 }
806
807 #[test]
808 fn test_new_already_replied() {
809 let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
810 assert_eq!(comment.calculation_list, "");
811 assert!(comment.status.already_replied_or_rejected);
812 }
813
814 #[test]
815 fn test_locale_fallback_note() {
816 let consts = Consts::default();
817 let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
818 .extract(&consts)
819 .calc(&consts);
820 let reply = comment.get_reply(&consts);
821 assert_eq!(
822 reply,
823 "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))*"
824 );
825 }
826
827 #[test]
828 fn test_limit_hit_note() {
829 let consts = Consts::default();
830 let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
831 .extract(&consts)
832 .calc(&consts);
833 comment.add_status(Status::LIMIT_HIT);
834 let reply = comment.get_reply(&consts);
835 assert_eq!(
836 reply,
837 "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))*"
838 );
839 }
840}