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        // Add Note
541        let multiple = self.calculation_list.len() > 1;
542        if !self.commands.no_note {
543            if self.status.limit_hit {
544                let _ = note.write_str(
545                    locale
546                        .notes
547                        .limit_hit
548                        .as_ref()
549                        .map(AsRef::as_ref)
550                        .unwrap_or(
551                            "I have repeated myself enough, I won't do that calculation again.",
552                        ),
553                );
554                let _ = note.write_str("\n\n");
555            } else if self
556                .calculation_list
557                .iter()
558                .any(Calculation::is_digit_tower)
559            {
560                if multiple {
561                    let _ = note.write_str(&locale.notes.tower_mult);
562                    let _ = note.write_str("\n\n");
563                } else {
564                    let _ = note.write_str(&locale.notes.tower);
565                    let _ = note.write_str("\n\n");
566                }
567            } else if self
568                .calculation_list
569                .iter()
570                .any(Calculation::is_aproximate_digits)
571            {
572                if multiple {
573                    let _ = note.write_str(&locale.notes.digits_mult);
574                    let _ = note.write_str("\n\n");
575                } else {
576                    let _ = note.write_str(&locale.notes.digits);
577                    let _ = note.write_str("\n\n");
578                }
579            } else if self
580                .calculation_list
581                .iter()
582                .any(Calculation::is_approximate)
583            {
584                if multiple {
585                    let _ = note.write_str(&locale.notes.approx_mult);
586                    let _ = note.write_str("\n\n");
587                } else {
588                    let _ = note.write_str(&locale.notes.approx);
589                    let _ = note.write_str("\n\n");
590                }
591            } else if self.calculation_list.iter().any(Calculation::is_rounded) {
592                if multiple {
593                    let _ = note.write_str(&locale.notes.round_mult);
594                    let _ = note.write_str("\n\n");
595                } else {
596                    let _ = note.write_str(&locale.notes.round);
597                    let _ = note.write_str("\n\n");
598                }
599            } else if self
600                .calculation_list
601                .iter()
602                .any(|c| c.is_too_long(too_big_number))
603                && !(self.commands.write_out
604                    && self
605                        .calculation_list
606                        .iter()
607                        .all(|c| c.can_write_out(consts.float_precision)))
608            {
609                if multiple {
610                    let _ = note.write_str(&locale.notes.too_big_mult);
611                    let _ = note.write_str("\n\n");
612                } else {
613                    let _ = note.write_str(&locale.notes.too_big);
614                    let _ = note.write_str("\n\n");
615                }
616            } else if self.commands.write_out && self.locale != "en" {
617                let _ =
618                    note.write_str("I can only write out numbers in english, so I will do that.");
619                let _ = note.write_str("\n\n");
620            }
621        }
622
623        // Add Factorials
624        let mut reply = self
625            .calculation_list
626            .iter()
627            .fold(note.clone(), |mut acc, factorial| {
628                let _ = factorial.format(
629                    &mut acc,
630                    FormatOptions {
631                        force_shorten: self.commands.shorten,
632                        write_out: self.commands.write_out,
633                        ..FormatOptions::NONE
634                    },
635                    too_big_number,
636                    consts,
637                    &locale.format,
638                );
639                acc
640            });
641
642        // If the reply was too long try force shortening all factorials
643        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length
644            && !self.commands.shorten
645            && !self
646                .calculation_list
647                .iter()
648                .all(|fact| fact.is_too_long(too_big_number))
649        {
650            if note.is_empty() && !self.commands.no_note {
651                if multiple {
652                    let _ = note.write_str(&locale.notes.too_big_mult);
653                } else {
654                    let _ = note.write_str(&locale.notes.too_big);
655                }
656                let _ = note.write_str("\n\n");
657            };
658            reply = self
659                .calculation_list
660                .iter()
661                .fold(note, |mut acc, factorial| {
662                    let _ = factorial.format(
663                        &mut acc,
664                        FormatOptions {
665                            write_out: self.commands.write_out,
666                            ..FormatOptions::FORCE_SHORTEN
667                        },
668                        too_big_number,
669                        consts,
670                        &locale.format,
671                    );
672                    acc
673                });
674        }
675
676        let note = if !self.commands.no_note {
677            locale.notes.tetration.clone().into_owned() + "\n\n"
678        } else {
679            String::new()
680        };
681        // If the reply was too long try agressive shortening all factorials
682        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length && !self.commands.steps
683        {
684            reply = self
685                .calculation_list
686                .iter()
687                .fold(note, |mut acc, factorial| {
688                    let _ = factorial.format(
689                        &mut acc,
690                        FormatOptions {
691                            write_out: self.commands.write_out,
692                            ..{ FormatOptions::FORCE_SHORTEN | FormatOptions::AGRESSIVE_SHORTEN }
693                        },
694                        too_big_number,
695                        consts,
696                        &locale.format,
697                    );
698                    acc
699                });
700        }
701
702        let note = if !self.commands.no_note {
703            locale.notes.remove.clone().into_owned() + "\n\n"
704        } else {
705            String::new()
706        };
707        // Remove factorials until we can fit them in a comment
708        if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length {
709            let mut factorial_list: Vec<String> = self
710                .calculation_list
711                .iter()
712                .map(|fact| {
713                    let mut res = String::new();
714                    let _ = fact.format(
715                        &mut res,
716                        FormatOptions {
717                            agressive_shorten: !self.commands.steps,
718                            write_out: self.commands.write_out,
719                            ..FormatOptions::FORCE_SHORTEN
720                        },
721                        too_big_number,
722                        consts,
723                        &locale.format,
724                    );
725                    res
726                })
727                .collect();
728            'drop_last: {
729                while note.len()
730                    + factorial_list.iter().map(|s| s.len()).sum::<usize>()
731                    + locale.bot_disclaimer.len()
732                    + 16
733                    > self.max_length
734                {
735                    // remove last factorial (probably the biggest)
736                    factorial_list.pop();
737                    if factorial_list.is_empty() {
738                        reply = locale.notes.no_post.to_string();
739                        break 'drop_last;
740                    }
741                }
742                reply = factorial_list.iter().fold(note, |mut acc, factorial| {
743                    let _ = acc.write_str(factorial);
744                    acc
745                });
746            }
747        }
748        if !locale.bot_disclaimer.is_empty() {
749            reply.push_str("\n*^(");
750            reply.push_str(&locale.bot_disclaimer);
751            reply.push_str(")*");
752        }
753        reply
754    }
755}
756
757#[cfg(test)]
758mod tests {
759    use crate::{
760        calculation_results::Number,
761        calculation_tasks::{CalculationBase, CalculationJob},
762        locale::NumFormat,
763    };
764
765    const MAX_LENGTH: usize = 10_000;
766
767    use super::*;
768
769    type Comment<S> = super::Comment<(), S>;
770
771    #[test]
772    fn test_extraction_dedup() {
773        let consts = Consts::default();
774        let jobs = parse(
775            "24! -24! 2!? (2!?)!",
776            true,
777            &consts,
778            &NumFormat { decimal: '.' },
779        );
780        assert_eq!(
781            jobs,
782            [
783                CalculationJob {
784                    base: CalculationBase::Num(Number::Exact(24.into())),
785                    level: 1,
786                    negative: 0
787                },
788                CalculationJob {
789                    base: CalculationBase::Num(Number::Exact(24.into())),
790                    level: 1,
791                    negative: 1
792                },
793                CalculationJob {
794                    base: CalculationBase::Calc(Box::new(CalculationJob {
795                        base: CalculationBase::Num(Number::Exact(2.into())),
796                        level: 1,
797                        negative: 0
798                    })),
799                    level: -1,
800                    negative: 0
801                },
802                CalculationJob {
803                    base: CalculationBase::Calc(Box::new(CalculationJob {
804                        base: CalculationBase::Calc(Box::new(CalculationJob {
805                            base: CalculationBase::Num(Number::Exact(2.into())),
806                            level: 1,
807                            negative: 0
808                        })),
809                        level: -1,
810                        negative: 0
811                    })),
812                    level: 1,
813                    negative: 0
814                }
815            ]
816        );
817    }
818
819    #[test]
820    fn test_commands_from_comment_text() {
821        let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note !nested");
822        assert!(cmd1.shorten);
823        assert!(cmd1.steps);
824        assert!(cmd1.termial);
825        assert!(cmd1.no_note);
826        assert!(cmd1.nested);
827        let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note] [nest]");
828        assert!(cmd2.shorten);
829        assert!(cmd2.steps);
830        assert!(cmd2.termial);
831        assert!(cmd2.no_note);
832        assert!(cmd2.nested);
833        let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\] \[nest\]";
834        let cmd3 = Commands::from_comment_text(comment);
835        assert!(cmd3.shorten);
836        assert!(cmd3.steps);
837        assert!(cmd3.termial);
838        assert!(cmd3.no_note);
839        assert!(cmd3.nested);
840        let cmd4 = Commands::from_comment_text("shorten all triangle no_note nest");
841        assert!(!cmd4.shorten);
842        assert!(!cmd4.steps);
843        assert!(!cmd4.termial);
844        assert!(!cmd4.no_note);
845        assert!(!cmd4.nested);
846    }
847
848    #[test]
849    fn test_commands_overrides_from_comment_text() {
850        let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note multi");
851        assert!(cmd1.shorten);
852        assert!(cmd1.steps);
853        assert!(cmd1.termial);
854        assert!(cmd1.no_note);
855        assert!(cmd1.nested);
856    }
857
858    #[test]
859    fn test_might_have_factorial() {
860        assert!(Comment::might_have_factorial("5!"));
861        assert!(Comment::might_have_factorial("3?"));
862        assert!(!Comment::might_have_factorial("!?"));
863    }
864
865    #[test]
866    fn test_new_already_replied() {
867        let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
868        assert_eq!(comment.calculation_list, "");
869        assert!(comment.status.already_replied_or_rejected);
870    }
871
872    #[test]
873    fn test_locale_fallback_note() {
874        let consts = Consts::default();
875        let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
876            .extract(&consts)
877            .calc(&consts);
878        let reply = comment.get_reply(&consts);
879        assert_eq!(
880            reply,
881            "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))*"
882        );
883    }
884
885    #[test]
886    fn test_limit_hit_note() {
887        let consts = Consts::default();
888        let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
889            .extract(&consts)
890            .calc(&consts);
891        comment.add_status(Status::LIMIT_HIT);
892        let reply = comment.get_reply(&consts);
893        assert_eq!(
894            reply,
895            "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))*"
896        );
897    }
898
899    #[test]
900    fn test_write_out_unsupported_note() {
901        let consts = Consts::default();
902        let comment = Comment::new("1!", (), Commands::WRITE_OUT, MAX_LENGTH, "de")
903            .extract(&consts)
904            .calc(&consts);
905        let reply = comment.get_reply(&consts);
906        assert_eq!(
907            reply,
908            "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))*"
909        );
910    }
911}