factorion_lib/
comment.rs

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