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 #[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 termial: bool,
149 #[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 ($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 $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 ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
228 $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
229 };
230 ($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 $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
236 $var.contains(concat!($start,$end))
237 };
238 (@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 ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
244 $var.contains(concat!($start, $end))
245 };
246 (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
248 $var.contains(concat!($start,$end))
249 };
250}
251
252impl<Meta> CommentConstructed<Meta> {
253 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 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 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 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 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 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 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 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 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 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}