#[cfg(any(feature = "serde", test))]
use serde::{Deserialize, Serialize};
use crate::rug::integer::IntegerExt64;
use crate::rug::{Complete, Integer};
use crate::Consts;
use crate::calculation_results::{Calculation, FormatOptions};
use crate::calculation_tasks::{CalculationBase, CalculationJob};
use crate::parse::parse;
use std::fmt::Write;
use std::ops::*;
#[macro_export]
macro_rules! impl_bitwise {
($s_name:ident {$($s_fields:ident),*}, $t_name:ident, $fn_name:ident) => {
impl $t_name for $s_name {
type Output = Self;
fn $fn_name(self, rhs: Self) -> Self {
Self {
$($s_fields: self.$s_fields.$fn_name(rhs.$s_fields),)*
}
}
}
};
}
#[macro_export]
macro_rules! impl_all_bitwise {
($s_name:ident {$($s_fields:ident,)*}) => {impl_all_bitwise!($s_name {$($s_fields),*});};
($s_name:ident {$($s_fields:ident),*}) => {
impl_bitwise!($s_name {$($s_fields),*}, BitOr, bitor);
impl_bitwise!($s_name {$($s_fields),*}, BitXor, bitxor);
impl_bitwise!($s_name {$($s_fields),*}, BitAnd, bitand);
impl Not for $s_name {
type Output = Self;
fn not(self) -> Self {
Self {
$($s_fields: self.$s_fields.not(),)*
}
}
}
};
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
pub struct Comment<Meta, S> {
pub meta: Meta,
pub calculation_list: S,
pub notify: Option<String>,
pub status: Status,
pub commands: Commands,
pub max_length: usize,
pub locale: String,
}
pub type CommentConstructed<Meta> = Comment<Meta, String>;
pub type CommentExtracted<Meta> = Comment<Meta, Vec<CalculationJob>>;
pub type CommentCalculated<Meta> = Comment<Meta, Vec<Calculation>>;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
#[non_exhaustive]
pub struct Status {
pub already_replied_or_rejected: bool,
pub not_replied: bool,
pub number_too_big_to_calculate: bool,
pub no_factorial: bool,
pub reply_would_be_too_long: bool,
pub factorials_found: bool,
pub limit_hit: bool,
}
impl_all_bitwise!(Status {
already_replied_or_rejected,
not_replied,
number_too_big_to_calculate,
no_factorial,
reply_would_be_too_long,
factorials_found,
limit_hit,
});
#[allow(dead_code)]
impl Status {
pub const NONE: Self = Self {
already_replied_or_rejected: false,
not_replied: false,
number_too_big_to_calculate: false,
no_factorial: false,
reply_would_be_too_long: false,
factorials_found: false,
limit_hit: false,
};
pub const ALREADY_REPLIED_OR_REJECTED: Self = Self {
already_replied_or_rejected: true,
..Self::NONE
};
pub const NOT_REPLIED: Self = Self {
not_replied: true,
..Self::NONE
};
pub const NUMBER_TOO_BIG_TO_CALCULATE: Self = Self {
number_too_big_to_calculate: true,
..Self::NONE
};
pub const NO_FACTORIAL: Self = Self {
no_factorial: true,
..Self::NONE
};
pub const REPLY_WOULD_BE_TOO_LONG: Self = Self {
reply_would_be_too_long: true,
..Self::NONE
};
pub const FACTORIALS_FOUND: Self = Self {
factorials_found: true,
..Self::NONE
};
pub const LIMIT_HIT: Self = Self {
limit_hit: true,
..Self::NONE
};
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
#[cfg_attr(any(feature = "serde", test), derive(Serialize, Deserialize))]
#[non_exhaustive]
pub struct Commands {
#[cfg_attr(any(feature = "serde", test), serde(default))]
pub shorten: bool,
#[cfg_attr(any(feature = "serde", test), serde(default))]
pub steps: bool,
#[cfg_attr(any(feature = "serde", test), serde(default))]
pub nested: bool,
#[cfg_attr(any(feature = "serde", test), serde(default))]
pub termial: bool,
#[cfg_attr(any(feature = "serde", test), serde(default))]
pub no_note: bool,
#[cfg_attr(any(feature = "serde", test), serde(default))]
pub write_out: bool,
}
impl_all_bitwise!(Commands {
shorten,
steps,
nested,
termial,
no_note,
write_out,
});
#[allow(dead_code)]
impl Commands {
pub const NONE: Self = Self {
shorten: false,
steps: false,
nested: false,
termial: false,
no_note: false,
write_out: false,
};
pub const SHORTEN: Self = Self {
shorten: true,
..Self::NONE
};
pub const STEPS: Self = Self {
steps: true,
..Self::NONE
};
pub const NESTED: Self = Self {
nested: true,
..Self::NONE
};
pub const TERMIAL: Self = Self {
termial: true,
..Self::NONE
};
pub const NO_NOTE: Self = Self {
no_note: true,
..Self::NONE
};
pub const WRITE_OUT: Self = Self {
write_out: true,
..Self::NONE
};
}
impl Commands {
fn contains_command_format(text: &str, command: &str) -> bool {
let pattern1 = format!("\\[{command}\\]");
let pattern2 = format!("[{command}]");
let pattern3 = format!("!{command}");
text.contains(&pattern1) || text.contains(&pattern2) || text.contains(&pattern3)
}
pub fn from_comment_text(text: &str) -> Self {
Self {
shorten: Self::contains_command_format(text, "short")
|| Self::contains_command_format(text, "shorten"),
steps: Self::contains_command_format(text, "steps")
|| Self::contains_command_format(text, "all"),
nested: Self::contains_command_format(text, "nest")
|| Self::contains_command_format(text, "nested"),
termial: Self::contains_command_format(text, "termial")
|| Self::contains_command_format(text, "triangle"),
no_note: Self::contains_command_format(text, "no note")
|| Self::contains_command_format(text, "no\\_note")
|| Self::contains_command_format(text, "no_note"),
write_out: Self::contains_command_format(text, "write_out")
|| Self::contains_command_format(text, "write\\_out")
|| Self::contains_command_format(text, "write_num")
|| Self::contains_command_format(text, "write\\_num"),
}
}
pub fn overrides_from_comment_text(text: &str) -> Self {
Self {
shorten: !Self::contains_command_format(text, "long"),
steps: !(Self::contains_command_format(text, "no steps")
|| Self::contains_command_format(text, "no_steps")
|| Self::contains_command_format(text, "no\\_steps")),
nested: !(Self::contains_command_format(text, "no_nest")
|| Self::contains_command_format(text, "no\\_nest")
|| Self::contains_command_format(text, "multi")),
termial: !(Self::contains_command_format(text, "no termial")
|| Self::contains_command_format(text, "no_termial")
|| Self::contains_command_format(text, "no\\_termial")),
no_note: !Self::contains_command_format(text, "note"),
write_out: !(Self::contains_command_format(text, "dont_write_out")
|| Self::contains_command_format(text, "dont\\_write\\_out")
|| Self::contains_command_format(text, "normal\\_num")
|| Self::contains_command_format(text, "normal\\_num")),
}
}
}
macro_rules! contains_comb {
($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
$var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end,$($end_rest),*]) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
};
(@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
$var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start,$($start_rest),*], [$($end_rest),*])
};
($var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
$var.contains(concat!($start, $end)) || contains_comb!($var, [$($start_rest),*], [$end])
};
($var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
$var.contains(concat!($start, $end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
};
(@inner $var:ident, [$start:tt,$($start_rest:tt),* $(,)?], [$end:tt $(,)?]) => {
$var.contains(concat!($start,$end))
};
(@inner $var:ident, [$start:tt $(,)?], [$end:tt,$($end_rest:tt),* $(,)?]) => {
$var.contains(concat!($start,$end)) || contains_comb!(@inner $var, [$start], [$($end_rest),*])
};
($var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
$var.contains(concat!($start, $end))
};
(@inner $var:ident, [$start:tt $(,)?], [$end:tt $(,)?]) => {
$var.contains(concat!($start,$end))
};
}
impl<Meta> CommentConstructed<Meta> {
pub fn new(
comment_text: &str,
meta: Meta,
pre_commands: Commands,
max_length: usize,
locale: &str,
) -> Self {
let command_overrides = Commands::overrides_from_comment_text(comment_text);
let commands: Commands =
(Commands::from_comment_text(comment_text) | pre_commands) & command_overrides;
let mut status: Status = Default::default();
let text = if Self::might_have_factorial(comment_text) {
comment_text.to_owned()
} else {
status.no_factorial = true;
String::new()
};
Comment {
meta,
notify: None,
calculation_list: text,
status,
commands,
max_length,
locale: locale.to_owned(),
}
}
fn might_have_factorial(text: &str) -> bool {
contains_comb!(
text,
[
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
")",
"e",
"pi",
"phi",
"tau",
"π",
"ɸ",
"τ",
"infinity",
"inf",
"∞\u{303}",
"∞"
],
["!", "?"]
) || contains_comb!(
text,
["!"],
[
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"(",
"e",
"pi",
"phi",
"tau",
"π",
"ɸ",
"τ",
"infinity",
"inf",
"∞\u{303}",
"∞"
]
)
}
pub fn extract(self, consts: &Consts) -> CommentExtracted<Meta> {
let Comment {
meta,
calculation_list: comment_text,
notify,
mut status,
commands,
max_length,
locale,
} = self;
let mut pending_list: Vec<CalculationJob> = parse(
&comment_text,
commands.termial,
consts,
&consts
.locales
.get(&locale)
.unwrap_or(consts.locales.get(&consts.default_locale).unwrap())
.format
.number_format,
);
if commands.nested {
for calc in &mut pending_list {
Self::multi_to_nested(calc);
}
}
if pending_list.is_empty() {
status.no_factorial = true;
}
Comment {
meta,
calculation_list: pending_list,
notify,
status,
commands,
max_length,
locale,
}
}
fn multi_to_nested(mut calc: &mut CalculationJob) {
loop {
let level = calc.level.clamp(-1, 1);
let depth = calc.level.abs();
calc.level = level;
for _ in 1..depth {
let base = std::mem::replace(
&mut calc.base,
CalculationBase::Num(
crate::calculation_results::CalculationResult::ComplexInfinity,
),
);
let new_base = CalculationBase::Calc(Box::new(CalculationJob {
base,
level,
negative: 0,
}));
let _ = std::mem::replace(&mut calc.base, new_base);
}
let CalculationBase::Calc(next) = &mut calc.base else {
return;
};
calc = next;
}
}
pub fn new_already_replied(meta: Meta, max_length: usize, locale: &str) -> Self {
let text = String::new();
let status: Status = Status {
already_replied_or_rejected: true,
..Default::default()
};
let commands: Commands = Default::default();
Comment {
meta,
notify: None,
calculation_list: text,
status,
commands,
max_length,
locale: locale.to_owned(),
}
}
}
impl<Meta, S> Comment<Meta, S> {
pub fn add_status(&mut self, status: Status) {
self.status = self.status | status;
}
}
impl<Meta> CommentExtracted<Meta> {
pub fn calc(self, consts: &Consts) -> CommentCalculated<Meta> {
let Comment {
meta,
calculation_list: pending_list,
notify,
mut status,
commands,
max_length,
locale,
} = self;
let mut calculation_list: Vec<Calculation> = pending_list
.into_iter()
.flat_map(|calc| calc.execute(commands.steps, consts))
.filter_map(|x| {
if x.is_none() {
status.number_too_big_to_calculate = true;
};
x
})
.collect();
calculation_list.sort();
calculation_list.dedup();
calculation_list.sort_by_key(|x| x.steps.len());
if calculation_list.is_empty() {
status.no_factorial = true;
} else {
status.factorials_found = true;
}
Comment {
meta,
calculation_list,
notify,
status,
commands,
max_length,
locale,
}
}
}
impl<Meta> CommentCalculated<Meta> {
pub fn get_reply(&self, consts: &Consts) -> String {
let mut fell_back = false;
let locale = consts.locales.get(&self.locale).unwrap_or_else(|| {
fell_back = true;
consts.locales.get(&consts.default_locale).unwrap()
});
let mut note = self
.notify
.as_ref()
.map(|user| locale.notes.mention.replace("{mention}", user) + "\n\n")
.unwrap_or_default();
if fell_back {
let _ = note.write_str("Sorry, I currently don't speak ");
let _ = note.write_str(&self.locale);
let _ = note.write_str(". Maybe you could [teach me](https://github.com/tolik518/factorion-bot/blob/master/CONTRIBUTING.md#translation)? \n\n");
}
let too_big_number = Integer::u64_pow_u64(10, self.max_length as u64).complete();
let too_big_number = &too_big_number;
let multiple = self.calculation_list.len() > 1;
if !self.commands.no_note {
if self.status.limit_hit {
let _ = note.write_str(
locale
.notes
.limit_hit
.as_ref()
.map(AsRef::as_ref)
.unwrap_or(
"I have repeated myself enough, I won't do that calculation again.",
),
);
let _ = note.write_str("\n\n");
} else if self
.calculation_list
.iter()
.any(Calculation::is_digit_tower)
{
if multiple {
let _ = note.write_str(&locale.notes.tower_mult);
let _ = note.write_str("\n\n");
} else {
let _ = note.write_str(&locale.notes.tower);
let _ = note.write_str("\n\n");
}
} else if self
.calculation_list
.iter()
.any(Calculation::is_aproximate_digits)
{
if multiple {
let _ = note.write_str(&locale.notes.digits_mult);
let _ = note.write_str("\n\n");
} else {
let _ = note.write_str(&locale.notes.digits);
let _ = note.write_str("\n\n");
}
} else if self
.calculation_list
.iter()
.any(Calculation::is_approximate)
{
if multiple {
let _ = note.write_str(&locale.notes.approx_mult);
let _ = note.write_str("\n\n");
} else {
let _ = note.write_str(&locale.notes.approx);
let _ = note.write_str("\n\n");
}
} else if self.calculation_list.iter().any(Calculation::is_rounded) {
if multiple {
let _ = note.write_str(&locale.notes.round_mult);
let _ = note.write_str("\n\n");
} else {
let _ = note.write_str(&locale.notes.round);
let _ = note.write_str("\n\n");
}
} else if self
.calculation_list
.iter()
.any(|c| c.is_too_long(too_big_number))
&& !(self.commands.write_out
&& self
.calculation_list
.iter()
.all(|c| c.can_write_out(consts.float_precision)))
{
if multiple {
let _ = note.write_str(&locale.notes.too_big_mult);
let _ = note.write_str("\n\n");
} else {
let _ = note.write_str(&locale.notes.too_big);
let _ = note.write_str("\n\n");
}
} else if self.commands.write_out && self.locale != "en" {
let _ =
note.write_str("I can only write out numbers in english, so I will do that.");
let _ = note.write_str("\n\n");
}
}
let mut reply = self
.calculation_list
.iter()
.fold(note.clone(), |mut acc, factorial| {
let _ = factorial.format(
&mut acc,
FormatOptions {
force_shorten: self.commands.shorten,
write_out: self.commands.write_out,
..FormatOptions::NONE
},
too_big_number,
consts,
&locale.format,
);
acc
});
if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length
&& !self.commands.shorten
&& !self
.calculation_list
.iter()
.all(|fact| fact.is_too_long(too_big_number))
{
if note.is_empty() && !self.commands.no_note {
if multiple {
let _ = note.write_str(&locale.notes.too_big_mult);
} else {
let _ = note.write_str(&locale.notes.too_big);
}
let _ = note.write_str("\n\n");
};
reply = self
.calculation_list
.iter()
.fold(note, |mut acc, factorial| {
let _ = factorial.format(
&mut acc,
FormatOptions {
write_out: self.commands.write_out,
..FormatOptions::FORCE_SHORTEN
},
too_big_number,
consts,
&locale.format,
);
acc
});
}
let note = if !self.commands.no_note {
locale.notes.tetration.clone().into_owned() + "\n\n"
} else {
String::new()
};
if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length && !self.commands.steps
{
reply = self
.calculation_list
.iter()
.fold(note, |mut acc, factorial| {
let _ = factorial.format(
&mut acc,
FormatOptions {
write_out: self.commands.write_out,
..{ FormatOptions::FORCE_SHORTEN | FormatOptions::AGRESSIVE_SHORTEN }
},
too_big_number,
consts,
&locale.format,
);
acc
});
}
let note = if !self.commands.no_note {
locale.notes.remove.clone().into_owned() + "\n\n"
} else {
String::new()
};
if reply.len() + locale.bot_disclaimer.len() + 16 > self.max_length {
let mut factorial_list: Vec<String> = self
.calculation_list
.iter()
.map(|fact| {
let mut res = String::new();
let _ = fact.format(
&mut res,
FormatOptions {
agressive_shorten: !self.commands.steps,
write_out: self.commands.write_out,
..FormatOptions::FORCE_SHORTEN
},
too_big_number,
consts,
&locale.format,
);
res
})
.collect();
'drop_last: {
while note.len()
+ factorial_list.iter().map(|s| s.len()).sum::<usize>()
+ locale.bot_disclaimer.len()
+ 16
> self.max_length
{
factorial_list.pop();
if factorial_list.is_empty() {
reply = locale.notes.no_post.to_string();
break 'drop_last;
}
}
reply = factorial_list.iter().fold(note, |mut acc, factorial| {
let _ = acc.write_str(factorial);
acc
});
}
}
if !locale.bot_disclaimer.is_empty() {
reply.push_str("\n*^(");
reply.push_str(&locale.bot_disclaimer);
reply.push_str(")*");
}
reply
}
}
#[cfg(test)]
mod tests {
use crate::{
calculation_results::Number,
calculation_tasks::{CalculationBase, CalculationJob},
locale::NumFormat,
};
const MAX_LENGTH: usize = 10_000;
use super::*;
type Comment<S> = super::Comment<(), S>;
#[test]
fn test_extraction_dedup() {
let consts = Consts::default();
let jobs = parse(
"24! -24! 2!? (2!?)!",
true,
&consts,
&NumFormat { decimal: '.' },
);
assert_eq!(
jobs,
[
CalculationJob {
base: CalculationBase::Num(Number::Exact(24.into())),
level: 1,
negative: 0
},
CalculationJob {
base: CalculationBase::Num(Number::Exact(24.into())),
level: 1,
negative: 1
},
CalculationJob {
base: CalculationBase::Calc(Box::new(CalculationJob {
base: CalculationBase::Num(Number::Exact(2.into())),
level: 1,
negative: 0
})),
level: -1,
negative: 0
},
CalculationJob {
base: CalculationBase::Calc(Box::new(CalculationJob {
base: CalculationBase::Calc(Box::new(CalculationJob {
base: CalculationBase::Num(Number::Exact(2.into())),
level: 1,
negative: 0
})),
level: -1,
negative: 0
})),
level: 1,
negative: 0
}
]
);
}
#[test]
fn test_commands_from_comment_text() {
let cmd1 = Commands::from_comment_text("!shorten!all !triangle !no_note !nested");
assert!(cmd1.shorten);
assert!(cmd1.steps);
assert!(cmd1.termial);
assert!(cmd1.no_note);
assert!(cmd1.nested);
let cmd2 = Commands::from_comment_text("[shorten][all] [triangle] [no_note] [nest]");
assert!(cmd2.shorten);
assert!(cmd2.steps);
assert!(cmd2.termial);
assert!(cmd2.no_note);
assert!(cmd2.nested);
let comment = r"\[shorten\]\[all\] \[triangle\] \[no_note\] \[nest\]";
let cmd3 = Commands::from_comment_text(comment);
assert!(cmd3.shorten);
assert!(cmd3.steps);
assert!(cmd3.termial);
assert!(cmd3.no_note);
assert!(cmd3.nested);
let cmd4 = Commands::from_comment_text("shorten all triangle no_note nest");
assert!(!cmd4.shorten);
assert!(!cmd4.steps);
assert!(!cmd4.termial);
assert!(!cmd4.no_note);
assert!(!cmd4.nested);
}
#[test]
fn test_commands_overrides_from_comment_text() {
let cmd1 = Commands::overrides_from_comment_text("long no_steps no_termial note multi");
assert!(cmd1.shorten);
assert!(cmd1.steps);
assert!(cmd1.termial);
assert!(cmd1.no_note);
assert!(cmd1.nested);
}
#[test]
fn test_might_have_factorial() {
assert!(Comment::might_have_factorial("5!"));
assert!(Comment::might_have_factorial("3?"));
assert!(!Comment::might_have_factorial("!?"));
}
#[test]
fn test_new_already_replied() {
let comment = Comment::new_already_replied((), MAX_LENGTH, "en");
assert_eq!(comment.calculation_list, "");
assert!(comment.status.already_replied_or_rejected);
}
#[test]
fn test_locale_fallback_note() {
let consts = Consts::default();
let comment = Comment::new_already_replied((), MAX_LENGTH, "n/a")
.extract(&consts)
.calc(&consts);
let reply = comment.get_reply(&consts);
assert_eq!(
reply,
"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))*"
);
}
#[test]
fn test_limit_hit_note() {
let consts = Consts::default();
let mut comment = Comment::new_already_replied((), MAX_LENGTH, "en")
.extract(&consts)
.calc(&consts);
comment.add_status(Status::LIMIT_HIT);
let reply = comment.get_reply(&consts);
assert_eq!(
reply,
"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))*"
);
}
#[test]
fn test_write_out_unsupported_note() {
let consts = Consts::default();
let comment = Comment::new("1!", (), Commands::WRITE_OUT, MAX_LENGTH, "de")
.extract(&consts)
.calc(&consts);
let reply = comment.get_reply(&consts);
assert_eq!(
reply,
"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))*"
);
}
}