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