1use serde::{Deserialize, Serialize};
4
5use crate::rug::integer::IntegerExt64;
6use crate::rug::{Complete, Integer};
7
8use crate::calculation_results::Calculation;
9use crate::calculation_tasks::CalculationJob;
10use crate::parse::parse;
11
12use std::fmt::Write;
13use std::ops::*;
14macro_rules! impl_bitwise {
15 ($s_name:ident {$($s_fields:ident),*}, $t_name:ident, $fn_name:ident) => {
16 impl $t_name for $s_name {
17 type Output = Self;
18 fn $fn_name(self, rhs: Self) -> Self {
19 Self {
20 $($s_fields: self.$s_fields.$fn_name(rhs.$s_fields),)*
21 }
22 }
23 }
24 };
25}
26macro_rules! impl_all_bitwise {
27 ($s_name:ident {$($s_fields:ident,)*}) => {impl_all_bitwise!($s_name {$($s_fields),*});};
28 ($s_name:ident {$($s_fields:ident),*}) => {
29 impl_bitwise!($s_name {$($s_fields),*}, BitOr, bitor);
30 impl_bitwise!($s_name {$($s_fields),*}, BitXor, bitxor);
31 impl_bitwise!($s_name {$($s_fields),*}, BitAnd, bitand);
32 impl Not for $s_name {
33 type Output = Self;
34 fn not(self) -> Self {
35 Self {
36 $($s_fields: self.$s_fields.not(),)*
37 }
38 }
39 }
40 };
41}
42
43#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
50pub struct Comment<Meta, S> {
51 pub meta: Meta,
53 pub calculation_list: S,
55 pub notify: Option<String>,
57 pub status: Status,
58 pub commands: Commands,
59 pub max_length: usize,
61}
62pub type CommentConstructed<Meta> = Comment<Meta, String>;
64pub type CommentExtracted<Meta> = Comment<Meta, Vec<CalculationJob>>;
66pub type CommentCalculated<Meta> = Comment<Meta, Vec<Calculation>>;
68
69#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
70pub struct Status {
71 pub already_replied_or_rejected: bool,
72 pub not_replied: bool,
73 pub number_too_big_to_calculate: bool,
74 pub no_factorial: bool,
75 pub reply_would_be_too_long: bool,
76 pub factorials_found: bool,
77}
78
79impl_all_bitwise!(Status {
80 already_replied_or_rejected,
81 not_replied,
82 number_too_big_to_calculate,
83 no_factorial,
84 reply_would_be_too_long,
85 factorials_found,
86});
87#[allow(dead_code)]
88impl Status {
89 pub const NONE: Self = Self {
90 already_replied_or_rejected: false,
91 not_replied: false,
92 number_too_big_to_calculate: false,
93 no_factorial: false,
94 reply_would_be_too_long: false,
95 factorials_found: false,
96 };
97 pub const ALREADY_REPLIED_OR_REJECTED: Self = Self {
98 already_replied_or_rejected: true,
99 ..Self::NONE
100 };
101 pub const NOT_REPLIED: Self = Self {
102 not_replied: true,
103 ..Self::NONE
104 };
105 pub const NUMBER_TOO_BIG_TO_CALCULATE: Self = Self {
106 number_too_big_to_calculate: true,
107 ..Self::NONE
108 };
109 pub const NO_FACTORIAL: Self = Self {
110 no_factorial: true,
111 ..Self::NONE
112 };
113 pub const REPLY_WOULD_BE_TOO_LONG: Self = Self {
114 reply_would_be_too_long: true,
115 ..Self::NONE
116 };
117 pub const FACTORIALS_FOUND: Self = Self {
118 factorials_found: true,
119 ..Self::NONE
120 };
121}
122
123#[derive(
124 Debug, Copy, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord, Serialize, Deserialize,
125)]
126pub struct Commands {
127 pub shorten: bool,
129 pub steps: bool,
131 pub termial: bool,
133 pub no_note: bool,
135 pub post_only: bool,
136}
137impl_all_bitwise!(Commands {
138 shorten,
139 steps,
140 termial,
141 no_note,
142 post_only,
143});
144#[allow(dead_code)]
145impl Commands {
146 pub const NONE: Self = Self {
147 shorten: false,
148 steps: false,
149 termial: false,
150 no_note: false,
151 post_only: false,
152 };
153 pub const SHORTEN: Self = Self {
154 shorten: true,
155 ..Self::NONE
156 };
157 pub const STEPS: Self = Self {
158 steps: true,
159 ..Self::NONE
160 };
161 pub const TERMIAL: Self = Self {
162 termial: true,
163 ..Self::NONE
164 };
165 pub const NO_NOTE: Self = Self {
166 no_note: true,
167 ..Self::NONE
168 };
169 pub const POST_ONLY: Self = Self {
170 post_only: true,
171 ..Self::NONE
172 };
173}
174
175impl Commands {
176 fn contains_command_format(text: &str, command: &str) -> bool {
177 let pattern1 = format!("\\[{command}\\]");
178 let pattern2 = format!("[{command}]");
179 let pattern3 = format!("!{command}");
180 text.contains(&pattern1) || text.contains(&pattern2) || text.contains(&pattern3)
181 }
182
183 pub fn from_comment_text(text: &str) -> Self {
184 Self {
185 shorten: Self::contains_command_format(text, "short")
186 || Self::contains_command_format(text, "shorten"),
187 steps: Self::contains_command_format(text, "steps")
188 || Self::contains_command_format(text, "all"),
189 termial: Self::contains_command_format(text, "termial")
190 || Self::contains_command_format(text, "triangle"),
191 no_note: Self::contains_command_format(text, "no note")
192 || Self::contains_command_format(text, "no_note"),
193 post_only: false,
194 }
195 }
196 pub fn overrides_from_comment_text(text: &str) -> Self {
197 Self {
198 shorten: !Self::contains_command_format(text, "long"),
199 steps: !(Self::contains_command_format(text, "no steps")
200 | Self::contains_command_format(text, "no_steps")),
201 termial: !(Self::contains_command_format(text, "no termial")
202 | Self::contains_command_format(text, "no_termial")),
203 no_note: !Self::contains_command_format(text, "note"),
204 post_only: true,
205 }
206 }
207}
208
209const FOOTER_TEXT: &str = "\n*^(This action was performed by a bot.)*";
210
211macro_rules! contains_comb {
212 ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
214 $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end,$($end_rest),*]) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
215 };
216 (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
218 $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
219 };
220 ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
222 $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
223 };
224 ($var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
226 $var.contains(concat!($start, $end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
227 };
228 (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
230 $var.contains(concat!($start,$end))
231 };
232 (@inner $var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
234 $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
235 };
236 ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
238 $var.contains(concat!($start, $end))
239 };
240 (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
242 $var.contains(concat!($start,$end))
243 };
244}
245
246impl<Meta> CommentConstructed<Meta> {
247 pub fn new(comment_text: &str, meta: Meta, pre_commands: Commands, max_length: usize) -> Self {
249 let command_overrides = Commands::overrides_from_comment_text(comment_text);
250 let commands: Commands =
251 (Commands::from_comment_text(comment_text) | pre_commands) & command_overrides;
252
253 let mut status: Status = Default::default();
254
255 let text = if Self::might_have_factorial(comment_text) {
256 comment_text.to_owned()
257 } else {
258 status.no_factorial = true;
259 String::new()
260 };
261
262 Comment {
263 meta,
264 notify: None,
265 calculation_list: text,
266 status,
267 commands,
268 max_length: max_length - FOOTER_TEXT.len() - 10,
269 }
270 }
271
272 fn might_have_factorial(text: &str) -> bool {
273 contains_comb!(
274 text,
275 [
276 "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ")", "e", "pi", "phi", "tau",
277 "π", "ɸ", "τ"
278 ],
279 ["!", "?"]
280 ) || contains_comb!(
281 text,
282 ["!"],
283 [
284 "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "(", "e", "pi", "phi", "tau",
285 "π", "ɸ", "τ"
286 ]
287 )
288 }
289
290 pub fn extract(self) -> CommentExtracted<Meta> {
292 let Comment {
293 meta,
294 calculation_list: comment_text,
295 notify,
296 mut status,
297 commands,
298 max_length,
299 } = self;
300 let pending_list: Vec<CalculationJob> = parse(&comment_text, commands.termial);
301
302 if pending_list.is_empty() {
303 status.no_factorial = true;
304 }
305
306 Comment {
307 meta,
308 calculation_list: pending_list,
309 notify,
310 status,
311 commands,
312 max_length,
313 }
314 }
315
316 pub fn new_already_replied(meta: Meta, max_length: usize) -> Self {
318 let text = String::new();
319 let status: Status = Status {
320 already_replied_or_rejected: true,
321 ..Default::default()
322 };
323 let commands: Commands = Default::default();
324
325 Comment {
326 meta,
327 notify: None,
328 calculation_list: text,
329 status,
330 commands,
331 max_length: max_length - FOOTER_TEXT.len() - 10,
332 }
333 }
334}
335impl<Meta, S> Comment<Meta, S> {
336 pub fn add_status(&mut self, status: Status) {
337 self.status = self.status | status;
338 }
339}
340impl<Meta> CommentExtracted<Meta> {
341 pub fn calc(self) -> CommentCalculated<Meta> {
343 let Comment {
344 meta,
345 calculation_list: pending_list,
346 notify,
347 mut status,
348 commands,
349 max_length,
350 } = self;
351 let mut calculation_list: Vec<Calculation> = pending_list
352 .into_iter()
353 .flat_map(|calc| calc.execute(commands.steps))
354 .filter_map(|x| {
355 if x.is_none() {
356 status.number_too_big_to_calculate = true;
357 };
358 x
359 })
360 .collect();
361
362 calculation_list.sort();
363 calculation_list.dedup();
364 calculation_list.sort_by_key(|x| x.steps.len());
365
366 if calculation_list.is_empty() {
367 status.no_factorial = true;
368 } else {
369 status.factorials_found = true;
370 }
371 Comment {
372 meta,
373 calculation_list,
374 notify,
375 status,
376 commands,
377 max_length,
378 }
379 }
380}
381impl<Meta> CommentCalculated<Meta> {
382 pub fn get_reply(&self) -> String {
384 let mut note = self
385 .notify
386 .as_ref()
387 .map(|user| format!("Hey {user}! \n\n"))
388 .unwrap_or_default();
389
390 let too_big_number = Integer::u64_pow_u64(10, self.max_length as u64).complete();
391 let too_big_number = &too_big_number;
392
393 let multiple = self.calculation_list.len() > 1;
395 if !self.commands.no_note {
396 if self
397 .calculation_list
398 .iter()
399 .any(Calculation::is_digit_tower)
400 {
401 if multiple {
402 let _ = note.write_str("Some of these are so large, that I can't even give the number of digits of them, so I have to make a power of ten tower.\n\n");
403 } else {
404 let _ = note.write_str("That is so large, that I can't even give the number of digits of it, so I have to make a power of ten tower.\n\n");
405 }
406 } else if self
407 .calculation_list
408 .iter()
409 .any(Calculation::is_aproximate_digits)
410 {
411 if multiple {
412 let _ = note.write_str("Some of these are so large, that I can't even approximate them well, so I can only give you an approximation on the number of digits.\n\n");
413 } else {
414 let _ = note.write_str("That number is so large, that I can't even approximate it well, so I can only give you an approximation on the number of digits.\n\n");
415 }
416 } else if self
417 .calculation_list
418 .iter()
419 .any(Calculation::is_approximate)
420 {
421 if multiple {
422 let _ = note.write_str(
423 "Some of those are so large, that I can't calculate them, so I'll have to approximate.\n\n",
424 );
425 } else {
426 let _ = note.write_str(
427 "That is so large, that I can't calculate it, so I'll have to approximate.\n\n",
428 );
429 }
430 } else if self.calculation_list.iter().any(Calculation::is_rounded) {
431 let _ = note.write_str("I can't calculate that large factorials of decimals. So I had to round at some point.\n\n");
432 } else if self
433 .calculation_list
434 .iter()
435 .any(|c| c.is_too_long(too_big_number))
436 {
437 if multiple {
438 let _ = note.write_str("If I post the whole numbers, the comment would get too long. So I had to turn them into scientific notation.\n\n");
439 } else {
440 let _ = note.write_str("If I post the whole number, the comment would get too long. So I had to turn it into scientific notation.\n\n");
441 }
442 }
443 }
444
445 let mut reply = self
447 .calculation_list
448 .iter()
449 .fold(note.clone(), |mut acc, factorial| {
450 let _ = factorial.format(&mut acc, self.commands.shorten, false, too_big_number);
451 acc
452 });
453
454 if reply.len() > self.max_length
456 && !self.commands.shorten
457 && !self
458 .calculation_list
459 .iter()
460 .all(|fact| fact.is_too_long(too_big_number))
461 {
462 if note.is_empty() && !self.commands.no_note {
463 let _ = note.write_str("If I post the whole numbers, the comment would get too long. So I had to turn them into scientific notation.\n\n");
464 };
465 reply = self
466 .calculation_list
467 .iter()
468 .fold(note, |mut acc, factorial| {
469 let _ = factorial.format(&mut acc, true, false, too_big_number);
470 acc
471 });
472 }
473
474 let note = "If I posted all numbers, the comment would get too long. So I had to remove some of them. \n\n";
476 if reply.len() > self.max_length {
477 let mut factorial_list: Vec<String> = self
478 .calculation_list
479 .iter()
480 .map(|fact| {
481 let mut res = String::new();
482 let _ = fact.format(&mut res, true, false, too_big_number);
483 res
484 })
485 .collect();
486 'drop_last: {
487 while note.len() + factorial_list.iter().map(|s| s.len()).sum::<usize>()
488 > self.max_length
489 {
490 factorial_list.pop();
492 if factorial_list.is_empty() {
493 if self.calculation_list.len() == 1 {
494 let note = "That is so large, I can't even fit it in a comment with a power of 10 tower, so I'll have to use tetration!\n\n";
495 reply = self.calculation_list.iter().fold(
496 note.to_string(),
497 |mut acc, factorial| {
498 let _ = factorial.format(&mut acc, true, true, too_big_number);
499 acc
500 },
501 );
502 if reply.len() <= self.max_length {
503 break 'drop_last;
504 }
505 }
506 reply = "Sorry, but the reply text for all those number would be _really_ long, so I'd rather not even try posting lmao\n".to_string();
507 break 'drop_last;
508 }
509 }
510 reply = factorial_list
511 .iter()
512 .fold(note.to_string(), |acc, factorial| {
513 format!("{acc}{factorial}")
514 });
515 }
516 }
517
518 reply.push_str(FOOTER_TEXT);
519 reply
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use crate::{
526 calculation_results::Number,
527 calculation_tasks::{CalculationBase, CalculationJob},
528 };
529
530 const MAX_LENGTH: usize = 10_000;
531
532 use super::*;
533
534 type Comment<S> = super::Comment<(), S>;
535
536 #[test]
537 fn test_extraction_dedup() {
538 let _ = crate::init_default();
539 let jobs = parse("24! -24! 2!? (2!?)!", true);
540 assert_eq!(
541 jobs,
542 [
543 CalculationJob {
544 base: CalculationBase::Num(Number::Exact(24.into())),
545 level: 1,
546 negative: 0
547 },
548 CalculationJob {
549 base: CalculationBase::Num(Number::Exact(24.into())),
550 level: 1,
551 negative: 1
552 },
553 CalculationJob {
554 base: CalculationBase::Calc(Box::new(CalculationJob {
555 base: CalculationBase::Num(Number::Exact(2.into())),
556 level: 1,
557 negative: 0
558 })),
559 level: -1,
560 negative: 0
561 },
562 CalculationJob {
563 base: CalculationBase::Calc(Box::new(CalculationJob {
564 base: CalculationBase::Calc(Box::new(CalculationJob {
565 base: CalculationBase::Num(Number::Exact(2.into())),
566 level: 1,
567 negative: 0
568 })),
569 level: -1,
570 negative: 0
571 })),
572 level: 1,
573 negative: 0
574 }
575 ]
576 );
577 }
578
579 #[test]
580 fn test_commands_from_comment_text() {
581 let _ = crate::init_default();
582 let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note");
583 assert!(cmd1.shorten);
584 assert!(cmd1.steps);
585 assert!(cmd1.termial);
586 assert!(cmd1.no_note);
587 assert!(!cmd1.post_only);
588 let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note]");
589 assert!(cmd2.shorten);
590 assert!(cmd2.steps);
591 assert!(cmd2.termial);
592 assert!(cmd2.no_note);
593 assert!(!cmd2.post_only);
594 let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\]";
595 let cmd3 = Commands::from_comment_text(comment);
596 assert!(cmd3.shorten);
597 assert!(cmd3.steps);
598 assert!(cmd3.termial);
599 assert!(cmd3.no_note);
600 assert!(!cmd3.post_only);
601 let cmd4 = Commands::from_comment_text("shorten all triangle no_note");
602 assert!(!cmd4.shorten);
603 assert!(!cmd4.steps);
604 assert!(!cmd4.termial);
605 assert!(!cmd4.no_note);
606 assert!(!cmd4.post_only);
607 }
608
609 #[test]
610 fn test_commands_overrides_from_comment_text() {
611 let _ = crate::init_default();
612 let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note");
613 assert!(cmd1.shorten);
614 assert!(cmd1.steps);
615 assert!(cmd1.termial);
616 assert!(cmd1.no_note);
617 assert!(cmd1.post_only);
618 }
619
620 #[test]
621 fn test_might_have_factorial() {
622 let _ = crate::init_default();
623 assert!(Comment::might_have_factorial("5!"));
624 assert!(Comment::might_have_factorial("3?"));
625 assert!(!Comment::might_have_factorial("!?"));
626 }
627
628 #[test]
629 fn test_new_already_replied() {
630 let _ = crate::init_default();
631 let comment = Comment::new_already_replied((), MAX_LENGTH);
632 assert_eq!(comment.calculation_list, "");
633 assert!(comment.status.already_replied_or_rejected);
634 }
635}