factorion_lib/
comment.rs

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