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