Skip to main content

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, FormatOptions};
11use crate::calculation_tasks::{CalculationBase, CalculationJob};
12use crate::parse::parse;
13
14use std::fmt::Write;
15use std::ops::*;
16#[macro_export]
17macro_rules! impl_bitwise {
18    ($s_name:ident {$($s_fields:ident),*}, $t_name:ident, $fn_name:ident) => {
19        impl $t_name for $s_name {
20            type Output = Self;
21            fn $fn_name(self, rhs: Self) -> Self {
22                Self {
23                    $($s_fields: self.$s_fields.$fn_name(rhs.$s_fields),)*
24                }
25            }
26        }
27    };
28}
29#[macro_export]
30macro_rules! impl_all_bitwise {
31    ($s_name:ident {$($s_fields:ident,)*}) => {impl_all_bitwise!($s_name {$($s_fields),*});};
32    ($s_name:ident {$($s_fields:ident),*}) => {
33        impl_bitwise!($s_name {$($s_fields),*}, BitOr, bitor);
34        impl_bitwise!($s_name {$($s_fields),*}, BitXor, bitxor);
35        impl_bitwise!($s_name {$($s_fields),*}, BitAnd, bitand);
36        impl Not for $s_name {
37            type Output = Self;
38            fn not(self) -> Self {
39                Self {
40                    $($s_fields: self.$s_fields.not(),)*
41                }
42            }
43        }
44    };
45}
46
47/// The primary abstraction.
48/// Construct -> Extract -> Calculate -> Get Reply
49///
50/// Uses a generic for Metadata (meta).
51///
52/// Uses three type-states exposed as the aliases [CommentConstructed], [CommentExtracted], and [CommentCalculated].
53#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
54#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
55pub struct Comment<Meta, S> {
56    /// Metadata (generic)
57    pub meta: Meta,
58    /// Data for the current step
59    pub calculation_list: S,
60    /// If Some will prepend a "Hey {string}!" to the reply.
61    pub notify: Option<String>,
62    pub status: Status,
63    pub commands: Commands,
64    /// How long the reply may at most be
65    pub max_length: usize,
66    pub locale: String,
67}
68/// Base [Comment], contains the comment text, if it might have a calculation. Use [extract](Comment::extract).
69pub type CommentConstructed<Meta> = Comment<Meta, String>;
70/// Extracted [Comment], contains the calculations to be done. Use [calc](Comment::calc).
71pub type CommentExtracted<Meta> = Comment<Meta, Vec<CalculationJob>>;
72/// Calculated [Comment], contains the results along with how we go to them. Use [get_reply](Comment::get_reply).
73pub type CommentCalculated<Meta> = Comment<Meta, Vec<Calculation>>;
74
75#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
76#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
77#[non_exhaustive]
78pub struct Status {
79    pub already_replied_or_rejected: bool,
80    pub not_replied: bool,
81    pub number_too_big_to_calculate: bool,
82    pub no_factorial: bool,
83    pub reply_would_be_too_long: bool,
84    pub factorials_found: bool,
85    pub limit_hit: bool,
86    pub nested_used: bool,
87}
88
89impl_all_bitwise!(Status {
90    already_replied_or_rejected,
91    not_replied,
92    number_too_big_to_calculate,
93    no_factorial,
94    reply_would_be_too_long,
95    factorials_found,
96    limit_hit,
97    nested_used,
98});
99#[allow(dead_code)]
100impl Status {
101    pub const NONE: Self = Self {
102        already_replied_or_rejected: false,
103        not_replied: false,
104        number_too_big_to_calculate: false,
105        no_factorial: false,
106        reply_would_be_too_long: false,
107        factorials_found: false,
108        limit_hit: false,
109        nested_used: false,
110    };
111    pub const ALREADY_REPLIED_OR_REJECTED: Self = Self {
112        already_replied_or_rejected: true,
113        ..Self::NONE
114    };
115    pub const NOT_REPLIED: Self = Self {
116        not_replied: true,
117        ..Self::NONE
118    };
119    pub const NUMBER_TOO_BIG_TO_CALCULATE: Self = Self {
120        number_too_big_to_calculate: true,
121        ..Self::NONE
122    };
123    pub const NO_FACTORIAL: Self = Self {
124        no_factorial: true,
125        ..Self::NONE
126    };
127    pub const REPLY_WOULD_BE_TOO_LONG: Self = Self {
128        reply_would_be_too_long: true,
129        ..Self::NONE
130    };
131    pub const FACTORIALS_FOUND: Self = Self {
132        factorials_found: true,
133        ..Self::NONE
134    };
135    pub const LIMIT_HIT: Self = Self {
136        limit_hit: true,
137        ..Self::NONE
138    };
139    pub const NESTED_USED: Self = Self {
140        nested_used: true,
141        ..Self::NONE
142    };
143}
144
145#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
146#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
147#[non_exhaustive]
148pub struct Commands {
149    /// Turn all integers into scientific notiation if that makes them shorter.
150    #[cfg_attr(any(feature = "serde", test), serde(default))]
151    pub shorten: bool,
152    /// Return all the intermediate results for nested calculations.
153    #[cfg_attr(any(feature = "serde", test), serde(default))]
154    pub steps: bool,
155    /// Interpret multi-ops as nested ops.
156    #[cfg_attr(any(feature = "serde", test), serde(default))]
157    pub nested: bool,
158    /// Parse and calculate termials.
159    #[cfg_attr(any(feature = "serde", test), serde(default))]
160    pub termial: bool,
161    /// Disable the beginning note.
162    #[cfg_attr(any(feature = "serde", test), serde(default))]
163    pub no_note: bool,
164    /// Write out the number as a word if possible.
165    #[cfg_attr(any(feature = "serde", test), serde(default))]
166    pub write_out: bool,
167}
168impl_all_bitwise!(Commands {
169    shorten,
170    steps,
171    nested,
172    termial,
173    no_note,
174    write_out,
175});
176#[allow(dead_code)]
177impl Commands {
178    pub const NONE: Self = Self {
179        shorten: false,
180        steps: false,
181        nested: false,
182        termial: false,
183        no_note: false,
184        write_out: false,
185    };
186    pub const SHORTEN: Self = Self {
187        shorten: true,
188        ..Self::NONE
189    };
190    pub const STEPS: Self = Self {
191        steps: true,
192        ..Self::NONE
193    };
194    pub const NESTED: Self = Self {
195        nested: true,
196        ..Self::NONE
197    };
198    pub const TERMIAL: Self = Self {
199        termial: true,
200        ..Self::NONE
201    };
202    pub const NO_NOTE: Self = Self {
203        no_note: true,
204        ..Self::NONE
205    };
206    pub const WRITE_OUT: Self = Self {
207        write_out: true,
208        ..Self::NONE
209    };
210}
211
212impl Commands {
213    fn contains_command_format(text: &str, command: &str) -> bool {
214        let pattern1 = format!("\\[{command}\\]");
215        let pattern2 = format!("[{command}]");
216        let pattern3 = format!("!{command}");
217        text.contains(&pattern1) || text.contains(&pattern2) || text.contains(&pattern3)
218    }
219
220    pub fn from_comment_text(text: &str) -> Self {
221        Self {
222            shorten: Self::contains_command_format(text, "short")
223                || Self::contains_command_format(text, "shorten"),
224            steps: Self::contains_command_format(text, "steps")
225                || Self::contains_command_format(text, "all"),
226            nested: Self::contains_command_format(text, "nest")
227                || Self::contains_command_format(text, "nested")
228                || Self::contains_command_format(text, "no multi")
229                || Self::contains_command_format(text, "no\\_multi")
230                || Self::contains_command_format(text, "no_multi"),
231            termial: Self::contains_command_format(text, "termial")
232                || Self::contains_command_format(text, "triangle"),
233            no_note: Self::contains_command_format(text, "no note")
234                || Self::contains_command_format(text, "no\\_note")
235                || Self::contains_command_format(text, "no_note"),
236            write_out: Self::contains_command_format(text, "write_out")
237                || Self::contains_command_format(text, "write\\_out")
238                || Self::contains_command_format(text, "write_num")
239                || Self::contains_command_format(text, "write\\_num"),
240        }
241    }
242    pub fn overrides_from_comment_text(text: &str) -> Self {
243        Self {
244            shorten: !Self::contains_command_format(text, "long"),
245            steps: !(Self::contains_command_format(text, "no steps")
246                || Self::contains_command_format(text, "no_steps")
247                || Self::contains_command_format(text, "no\\_steps")),
248            nested: !(Self::contains_command_format(text, "no_nest")
249                || Self::contains_command_format(text, "no\\_nest")
250                || Self::contains_command_format(text, "multi")),
251            termial: !(Self::contains_command_format(text, "no termial")
252                || Self::contains_command_format(text, "no_termial")
253                || Self::contains_command_format(text, "no\\_termial")),
254            no_note: !Self::contains_command_format(text, "note"),
255            write_out: !(Self::contains_command_format(text, "dont_write_out")
256                || Self::contains_command_format(text, "dont\\_write\\_out")
257                || Self::contains_command_format(text, "normal num")
258                || Self::contains_command_format(text, "normal\\_num")),
259        }
260    }
261}
262
263macro_rules! contains_comb {
264    // top level (advance both separately)
265    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
266        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end,$($end_rest),*]) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
267    };
268    // inner (advance only end)
269    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
270        $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
271    };
272    // top level (advance both separately) singular end (advance only start)
273    ($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
274        $var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
275    };
276    // top level (advance both separately) singular start (advance only end)
277    ($var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
278        $var.contains(concat!($start, $end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
279    };
280    // inner (advance only end) singular end (advance only start, so nothing)
281    (@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
282        $var.contains(concat!($start,$end))
283    };
284    // inner (advance only end) singular end (advance only end)
285    (@inner $var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
286        $var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
287    };
288    // top level (advance both separately) singular start and end (no advance)
289    ($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
290        $var.contains(concat!($start, $end))
291    };
292    // inner (advance only end) singular start and end (no advance)
293    (@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
294        $var.contains(concat!($start,$end))
295    };
296}
297
298impl<Meta> CommentConstructed<Meta> {
299    /// Takes a raw comment, finds the factorials and commands, and packages it, also checks if it might have something to calculate.
300    pub fn new(
301        comment_text: &str,
302        meta: Meta,
303        pre_commands: Commands,
304        max_length: usize,
305        locale: &str,
306    ) -> Self {
307        let command_overrides = Commands::overrides_from_comment_text(comment_text);
308        let commands: Commands =
309            (Commands::from_comment_text(comment_text) | pre_commands) & command_overrides;
310
311        let mut status: Status = Default::default();
312        if Commands::contains_command_format(comment_text, "nest")
313            || Commands::contains_command_format(comment_text, "nested")
314            || Commands::contains_command_format(comment_text, "no_nest")
315            || Commands::contains_command_format(comment_text, "no\\_nest")
316        {
317            status.nested_used = true;
318        }
319
320        let text = if Self::might_have_factorial(comment_text) {
321            comment_text.to_owned()
322        } else {
323            status.no_factorial = true;
324            String::new()
325        };
326
327        Comment {
328            meta,
329            notify: None,
330            calculation_list: text,
331            status,
332            commands,
333            max_length,
334            locale: locale.to_owned(),
335        }
336    }
337
338    fn might_have_factorial(text: &str) -> bool {
339        contains_comb!(
340            text,
341            [
342                "0",
343                "1",
344                "2",
345                "3",
346                "4",
347                "5",
348                "6",
349                "7",
350                "8",
351                "9",
352                ")",
353                "e",
354                "pi",
355                "phi",
356                "tau",
357                "π",
358                "ɸ",
359                "τ",
360                "infinity",
361                "inf",
362                "∞\u{303}",
363                "∞"
364            ],
365            ["!", "?"]
366        ) || contains_comb!(
367            text,
368            ["!"],
369            [
370                "0",
371                "1",
372                "2",
373                "3",
374                "4",
375                "5",
376                "6",
377                "7",
378                "8",
379                "9",
380                "(",
381                "e",
382                "pi",
383                "phi",
384                "tau",
385                "π",
386                "ɸ",
387                "τ",
388                "infinity",
389                "inf",
390                "∞\u{303}",
391                "∞"
392            ]
393        )
394    }
395
396    /// Extracts the calculations using [parse](mod@crate::parse).
397    pub fn extract(self, consts: &Consts) -> CommentExtracted<Meta> {
398        let Comment {
399            meta,
400            calculation_list: comment_text,
401            notify,
402            mut status,
403            commands,
404            max_length,
405            locale,
406        } = self;
407        let mut pending_list: Vec<CalculationJob> = parse(
408            &comment_text,
409            commands.termial,
410            consts,
411            &consts
412                .locales
413                .get(&locale)
414                .unwrap_or(consts.locales.get(&consts.default_locale).unwrap())
415                .format
416                .number_format,
417        );
418
419        if commands.nested {
420            for calc in &mut pending_list {
421                Self::multi_to_nested(calc);
422            }
423        }
424
425        if pending_list.is_empty() {
426            status.no_factorial = true;
427        }
428
429        Comment {
430            meta,
431            calculation_list: pending_list,
432            notify,
433            status,
434            commands,
435            max_length,
436            locale,
437        }
438    }
439
440    fn multi_to_nested(mut calc: &mut CalculationJob) {
441        loop {
442            let level = calc.level.clamp(-1, 1);
443            let depth = calc.level.abs();
444            calc.level = level;
445            for _ in 1..depth {
446                let base = std::mem::replace(
447                    &mut calc.base,
448                    CalculationBase::Num(
449                        crate::calculation_results::CalculationResult::ComplexInfinity,
450                    ),
451                );
452                let new_base = CalculationBase::Calc(Box::new(CalculationJob {
453                    base,
454                    level,
455                    negative: 0,
456                }));
457                let _ = std::mem::replace(&mut calc.base, new_base);
458            }
459            let CalculationBase::Calc(next) = &mut calc.base else {
460                return;
461            };
462            calc = next;
463        }
464    }
465
466    /// Constructs an empty comment with [Status] already_replied_or_rejected set.
467    pub fn new_already_replied(meta: Meta, max_length: usize, locale: &str) -> Self {
468        let text = String::new();
469        let status: Status = Status {
470            already_replied_or_rejected: true,
471            ..Default::default()
472        };
473        let commands: Commands = Default::default();
474
475        Comment {
476            meta,
477            notify: None,
478            calculation_list: text,
479            status,
480            commands,
481            max_length,
482            locale: locale.to_owned(),
483        }
484    }
485}
486impl<Meta, S> Comment<Meta, S> {
487    pub fn add_status(&mut self, status: Status) {
488        self.status = self.status | status;
489    }
490}
491impl<Meta> CommentExtracted<Meta> {
492    /// Does the calculations using [calculation_tasks](crate::calculation_tasks).
493    pub fn calc(self, consts: &Consts) -> CommentCalculated<Meta> {
494        let Comment {
495            meta,
496            calculation_list: pending_list,
497            notify,
498            mut status,
499            commands,
500            max_length,
501            locale,
502        } = self;
503        let mut calculation_list: Vec<Calculation> = pending_list
504            .into_iter()
505            .flat_map(|calc| calc.execute(commands.steps, consts))
506            .filter_map(|x| {
507                if x.is_none() {
508                    status.number_too_big_to_calculate = true;
509                };
510                x
511            })
512            .collect();
513
514        calculation_list.sort();
515        calculation_list.dedup();
516        calculation_list.sort_by_key(|x| x.steps.len());
517
518        if calculation_list.is_empty() {
519            status.no_factorial = true;
520        } else {
521            status.factorials_found = true;
522        }
523        Comment {
524            meta,
525            calculation_list,
526            notify,
527            status,
528            commands,
529            max_length,
530            locale,
531        }
532    }
533}
534impl<Meta> CommentCalculated<Meta> {
535    /// Does the formatting for the reply using [calculation_result](crate::calculation_results).
536    pub fn get_reply(&self, consts: &Consts) -> String {
537        let mut fell_back = false;
538        let locale = consts.locales.get(&self.locale).unwrap_or_else(|| {
539            fell_back = true;
540            consts.locales.get(&consts.default_locale).unwrap()
541        });
542        let mut note = self
543            .notify
544            .as_ref()
545            .map(|user| locale.notes.mention.replace("{mention}", user) + "\n\n")
546            .unwrap_or_default();
547
548        if fell_back {
549            let _ = note.write_str("Sorry, I currently don't speak ");
550            let _ = note.write_str(&self.locale);
551            let _ = note.write_str(". Maybe you could [teach me](https://github.com/tolik518/factorion-bot/blob/master/CONTRIBUTING.md#translation)? \n\n");
552        }
553
554        let too_big_number = Integer::u64_pow_u64(10, self.max_length as u64).complete();
555        let too_big_number = &too_big_number;
556
557        let multiple = self.add_note(consts, locale, &mut note, too_big_number);
558
559        // Add Factorials
560        let mut reply = self.add_factorials(
561            consts,
562            locale,
563            &note,
564            too_big_number,
565            FormatOptions {
566                force_shorten: self.commands.shorten,
567                write_out: self.commands.write_out,
568                ..FormatOptions::NONE
569            },
570        );
571
572        // If the reply was too long try force shortening all factorials
573        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length
574            && !self.commands.shorten
575            && !self
576                .calculation_list
577                .iter()
578                .all(|fact| fact.is_too_long(too_big_number))
579        {
580            if note.is_empty() && !self.commands.no_note {
581                if multiple {
582                    let _ = note.write_str(&locale.notes.too_big_mult);
583                } else {
584                    let _ = note.write_str(&locale.notes.too_big);
585                }
586                let _ = note.write_str("\n\n");
587            };
588            reply = self.add_factorials(
589                consts,
590                locale,
591                &note,
592                too_big_number,
593                FormatOptions {
594                    write_out: self.commands.write_out,
595                    ..FormatOptions::FORCE_SHORTEN
596                },
597            );
598        }
599
600        let note = if !self.commands.no_note {
601            locale.notes.tetration.clone().into_owned() + "\n\n"
602        } else {
603            String::new()
604        };
605        // If the reply was too long try agressive shortening all factorials
606        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length && !self.commands.steps
607        {
608            reply = self.add_factorials(
609                consts,
610                locale,
611                &note,
612                too_big_number,
613                FormatOptions {
614                    write_out: self.commands.write_out,
615                    ..{ FormatOptions::FORCE_SHORTEN | FormatOptions::AGRESSIVE_SHORTEN }
616                },
617            );
618        }
619
620        let note = if !self.commands.no_note {
621            locale.notes.remove.clone().into_owned() + "\n\n"
622        } else {
623            String::new()
624        };
625        // Remove factorials until we can fit them in a comment
626        self.add_factorials_to_fit(consts, locale, too_big_number, &mut reply, note);
627        if !locale.bot_disclaimer.is_empty() {
628            reply.push_str("\n*^(");
629            reply.push_str(&locale.bot_disclaimer);
630            reply.push_str(")*");
631        }
632        reply
633    }
634
635    fn add_factorials_to_fit(
636        &self,
637        consts: &Consts<'_>,
638        locale: &crate::locale::Locale<'_>,
639        too_big_number: &Integer,
640        reply: &mut String,
641        note: String,
642    ) {
643        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length {
644            let mut factorial_list: Vec<String> = self
645                .calculation_list
646                .iter()
647                .map(|fact| {
648                    let mut res = String::new();
649                    let _ = fact.format(
650                        &mut res,
651                        FormatOptions {
652                            agressive_shorten: !self.commands.steps,
653                            write_out: self.commands.write_out,
654                            ..FormatOptions::FORCE_SHORTEN
655                        },
656                        too_big_number,
657                        consts,
658                        &locale.format,
659                    );
660                    res
661                })
662                .collect();
663            'drop_last: {
664                while note.len()
665                    + factorial_list.iter().map(|s| s.len()).sum::<usize>()
666                    + locale.bot_disclaimer.len()
667                    + 16
668                    > self.max_length
669                {
670                    // remove last factorial (probably the biggest)
671                    factorial_list.pop();
672                    if factorial_list.is_empty() {
673                        *reply = locale.notes.no_post.to_string();
674                        break 'drop_last;
675                    }
676                }
677                *reply = factorial_list.iter().fold(note, |mut acc, factorial| {
678                    let _ = acc.write_str(factorial);
679                    acc
680                });
681            }
682        }
683    }
684
685    fn add_factorials(
686        &self,
687        consts: &Consts<'_>,
688        locale: &crate::locale::Locale<'_>,
689        note: &str,
690        too_big_number: &Integer,
691        format_options: FormatOptions,
692    ) -> String {
693        self.calculation_list
694            .iter()
695            .fold(note.to_owned(), |mut acc, factorial| {
696                let _ = factorial.format(
697                    &mut acc,
698                    format_options.clone(),
699                    too_big_number,
700                    consts,
701                    &locale.format,
702                );
703                acc
704            })
705    }
706
707    fn add_note(
708        &self,
709        consts: &Consts<'_>,
710        locale: &crate::locale::Locale<'_>,
711        note: &mut String,
712        too_big_number: &Integer,
713    ) -> bool {
714        let multiple = self.calculation_list.len() > 1;
715        if self.status.nested_used {
716            let _ = note.write_str(locale.notes.nested_used
717                .as_ref()
718                .map(AsRef::as_ref)
719                .unwrap_or("The command \"nested\" is now \"no_multi\", which is way more clear in what it means. I'll ignore the former at some point in the future.")
720            );
721            let _ = note.write_str("\n\n");
722        }
723        if !self.commands.no_note {
724            if self.status.limit_hit {
725                let _ = note.write_str(
726                    locale
727                        .notes
728                        .limit_hit
729                        .as_ref()
730                        .map(AsRef::as_ref)
731                        .unwrap_or(
732                            "I have repeated myself enough, I won't do that calculation again.",
733                        ),
734                );
735                let _ = note.write_str("\n\n");
736            } else if self
737                .calculation_list
738                .iter()
739                .any(Calculation::is_digit_tower)
740            {
741                if multiple {
742                    let _ = note.write_str(&locale.notes.tower_mult);
743                    let _ = note.write_str("\n\n");
744                } else {
745                    let _ = note.write_str(&locale.notes.tower);
746                    let _ = note.write_str("\n\n");
747                }
748            } else if self
749                .calculation_list
750                .iter()
751                .any(Calculation::is_aproximate_digits)
752            {
753                if multiple {
754                    let _ = note.write_str(&locale.notes.digits_mult);
755                    let _ = note.write_str("\n\n");
756                } else {
757                    let _ = note.write_str(&locale.notes.digits);
758                    let _ = note.write_str("\n\n");
759                }
760            } else if self
761                .calculation_list
762                .iter()
763                .any(Calculation::is_approximate)
764            {
765                if multiple {
766                    let _ = note.write_str(&locale.notes.approx_mult);
767                    let _ = note.write_str("\n\n");
768                } else {
769                    let _ = note.write_str(&locale.notes.approx);
770                    let _ = note.write_str("\n\n");
771                }
772            } else if self.calculation_list.iter().any(Calculation::is_rounded) {
773                if multiple {
774                    let _ = note.write_str(&locale.notes.round_mult);
775                    let _ = note.write_str("\n\n");
776                } else {
777                    let _ = note.write_str(&locale.notes.round);
778                    let _ = note.write_str("\n\n");
779                }
780            } else if self
781                .calculation_list
782                .iter()
783                .any(|c| c.is_too_long(too_big_number))
784                && !(self.commands.write_out
785                    && self
786                        .calculation_list
787                        .iter()
788                        .all(|c| c.can_write_out(consts.float_precision)))
789            {
790                if multiple {
791                    let _ = note.write_str(&locale.notes.too_big_mult);
792                    let _ = note.write_str("\n\n");
793                } else {
794                    let _ = note.write_str(&locale.notes.too_big);
795                    let _ = note.write_str("\n\n");
796                }
797            } else if self.commands.write_out && self.locale != "en" {
798                let _ =
799                    note.write_str("I can only write out numbers in english, so I will do that.");
800                let _ = note.write_str("\n\n");
801            }
802        }
803        multiple
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use crate::{
810        calculation_results::Number,
811        calculation_tasks::{CalculationBase, CalculationJob},
812        locale::NumFormat,
813    };
814
815    const MAX_LENGTH: usize = 10_000;
816
817    use super::*;
818
819    type Comment<S> = super::Comment<(), S>;
820
821    #[test]
822    fn test_extraction_dedup() {
823        let consts = Consts::default();
824        let jobs = parse(
825            "24! -24! 2!? (2!?)!",
826            true,
827            &consts,
828            &NumFormat { decimal: '.' },
829        );
830        assert_eq!(
831            jobs,
832            [
833                CalculationJob {
834                    base: CalculationBase::Num(Number::Exact(24.into())),
835                    level: 1,
836                    negative: 0
837                },
838                CalculationJob {
839                    base: CalculationBase::Num(Number::Exact(24.into())),
840                    level: 1,
841                    negative: 1
842                },
843                CalculationJob {
844                    base: CalculationBase::Calc(Box::new(CalculationJob {
845                        base: CalculationBase::Num(Number::Exact(2.into())),
846                        level: 1,
847                        negative: 0
848                    })),
849                    level: -1,
850                    negative: 0
851                },
852                CalculationJob {
853                    base: CalculationBase::Calc(Box::new(CalculationJob {
854                        base: CalculationBase::Calc(Box::new(CalculationJob {
855                            base: CalculationBase::Num(Number::Exact(2.into())),
856                            level: 1,
857                            negative: 0
858                        })),
859                        level: -1,
860                        negative: 0
861                    })),
862                    level: 1,
863                    negative: 0
864                }
865            ]
866        );
867    }
868
869    #[test]
870    fn test_commands_from_comment_text() {
871        let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note !nested");
872        assert!(cmd1.shorten);
873        assert!(cmd1.steps);
874        assert!(cmd1.termial);
875        assert!(cmd1.no_note);
876        assert!(cmd1.nested);
877        let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note] [nest]");
878        assert!(cmd2.shorten);
879        assert!(cmd2.steps);
880        assert!(cmd2.termial);
881        assert!(cmd2.no_note);
882        assert!(cmd2.nested);
883        let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\] \[nest\]";
884        let cmd3 = Commands::from_comment_text(comment);
885        assert!(cmd3.shorten);
886        assert!(cmd3.steps);
887        assert!(cmd3.termial);
888        assert!(cmd3.no_note);
889        assert!(cmd3.nested);
890        let cmd4 = Commands::from_comment_text("shorten all triangle no_note nest");
891        assert!(!cmd4.shorten);
892        assert!(!cmd4.steps);
893        assert!(!cmd4.termial);
894        assert!(!cmd4.no_note);
895        assert!(!cmd4.nested);
896    }
897
898    #[test]
899    fn test_commands_overrides_from_comment_text() {
900        let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note multi");
901        assert!(cmd1.shorten);
902        assert!(cmd1.steps);
903        assert!(cmd1.termial);
904        assert!(cmd1.no_note);
905        assert!(cmd1.nested);
906    }
907
908    #[test]
909    fn test_might_have_factorial() {
910        assert!(Comment::might_have_factorial("5!"));
911        assert!(Comment::might_have_factorial("3?"));
912        assert!(!Comment::might_have_factorial("!?"));
913    }
914
915    #[test]
916    fn test_new_already_replied() {
917        let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
918        assert_eq!(comment.calculation_list, "");
919        assert!(comment.status.already_replied_or_rejected);
920    }
921
922    #[test]
923    fn test_locale_fallback_note() {
924        let consts = Consts::default();
925        let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
926            .extract(&consts)
927            .calc(&consts);
928        let reply = comment.get_reply(&consts);
929        assert_eq!(
930            reply,
931            "Sorry, I currently don't speak n/a. Maybe you could [teach me](https://github.com/tolik518/factorion-bot/blob/master/CONTRIBUTING.md#translation)? \n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*"
932        );
933    }
934
935    #[test]
936    fn test_limit_hit_note() {
937        let consts = Consts::default();
938        let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
939            .extract(&consts)
940            .calc(&consts);
941        comment.add_status(Status::LIMIT_HIT);
942        let reply = comment.get_reply(&consts);
943        assert_eq!(
944            reply,
945            "I have repeated myself enough, I won't do that calculation again.\n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*"
946        );
947    }
948
949    #[test]
950    fn test_write_out_unsupported_note() {
951        let consts = Consts::default();
952        let comment = Comment::new("1!", (), Commands::WRITE_OUT, MAX_LENGTH, "de")
953            .extract(&consts)
954            .calc(&consts);
955        let reply = comment.get_reply(&consts);
956        assert_eq!(
957            reply,
958            "I can only write out numbers in english, so I will do that.\n\nFakultät von one ist one \n\n\n*^(Dieser Kommentar wurde automatisch geschrieben | [Quelltext](http://f.r0.fyi))*"
959        );
960    }
961
962    #[test]
963    fn test_nested_used_note() {
964        let consts = Consts::default();
965        let comment = Comment::new("n!!!!! !nested", (), Commands::NONE, MAX_LENGTH, "en")
966            .extract(&consts)
967            .calc(&consts);
968        let reply = comment.get_reply(&consts);
969        assert_eq!(
970            reply,
971            "The command \"nested\" is now \"no_multi\", which is way more clear in what it means. I'll ignore the former at some point in the future.\n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*"
972        );
973    }
974}