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