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::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 pub shorten: bool,
142 pub steps: bool,
144 pub termial: bool,
146 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 ($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 $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 ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
233 $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
234 };
235 ($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 $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
241 $var.contains(concat!($start,$end))
242 };
243 (@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 ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
249 $var.contains(concat!($start, $end))
250 };
251 (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
253 $var.contains(concat!($start,$end))
254 };
255}
256
257impl<Meta> CommentConstructed<Meta> {
258 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 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 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 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 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 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 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 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 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 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}