factorion_lib/
comment.rs

1//! Parses comments and generates the reply.
2
3use 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/// The primary abstraction.
42/// Construct -> Extract -> Calculate -> Get Reply
43///
44/// Uses a generic for Metadata (meta).
45///
46/// Uses three type-states exposed as the aliases [CommentConstructed], [CommentExtracted], and [CommentCalculated].
47#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
48pub struct Comment<Meta, S> {
49    /// Metadata (generic)
50    pub meta: Meta,
51    /// Data for the current step
52    pub calculation_list: S,
53    /// If Some will prepend a "Hey {string}!" to the reply.
54    pub notify: Option<String>,
55    pub status: Status,
56    pub commands: Commands,
57    /// How long the reply may at most be
58    pub max_length: usize,
59}
60/// Base [Comment], contains the comment text, if it might have a calculation. Use [extract](Comment::extract).
61pub type CommentConstructed<Meta> = Comment<Meta, String>;
62/// Extracted [Comment], contains the calculations to be done. Use [calc](Comment::calc).
63pub type CommentExtracted<Meta> = Comment<Meta, Vec<CalculationJob>>;
64/// Calculated [Comment], contains the results along with how we go to them. Use [get_reply](Comment::get_reply).
65pub 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    /// Turn all integers into scientific notiation if that makes them shorter.
124    pub shorten: bool,
125    /// Return all the intermediate results for nested calculations.
126    pub steps: bool,
127    /// Parse and calculate termials.
128    pub termial: bool,
129    /// Disable the beginning note.
130    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    // top level (advance both separately)
209    ($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 (advance only end)
213    (@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    // top level (advance both separately) singular end (advance only start)
217    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
218        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
219    };
220    // top level (advance both separately) singular start (advance only end)
221    ($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 (advance only end) singular end (advance only start, so nothing)
225    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
226        $var.contains(concat!($start,$end))
227    };
228    // inner (advance only end) singular end (advance only end)
229    (@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    // top level (advance both separately) singular start and end (no advance)
233    ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
234        $var.contains(concat!($start, $end))
235    };
236    // inner (advance only end) singular start and end (no advance)
237    (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
238        $var.contains(concat!($start,$end))
239    };
240}
241
242impl<Meta> CommentConstructed<Meta> {
243    /// Takes a raw comment, finds the factorials and commands, and packages it, also checks if it might have something to calculate.
244    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    /// Extracts the calculations using [parse](mod@crate::parse).
287    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    /// Constructs an empty comment with [Status] already_replied_or_rejected set.
313    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    /// Does the calculations using [calculation_tasks](crate::calculation_tasks).
338    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    /// Does the formatting for the reply using [calculation_result](crate::calculation_results).
379    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        // Add Note
390        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        // Add Factorials
442        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 the reply was too long try force shortening all factorials
451        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        // Remove factorials until we can fit them in a comment
471        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                    // remove last factorial (probably the biggest)
487                    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}