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))]
75pub struct Status {
76 pub already_replied_or_rejected: bool,
77 pub not_replied: bool,
78 pub number_too_big_to_calculate: bool,
79 pub no_factorial: bool,
80 pub reply_would_be_too_long: bool,
81 pub factorials_found: bool,
82}
83
84impl_all_bitwise!(Status {
85 already_replied_or_rejected,
86 not_replied,
87 number_too_big_to_calculate,
88 no_factorial,
89 reply_would_be_too_long,
90 factorials_found,
91});
92#[allow(dead_code)]
93impl Status {
94 pub const NONE: Self = Self {
95 already_replied_or_rejected: false,
96 not_replied: false,
97 number_too_big_to_calculate: false,
98 no_factorial: false,
99 reply_would_be_too_long: false,
100 factorials_found: false,
101 };
102 pub const ALREADY_REPLIED_OR_REJECTED: Self = Self {
103 already_replied_or_rejected: true,
104 ..Self::NONE
105 };
106 pub const NOT_REPLIED: Self = Self {
107 not_replied: true,
108 ..Self::NONE
109 };
110 pub const NUMBER_TOO_BIG_TO_CALCULATE: Self = Self {
111 number_too_big_to_calculate: true,
112 ..Self::NONE
113 };
114 pub const NO_FACTORIAL: Self = Self {
115 no_factorial: true,
116 ..Self::NONE
117 };
118 pub const REPLY_WOULD_BE_TOO_LONG: Self = Self {
119 reply_would_be_too_long: true,
120 ..Self::NONE
121 };
122 pub const FACTORIALS_FOUND: Self = Self {
123 factorials_found: true,
124 ..Self::NONE
125 };
126}
127
128#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
129#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
130pub struct Commands {
131 pub shorten: bool,
133 pub steps: bool,
135 pub termial: bool,
137 pub no_note: bool,
139 pub post_only: bool,
140}
141impl_all_bitwise!(Commands {
142 shorten,
143 steps,
144 termial,
145 no_note,
146 post_only,
147});
148#[allow(dead_code)]
149impl Commands {
150 pub const NONE: Self = Self {
151 shorten: false,
152 steps: false,
153 termial: false,
154 no_note: false,
155 post_only: false,
156 };
157 pub const SHORTEN: Self = Self {
158 shorten: true,
159 ..Self::NONE
160 };
161 pub const STEPS: Self = Self {
162 steps: true,
163 ..Self::NONE
164 };
165 pub const TERMIAL: Self = Self {
166 termial: true,
167 ..Self::NONE
168 };
169 pub const NO_NOTE: Self = Self {
170 no_note: true,
171 ..Self::NONE
172 };
173 pub const POST_ONLY: Self = Self {
174 post_only: true,
175 ..Self::NONE
176 };
177}
178
179impl Commands {
180 fn contains_command_format(text: &str, command: &str) -> bool {
181 let pattern1 = format!("\\[{command}\\]");
182 let pattern2 = format!("[{command}]");
183 let pattern3 = format!("!{command}");
184 text.contains(&pattern1) || text.contains(&pattern2) || text.contains(&pattern3)
185 }
186
187 pub fn from_comment_text(text: &str) -> Self {
188 Self {
189 shorten: Self::contains_command_format(text, "short")
190 || Self::contains_command_format(text, "shorten"),
191 steps: Self::contains_command_format(text, "steps")
192 || Self::contains_command_format(text, "all"),
193 termial: Self::contains_command_format(text, "termial")
194 || Self::contains_command_format(text, "triangle"),
195 no_note: Self::contains_command_format(text, "no note")
196 || Self::contains_command_format(text, "no_note"),
197 post_only: false,
198 }
199 }
200 pub fn overrides_from_comment_text(text: &str) -> Self {
201 Self {
202 shorten: !Self::contains_command_format(text, "long"),
203 steps: !(Self::contains_command_format(text, "no steps")
204 | Self::contains_command_format(text, "no_steps")),
205 termial: !(Self::contains_command_format(text, "no termial")
206 | Self::contains_command_format(text, "no_termial")),
207 no_note: !Self::contains_command_format(text, "note"),
208 post_only: true,
209 }
210 }
211}
212
213macro_rules! contains_comb {
214 ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
216 $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end,$($end_rest),*]) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
217 };
218 (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
220 $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
221 };
222 ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
224 $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
225 };
226 ($var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
228 $var.contains(concat!($start, $end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
229 };
230 (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
232 $var.contains(concat!($start,$end))
233 };
234 (@inner $var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
236 $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
237 };
238 ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
240 $var.contains(concat!($start, $end))
241 };
242 (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
244 $var.contains(concat!($start,$end))
245 };
246}
247
248impl<Meta> CommentConstructed<Meta> {
249 pub fn new(
251 comment_text: &str,
252 meta: Meta,
253 pre_commands: Commands,
254 max_length: usize,
255 locale: &str,
256 ) -> Self {
257 let command_overrides = Commands::overrides_from_comment_text(comment_text);
258 let commands: Commands =
259 (Commands::from_comment_text(comment_text) | pre_commands) & command_overrides;
260
261 let mut status: Status = Default::default();
262
263 let text = if Self::might_have_factorial(comment_text) {
264 comment_text.to_owned()
265 } else {
266 status.no_factorial = true;
267 String::new()
268 };
269
270 Comment {
271 meta,
272 notify: None,
273 calculation_list: text,
274 status,
275 commands,
276 max_length,
277 locale: locale.to_owned(),
278 }
279 }
280
281 fn might_have_factorial(text: &str) -> bool {
282 contains_comb!(
283 text,
284 [
285 "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ")", "e", "pi", "phi", "tau",
286 "π", "ɸ", "τ"
287 ],
288 ["!", "?"]
289 ) || contains_comb!(
290 text,
291 ["!"],
292 [
293 "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "(", "e", "pi", "phi", "tau",
294 "π", "ɸ", "τ"
295 ]
296 )
297 }
298
299 pub fn extract(self, consts: &Consts) -> CommentExtracted<Meta> {
301 let Comment {
302 meta,
303 calculation_list: comment_text,
304 notify,
305 mut status,
306 commands,
307 max_length,
308 locale,
309 } = self;
310 let pending_list: Vec<CalculationJob> = parse(
311 &comment_text,
312 commands.termial,
313 consts,
314 &consts
315 .locales
316 .get(&locale)
317 .unwrap_or(consts.locales.get(&consts.default_locale).unwrap())
318 .format()
319 .number_format(),
320 );
321
322 if pending_list.is_empty() {
323 status.no_factorial = true;
324 }
325
326 Comment {
327 meta,
328 calculation_list: pending_list,
329 notify,
330 status,
331 commands,
332 max_length,
333 locale,
334 }
335 }
336
337 pub fn new_already_replied(meta: Meta, max_length: usize, locale: &str) -> Self {
339 let text = String::new();
340 let status: Status = Status {
341 already_replied_or_rejected: true,
342 ..Default::default()
343 };
344 let commands: Commands = Default::default();
345
346 Comment {
347 meta,
348 notify: None,
349 calculation_list: text,
350 status,
351 commands,
352 max_length,
353 locale: locale.to_owned(),
354 }
355 }
356}
357impl<Meta, S> Comment<Meta, S> {
358 pub fn add_status(&mut self, status: Status) {
359 self.status = self.status | status;
360 }
361}
362impl<Meta> CommentExtracted<Meta> {
363 pub fn calc(self, consts: &Consts) -> CommentCalculated<Meta> {
365 let Comment {
366 meta,
367 calculation_list: pending_list,
368 notify,
369 mut status,
370 commands,
371 max_length,
372 locale,
373 } = self;
374 let mut calculation_list: Vec<Calculation> = pending_list
375 .into_iter()
376 .flat_map(|calc| calc.execute(commands.steps, consts))
377 .filter_map(|x| {
378 if x.is_none() {
379 status.number_too_big_to_calculate = true;
380 };
381 x
382 })
383 .collect();
384
385 calculation_list.sort();
386 calculation_list.dedup();
387 calculation_list.sort_by_key(|x| x.steps.len());
388
389 if calculation_list.is_empty() {
390 status.no_factorial = true;
391 } else {
392 status.factorials_found = true;
393 }
394 Comment {
395 meta,
396 calculation_list,
397 notify,
398 status,
399 commands,
400 max_length,
401 locale,
402 }
403 }
404}
405impl<Meta> CommentCalculated<Meta> {
406 pub fn get_reply(&self, consts: &Consts) -> String {
408 let locale = consts
409 .locales
410 .get(&self.locale)
411 .unwrap_or(consts.locales.get(&consts.default_locale).unwrap());
412 let mut note = self
413 .notify
414 .as_ref()
415 .map(|user| locale.notes().mention().replace("{mention}", user) + "\n\n")
416 .unwrap_or_default();
417
418 let too_big_number = Integer::u64_pow_u64(10, self.max_length as u64).complete();
419 let too_big_number = &too_big_number;
420
421 let multiple = self.calculation_list.len() > 1;
423 if !self.commands.no_note {
424 if self
425 .calculation_list
426 .iter()
427 .any(Calculation::is_digit_tower)
428 {
429 if multiple {
430 let _ = note.write_str(locale.notes().tower_mult());
431 let _ = note.write_str("\n\n");
432 } else {
433 let _ = note.write_str(locale.notes().tower());
434 let _ = note.write_str("\n\n");
435 }
436 } else if self
437 .calculation_list
438 .iter()
439 .any(Calculation::is_aproximate_digits)
440 {
441 if multiple {
442 let _ = note.write_str(locale.notes().digits_mult());
443 let _ = note.write_str("\n\n");
444 } else {
445 let _ = note.write_str(locale.notes().digits());
446 let _ = note.write_str("\n\n");
447 }
448 } else if self
449 .calculation_list
450 .iter()
451 .any(Calculation::is_approximate)
452 {
453 if multiple {
454 let _ = note.write_str(locale.notes().approx_mult());
455 let _ = note.write_str("\n\n");
456 } else {
457 let _ = note.write_str(locale.notes().approx());
458 let _ = note.write_str("\n\n");
459 }
460 } else if self.calculation_list.iter().any(Calculation::is_rounded) {
461 if multiple {
462 let _ = note.write_str(locale.notes().round_mult());
463 let _ = note.write_str("\n\n");
464 } else {
465 let _ = note.write_str(locale.notes().round());
466 let _ = note.write_str("\n\n");
467 }
468 } else if self
469 .calculation_list
470 .iter()
471 .any(|c| c.is_too_long(too_big_number))
472 {
473 if multiple {
474 let _ = note.write_str(locale.notes().too_big_mult());
475 let _ = note.write_str("\n\n");
476 } else {
477 let _ = note.write_str(locale.notes().too_big());
478 let _ = note.write_str("\n\n");
479 }
480 }
481 }
482
483 let mut reply = self
485 .calculation_list
486 .iter()
487 .fold(note.clone(), |mut acc, factorial| {
488 let _ = factorial.format(
489 &mut acc,
490 self.commands.shorten,
491 false,
492 too_big_number,
493 consts,
494 &locale.format(),
495 );
496 acc
497 });
498
499 if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length
501 && !self.commands.shorten
502 && !self
503 .calculation_list
504 .iter()
505 .all(|fact| fact.is_too_long(too_big_number))
506 {
507 if note.is_empty() && !self.commands.no_note {
508 let _ = note.write_str(locale.notes().remove());
509 };
510 reply = self
511 .calculation_list
512 .iter()
513 .fold(note, |mut acc, factorial| {
514 let _ = factorial.format(
515 &mut acc,
516 true,
517 false,
518 too_big_number,
519 consts,
520 &locale.format(),
521 );
522 acc
523 });
524 }
525
526 if reply.len() + locale.bot_disclaimer().len() + 16 > self.max_length {
528 let note = locale.notes().remove().clone().into_owned() + "\n\n";
529 let mut factorial_list: Vec<String> = self
530 .calculation_list
531 .iter()
532 .map(|fact| {
533 let mut res = String::new();
534 let _ = fact.format(
535 &mut res,
536 true,
537 false,
538 too_big_number,
539 consts,
540 &locale.format(),
541 );
542 res
543 })
544 .collect();
545 'drop_last: {
546 while note.len()
547 + factorial_list.iter().map(|s| s.len()).sum::<usize>()
548 + locale.bot_disclaimer().len()
549 + 16
550 > self.max_length
551 {
552 factorial_list.pop();
554 if factorial_list.is_empty() {
555 if self.calculation_list.len() == 1 {
556 let note = locale.notes().tetration().clone().into_owned() + "\n\n";
557 reply =
558 self.calculation_list
559 .iter()
560 .fold(note, |mut acc, factorial| {
561 let _ = factorial.format(
562 &mut acc,
563 true,
564 true,
565 too_big_number,
566 consts,
567 &locale.format(),
568 );
569 acc
570 });
571 if reply.len() <= self.max_length {
572 break 'drop_last;
573 }
574 }
575 reply = locale.notes().no_post().to_string();
576 break 'drop_last;
577 }
578 }
579 reply = factorial_list
580 .iter()
581 .fold(note, |acc, factorial| format!("{acc}{factorial}"));
582 }
583 }
584 if !locale.bot_disclaimer().is_empty() {
585 reply.push_str("\n*^(");
586 reply.push_str(locale.bot_disclaimer());
587 reply.push_str(")*");
588 }
589 reply
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use crate::{
596 calculation_results::Number,
597 calculation_tasks::{CalculationBase, CalculationJob},
598 locale::NumFormat,
599 };
600
601 const MAX_LENGTH: usize = 10_000;
602
603 use super::*;
604
605 type Comment<S> = super::Comment<(), S>;
606
607 #[test]
608 fn test_extraction_dedup() {
609 let consts = Consts::default();
610 let jobs = parse(
611 "24! -24! 2!? (2!?)!",
612 true,
613 &consts,
614 &NumFormat::V1(&crate::locale::v1::NumFormat { decimal: '.' }),
615 );
616 assert_eq!(
617 jobs,
618 [
619 CalculationJob {
620 base: CalculationBase::Num(Number::Exact(24.into())),
621 level: 1,
622 negative: 0
623 },
624 CalculationJob {
625 base: CalculationBase::Num(Number::Exact(24.into())),
626 level: 1,
627 negative: 1
628 },
629 CalculationJob {
630 base: CalculationBase::Calc(Box::new(CalculationJob {
631 base: CalculationBase::Num(Number::Exact(2.into())),
632 level: 1,
633 negative: 0
634 })),
635 level: -1,
636 negative: 0
637 },
638 CalculationJob {
639 base: CalculationBase::Calc(Box::new(CalculationJob {
640 base: CalculationBase::Calc(Box::new(CalculationJob {
641 base: CalculationBase::Num(Number::Exact(2.into())),
642 level: 1,
643 negative: 0
644 })),
645 level: -1,
646 negative: 0
647 })),
648 level: 1,
649 negative: 0
650 }
651 ]
652 );
653 }
654
655 #[test]
656 fn test_commands_from_comment_text() {
657 let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note");
658 assert!(cmd1.shorten);
659 assert!(cmd1.steps);
660 assert!(cmd1.termial);
661 assert!(cmd1.no_note);
662 assert!(!cmd1.post_only);
663 let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note]");
664 assert!(cmd2.shorten);
665 assert!(cmd2.steps);
666 assert!(cmd2.termial);
667 assert!(cmd2.no_note);
668 assert!(!cmd2.post_only);
669 let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\]";
670 let cmd3 = Commands::from_comment_text(comment);
671 assert!(cmd3.shorten);
672 assert!(cmd3.steps);
673 assert!(cmd3.termial);
674 assert!(cmd3.no_note);
675 assert!(!cmd3.post_only);
676 let cmd4 = Commands::from_comment_text("shorten all triangle no_note");
677 assert!(!cmd4.shorten);
678 assert!(!cmd4.steps);
679 assert!(!cmd4.termial);
680 assert!(!cmd4.no_note);
681 assert!(!cmd4.post_only);
682 }
683
684 #[test]
685 fn test_commands_overrides_from_comment_text() {
686 let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note");
687 assert!(cmd1.shorten);
688 assert!(cmd1.steps);
689 assert!(cmd1.termial);
690 assert!(cmd1.no_note);
691 assert!(cmd1.post_only);
692 }
693
694 #[test]
695 fn test_might_have_factorial() {
696 assert!(Comment::might_have_factorial("5!"));
697 assert!(Comment::might_have_factorial("3?"));
698 assert!(!Comment::might_have_factorial("!?"));
699 }
700
701 #[test]
702 fn test_new_already_replied() {
703 let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
704 assert_eq!(comment.calculation_list, "");
705 assert!(comment.status.already_replied_or_rejected);
706 }
707}