diff --git a/Cargo.lock b/Cargo.lock index 97f87dde..cf573d3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -399,7 +399,7 @@ dependencies = [ [[package]] name = "factorion-bot-discord" -version = "2.4.0" +version = "2.5.0" dependencies = [ "anyhow", "dotenvy", @@ -414,7 +414,7 @@ dependencies = [ [[package]] name = "factorion-bot-reddit" -version = "5.5.0" +version = "5.6.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -434,7 +434,7 @@ dependencies = [ [[package]] name = "factorion-lib" -version = "4.4.0" +version = "5.0.0" dependencies = [ "arbtest", "chrono", @@ -447,7 +447,7 @@ dependencies = [ [[package]] name = "factorion-math" -version = "1.0.1" +version = "1.0.2" dependencies = [ "gmp-mpfr-sys", "rug", diff --git a/factorion-bot-discord/Cargo.toml b/factorion-bot-discord/Cargo.toml index dc3805d0..960c09ce 100644 --- a/factorion-bot-discord/Cargo.toml +++ b/factorion-bot-discord/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "factorion-bot-discord" -version = "2.4.0" +version = "2.5.0" edition = "2024" description = "factorion-bot (for factorials and related) on Discord" license = "MIT" @@ -10,7 +10,7 @@ keywords = ["factorial", "termial", "bot", "math", "discord"] categories = ["mathematics", "web-programming", "parser-implementations"] [dependencies] -factorion-lib = { path = "../factorion-lib", version = "4.4.0", features = ["serde", "influxdb"] } +factorion-lib = { path = "../factorion-lib", version = "5.0.0", features = ["serde", "influxdb"] } serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] } tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "time"] } dotenvy = "^0.15.7" diff --git a/factorion-bot-reddit/Cargo.toml b/factorion-bot-reddit/Cargo.toml index e9e7f720..2cec8970 100644 --- a/factorion-bot-reddit/Cargo.toml +++ b/factorion-bot-reddit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "factorion-bot-reddit" -version = "5.5.0" +version = "5.6.0" edition = "2024" description = "factorion-bot (for factorials and related) on Reddit" license = "MIT" @@ -10,7 +10,7 @@ keywords = ["factorial", "termial", "bot", "math"] categories = ["mathematics", "web-programming", "parser-implementations"] [dependencies] -factorion-lib = {path = "../factorion-lib", version = "4.4.0", features = ["serde", "influxdb"]} +factorion-lib = {path = "../factorion-lib", version = "5.0.0", features = ["serde", "influxdb"]} reqwest = { version = "0.12.28", features = ["json", "native-tls"], default-features = false } serde = { version = "1.0.219", default-features = false, features = ["derive"] } serde_json = "1.0.140" diff --git a/factorion-lib/Cargo.toml b/factorion-lib/Cargo.toml index f40cb221..e2d44df1 100644 --- a/factorion-lib/Cargo.toml +++ b/factorion-lib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "factorion-lib" -version = "4.4.0" +version = "5.0.0" edition = "2024" description = "A library used to create bots to recognize and calculate factorials and related concepts" license = "MIT" @@ -10,7 +10,7 @@ keywords = ["factorial", "termial", "bot", "math"] categories = ["mathematics", "web-programming", "parser-implementations"] [dependencies] -factorion-math = {path = "../factorion-math", version = "1.0"} +factorion-math = {path = "../factorion-math", version = "1.0.2"} serde = {version = "1.0", features = ["derive"], optional = true} serde_json = {version = "1.0", optional = true} concat-idents = "1.1.4" diff --git a/factorion-lib/README.md b/factorion-lib/README.md index a4c18d09..817c6e9d 100644 --- a/factorion-lib/README.md +++ b/factorion-lib/README.md @@ -29,7 +29,7 @@ assert_eq!(reply, "Hey @you!\n\nTermial of factorial of 5 is 7260 \n\n\n*^(This ``` Or manually do the steps: ```rust -use factorion_lib::{parse::parse, calculation_tasks::{CalculationJob, CalculationBase}, calculation_results::{Calculation, CalculationResult, Number}, Consts}; +use factorion_lib::{parse::parse, calculation_tasks::{CalculationJob, CalculationBase}, calculation_results::{Calculation, CalculationResult, Number, FormatOptions}, Consts}; // You need to define constants first let consts = Consts::default(); @@ -61,6 +61,6 @@ assert_eq!(results, [ let result = results.remove(0); let mut formatted = String::new(); // Write the formatted result to a string (for efficiency). We don't want to shorten anything below that huge number -result.format(&mut formatted, false, false, &10000000000000000000u128.into(), &consts, &locale.format()).unwrap(); +result.format(&mut formatted, FormatOptions::NONE, &10000000000000000000u128.into(), &consts, &locale.format()).unwrap(); assert_eq!(formatted, "Factorial of 4 is 24 \n\n"); ``` diff --git a/factorion-lib/src/calculation_results.rs b/factorion-lib/src/calculation_results.rs index 6b350448..183e1528 100644 --- a/factorion-lib/src/calculation_results.rs +++ b/factorion-lib/src/calculation_results.rs @@ -1,13 +1,22 @@ //! This module handles the formatting of the calculations (`The factorial of Subfactorial of 5 is`, etc.) +use crate::format::{ + format_float, get_factorial_level_string, replace, truncate, write_out_number, +}; +use crate::impl_all_bitwise; +use crate::impl_bitwise; +use factorion_math::length; #[cfg(any(feature = "serde", test))] use serde::{Deserialize, Serialize}; +use std::ops::BitAnd; +use std::ops::BitOr; +use std::ops::BitXor; +use std::ops::Not; use crate::rug::float::OrdFloat; -use crate::rug::ops::{NegAssign, NotAssign, Pow}; +use crate::rug::ops::{NegAssign, NotAssign}; use crate::rug::{Float, Integer}; use crate::{Consts, locale}; -use std::borrow::Cow; use std::fmt; use std::fmt::Write; @@ -110,17 +119,47 @@ impl From for Number { Number::Float(value.into()) } } - +#[non_exhaustive] +#[derive(Debug, Clone, Default)] +pub struct FormatOptions { + pub force_shorten: bool, + pub agressive_shorten: bool, + pub write_out: bool, +} +impl_all_bitwise!(FormatOptions { + force_shorten, + agressive_shorten, + write_out, +}); +#[allow(dead_code)] +impl FormatOptions { + pub const NONE: Self = Self { + force_shorten: false, + agressive_shorten: false, + write_out: false, + }; + pub const FORCE_SHORTEN: Self = Self { + force_shorten: true, + ..Self::NONE + }; + pub const AGRESSIVE_SHORTEN: Self = Self { + agressive_shorten: true, + ..Self::NONE + }; + pub const WRITE_OUT: Self = Self { + write_out: true, + ..Self::NONE + }; +} impl CalculationResult { /// Formats a number. \ /// Shorten turns integers into scientific notation if that makes them shorter. \ /// Aggressive enables tertation for towers. - fn format( + pub fn format( &self, acc: &mut String, rough: &mut bool, - shorten: bool, - agressive: bool, + opts: FormatOptions, is_value: bool, consts: &Consts, locale: &locale::NumFormat, @@ -128,7 +167,9 @@ impl CalculationResult { let mut start = acc.len(); match &self { CalculationResult::Exact(factorial) => { - if shorten { + if opts.write_out && length(factorial, consts.float_precision) < 3000000 { + write_out_number(acc, factorial, consts)?; + } else if opts.force_shorten { let (s, r) = truncate(factorial, consts); *rough = r; acc.write_str(&s)?; @@ -140,7 +181,7 @@ impl CalculationResult { let base = base.as_float(); format_float(acc, base, consts)?; acc.write_str(" × 10^")?; - if shorten { + if opts.force_shorten { acc.write_str("(")?; acc.write_str(&truncate(exponent, consts).0)?; acc.write_str(")")?; @@ -149,16 +190,20 @@ impl CalculationResult { } } CalculationResult::ApproximateDigits(_, digits) => { - if is_value { - acc.write_str("10^(")?; - } - if shorten { - acc.write_str(&truncate(digits, consts).0)?; + if opts.write_out && !is_value && length(digits, consts.float_precision) < 3000000 { + write_out_number(acc, digits, consts)?; } else { - write!(acc, "{digits}")?; - } - if is_value { - acc.write_str(")")?; + if is_value { + acc.write_str("10^(")?; + } + if opts.force_shorten { + acc.write_str(&truncate(digits, consts).0)?; + } else { + write!(acc, "{digits}")?; + } + if is_value { + acc.write_str(")")?; + } } } CalculationResult::ApproximateDigitsTower(_, negative, depth, exponent) => { @@ -170,7 +215,8 @@ impl CalculationResult { acc.write_str(if *negative { "-" } else { "" })?; // If we have a one on top, we gain no information by printing the whole tower. // If depth is one, it is nicer to write 10¹ than ¹10. - if !agressive && depth <= usize::MAX && (depth <= 1 || exponent != &1) { + if !opts.agressive_shorten && depth <= usize::MAX && (depth <= 1 || exponent != &1) + { if depth > 0 { acc.write_str("10^(")?; } @@ -179,7 +225,7 @@ impl CalculationResult { acc.write_str(&"10\\^".repeat(depth.to_usize().unwrap() - 1))?; acc.write_str("(")?; } - if shorten { + if opts.force_shorten { acc.write_str(&truncate(exponent, consts).0)?; } else { write!(acc, "{exponent}")?; @@ -207,7 +253,11 @@ impl CalculationResult { format_float(acc, gamma, consts)?; } CalculationResult::ComplexInfinity => { - acc.write_str("∞\u{0303}")?; + if opts.write_out { + acc.write_str("complex infinity")?; + } else { + acc.write_str("∞\u{0303}")?; + } } } if *locale.decimal() != '.' { @@ -276,6 +326,13 @@ impl Calculation { pub fn is_too_long(&self, too_big_number: &Integer) -> bool { self.result.is_too_long(too_big_number) || self.value.is_too_long(too_big_number) } + pub fn can_write_out(&self, prec: u32) -> bool { + let CalculationResult::Exact(n) = &self.result else { + return false; + }; + + length(n, prec) <= 3000000 + } } impl Calculation { @@ -286,8 +343,7 @@ impl Calculation { pub fn format( &self, acc: &mut String, - force_shorten: bool, - agressive_shorten: bool, + options: FormatOptions, too_big_number: &Integer, consts: &Consts, locale: &locale::Format<'_>, @@ -297,7 +353,7 @@ impl Calculation { match ( &self.value, &self.result, - agressive_shorten && self.steps.len() > 1, + options.agressive_shorten && self.steps.len() > 1, ) { // All that (_, _, true) => locale.all_that(), @@ -320,8 +376,12 @@ impl Calculation { self.value.format( &mut number, &mut rough, - force_shorten || self.result.is_too_long(too_big_number) || agressive_shorten, - agressive_shorten, + FormatOptions { + force_shorten: options.force_shorten + || self.result.is_too_long(too_big_number) + || options.agressive_shorten, + ..options + }, true, consts, &locale.number_format(), @@ -336,8 +396,12 @@ impl Calculation { self.result.format( &mut number, &mut rough, - force_shorten || self.result.is_too_long(too_big_number) || agressive_shorten, - agressive_shorten, + FormatOptions { + force_shorten: options.force_shorten + || self.result.is_too_long(too_big_number) + || options.agressive_shorten, + ..options + }, false, consts, &locale.number_format(), @@ -362,7 +426,7 @@ impl Calculation { acc, start, "{factorial}", - &Self::get_factorial_level_string(level.abs(), locale), + &get_factorial_level_string(level.abs(), locale), ); replace( @@ -397,456 +461,18 @@ impl Calculation { Ok(()) } - - fn get_factorial_level_string<'a>(level: i32, locale: &'a locale::Format<'a>) -> Cow<'a, str> { - const SINGLES: [&str; 10] = [ - "", "un", "duo", "tre", "quattuor", "quin", "sex", "septen", "octo", "novem", - ]; - const SINGLES_LAST: [&str; 10] = [ - "", "un", "du", "tr", "quadr", "quint", "sext", "sept", "oct", "non", - ]; - const TENS: [&str; 10] = [ - "", - "dec", - "vigin", - "trigin", - "quadragin", - "quinquagin", - "sexagin", - "septuagin", - "octogin", - "nonagin", - ]; - const HUNDREDS: [&str; 10] = [ - "", - "cen", - "ducen", - // Note this differs from the wikipedia list to disambiguate from 103, which continuing the pattern should be trecentuple - "tricen", - "quadringen", - "quingen", - "sescen", - "septingen", - "octingen", - "nongen", - ]; - // Note that other than milluple, these are not found in a list, but continue the pattern from mill with different starts - const THOUSANDS: [&str; 10] = [ - "", "mill", "bill", "trill", "quadrill", "quintill", "sextill", "septill", "octill", - "nonill", - ]; - const TEN_THOUSANDS: [&str; 10] = [ - "", - "decill", - "vigintill", - "trigintill", - "quadragintill", - "quinquagintill", - "sexagintill", - "septuagintill", - "octogintill", - "nonagintill", - ]; - const HUNDRED_THOUSANDS: [&str; 10] = [ - "", - "centill", - "ducentill", - "tricentill", - "quadringentill", - "quingentill", - "sescentill", - "septingentill", - "octingentill", - "nongentill", - ]; - const BINDING_T: [[bool; 10]; 6] = [ - // Singles - [ - false, false, false, false, false, false, false, false, false, false, - ], - // Tens - [false, false, true, true, true, true, true, true, true, true], - // Hundreds - [false, true, true, true, true, true, true, true, true, true], - // Thousands - [ - false, false, false, false, false, false, false, false, false, false, - ], - // Tenthousands - [ - false, false, false, false, false, false, false, false, false, false, - ], - // Hundredthousands - [ - false, false, false, false, false, false, false, false, false, false, - ], - ]; - if let Some(s) = locale.num_overrides().get(&level) { - return s.as_ref().into(); - } - match level { - 0 => locale.sub().as_ref().into(), - 1 => "{factorial}".into(), - ..=999999 if !locale.force_num() => { - let singles = if level < 10 { SINGLES_LAST } else { SINGLES }; - let mut acc = String::new(); - let mut n = level; - let s = n % 10; - n /= 10; - acc.write_str(singles[s as usize]).unwrap(); - let t = n % 10; - n /= 10; - acc.write_str(TENS[t as usize]).unwrap(); - let h = n % 10; - n /= 10; - acc.write_str(HUNDREDS[h as usize]).unwrap(); - let th = n % 10; - n /= 10; - acc.write_str(THOUSANDS[th as usize]).unwrap(); - let tth = n % 10; - n /= 10; - acc.write_str(TEN_THOUSANDS[tth as usize]).unwrap(); - let hth = n % 10; - acc.write_str(HUNDRED_THOUSANDS[hth as usize]).unwrap(); - // Check if we need tuple not uple - let last_written = [s, t, h, th, tth, hth] - .iter() - .cloned() - .enumerate() - .rev() - .find(|(_, n)| *n != 0) - .unwrap(); - if BINDING_T[last_written.0][last_written.1 as usize] { - acc.write_str("t").unwrap(); - } - acc.write_str(locale.uple()).unwrap(); - - acc.into() - } - _ => { - let mut suffix = String::new(); - write!(&mut suffix, "{level}-{{factorial}}").unwrap(); - suffix.into() - } - } - } -} -/// Rounds a base 10 number string. \ -/// Uses the last digit to decide the rounding direction. \ -/// Rounds over 9s. This does **not** keep the length or turn rounded over digits into zeros. \ -/// If the input is all 9s, this will round to 10. \ -/// Stops when a decimal period is encountered, removing it. -/// -/// Returns whether it overflowed. -/// -/// # Panic -/// This function may panic if less than two digits are supplied, or if it contains a non-digit of base 10, that is not a period. -fn round(number: &mut String) -> bool { - // Check additional digit if we need to round - if let Some(digit) = number - .pop() - .map(|n| n.to_digit(10).expect("Not a base 10 number")) - && digit >= 5 - { - let mut last_digit = number - .pop() - .and_then(|n| n.to_digit(10)) - .expect("Not a base 10 number"); - // Carry over at 9s - while last_digit == 9 { - let Some(digit) = number.pop() else { - // If we reached the end we get 10 - number.push_str("10"); - return true; - }; - // Stop at decimal - if digit == '.' { - break; - } - let digit = digit.to_digit(10).expect("Not a base 10 number"); - last_digit = digit; - } - // Round up - let _ = write!(number, "{}", last_digit + 1); - } - false -} -fn truncate(number: &Integer, consts: &Consts) -> (String, bool) { - let prec = consts.float_precision; - if number == &0 { - return (number.to_string(), false); - } - let negative = number.is_negative(); - let orig_number = number; - let number = number.clone().abs(); - let length = (Float::with_val(prec, &number).ln() / Float::with_val(prec, 10).ln()) - .to_integer_round(crate::rug::float::Round::Down) - .unwrap() - .0; - let truncated_number: Integer = &number - / (Float::with_val(prec, 10) - .pow((length.clone() - consts.number_decimals_scientific - 1u8).max(Integer::ZERO)) - .to_integer() - .unwrap()); - let mut truncated_number = truncated_number.to_string(); - if truncated_number.len() > consts.number_decimals_scientific { - round(&mut truncated_number); - } - if let Some(mut digit) = truncated_number.pop() { - while digit == '0' { - digit = match truncated_number.pop() { - Some(x) => x, - None => break, - } - } - truncated_number.push(digit); - } - // Only add decimal if we have more than one digit - if truncated_number.len() > 1 { - truncated_number.insert(1, '.'); // Decimal point - } - if negative { - truncated_number.insert(0, '-'); - } - if length > consts.number_decimals_scientific + 1 { - (format!("{truncated_number} × 10^{length}"), true) - } else { - (orig_number.to_string(), false) - } -} -fn format_float(acc: &mut String, number: &Float, consts: &Consts) -> std::fmt::Result { - // -a.b x 10^c - // - - // a - // .b - // x 10^c - let mut number = number.clone(); - let negative = number.is_sign_negative(); - number = number.abs(); - if number == 0 { - return acc.write_char('0'); - } - let exponent = number - .clone() - .log10() - .to_integer_round(factorion_math::rug::float::Round::Down) - .expect("Could not round exponent") - .0; - if exponent > consts.number_decimals_scientific - || exponent < -(consts.number_decimals_scientific as isize) - { - number = number / Float::with_val(consts.float_precision, &exponent).exp10(); - } - let mut whole_number = number - .to_integer_round(factorion_math::rug::float::Round::Down) - .expect("Could not get integer part") - .0; - let decimal_part = number - &whole_number + 1; - let mut decimal_part = format!("{decimal_part}"); - // Remove "1." - decimal_part.remove(0); - decimal_part.remove(0); - decimal_part.truncate(consts.number_decimals_scientific + 1); - if decimal_part.len() > consts.number_decimals_scientific { - if round(&mut decimal_part) { - decimal_part.clear(); - whole_number += 1; - } - } - if let Some(mut digit) = decimal_part.pop() { - while digit == '0' { - digit = match decimal_part.pop() { - Some(x) => x, - None => break, - } - } - decimal_part.push(digit); - } - if negative { - acc.write_str("-")?; - } - write!(acc, "{whole_number}")?; - if !decimal_part.is_empty() && decimal_part != "0" { - acc.write_str(".")?; - acc.write_str(&decimal_part)?; - } - if exponent > consts.number_decimals_scientific - || exponent < -(consts.number_decimals_scientific as isize) - { - write!(acc, " × 10^{exponent}")?; - } - Ok(()) } -fn replace(s: &mut String, search_start: usize, from: &str, to: &str) -> usize { - if let Some(start) = s[search_start..].find(from) { - let start = start + search_start; - s.replace_range(start..(start + from.len()), to); - start - } else { - s.len() - } -} #[cfg(test)] -mod tests { +mod test { use super::*; use crate::recommended::FLOAT_PRECISION; - use crate::rug::Integer; + use factorion_math::rug::Complete; use std::{str::FromStr, sync::LazyLock}; - static TOO_BIG_NUMBER: LazyLock = LazyLock::new(|| Integer::from_str(&format!("1{}", "0".repeat(9999))).unwrap()); - #[test] - fn test_round_down() { - let mut number = String::from("1929472373"); - round(&mut number); - assert_eq!(number, "192947237"); - } - - #[test] - fn test_round_up() { - let mut number = String::from("74836748625"); - round(&mut number); - assert_eq!(number, "7483674863"); - } - - #[test] - fn test_round_carry() { - let mut number = String::from("24999999995"); - round(&mut number); - assert_eq!(number, "25"); - } - - #[test] - fn test_factorial_level_string() { - let en = locale::get_en(); - assert_eq!( - Calculation::get_factorial_level_string(1, &en.format()), - "{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(2, &en.format()), - "double-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(3, &en.format()), - "triple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(10, &en.format()), - "decuple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(45, &en.format()), - "quinquadragintuple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(50, &en.format()), - "quinquagintuple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(100, &en.format()), - "centuple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(521, &en.format()), - "unviginquingentuple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(1000, &en.format()), - "milluple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(4321, &en.format()), - "unvigintricenquadrilluple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(89342, &en.format()), - "duoquadragintricennonilloctogintilluple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(654321, &en.format()), - "unvigintricenquadrillquinquagintillsescentilluple-{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(1000000, &en.format()), - "1000000-{factorial}" - ); - let de = locale::get_de(); - assert_eq!( - Calculation::get_factorial_level_string(1, &de.format()), - "{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(2, &de.format()), - "doppel{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(3, &de.format()), - "trippel{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(45, &de.format()), - "quinquadragintupel{factorial}" - ); - } - - #[test] - fn test_truncate() { - let consts = Consts::default(); - assert_eq!(truncate(&Integer::from_str("0").unwrap(), &consts,).0, "0"); - assert_eq!( - truncate(&Integer::from_str("-1").unwrap(), &consts,).0, - "-1" - ); - assert_eq!( - truncate( - &Integer::from_str(&format!("1{}", "0".repeat(300))).unwrap(), - &consts - ) - .0, - "1 × 10^300" - ); - assert_eq!( - truncate( - &-Integer::from_str(&format!("1{}", "0".repeat(300))).unwrap(), - &consts - ) - .0, - "-1 × 10^300" - ); - assert_eq!( - truncate( - &Integer::from_str(&format!("1{}", "0".repeat(2000000))).unwrap(), - &consts - ) - .0, - "1 × 10^2000000" - ); - } - - #[test] - fn test_format_float() { - let consts = Consts::default(); - let x = Float::with_val(consts.float_precision, 1.5); - let mut acc = String::new(); - format_float(&mut acc, &x, &consts).unwrap(); - assert_eq!(acc, "1.5"); - let x = Float::with_val(consts.float_precision, -1.5); - let mut acc = String::new(); - format_float(&mut acc, &x, &consts).unwrap(); - assert_eq!(acc, "-1.5"); - let x = Float::with_val(consts.float_precision, 1); - let mut acc = String::new(); - format_float(&mut acc, &x, &consts).unwrap(); - assert_eq!(acc, "1"); - let x = Float::with_val(consts.float_precision, 1.5) - * Float::with_val(consts.float_precision, 50000).exp10(); - let mut acc = String::new(); - format_float(&mut acc, &x, &consts).unwrap(); - assert_eq!(acc, "1.5 × 10^50000"); - } + // NOTE: The factorials here might be wrong, but we don't care, we are just testing the formatting #[test] fn test_factorial_format() { @@ -860,8 +486,7 @@ mod tests { factorial .format( &mut acc, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -878,8 +503,7 @@ mod tests { factorial .format( &mut acc, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -899,8 +523,7 @@ mod tests { factorial .format( &mut acc, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -917,8 +540,7 @@ mod tests { factorial .format( &mut acc, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -935,33 +557,14 @@ mod tests { factorial .format( &mut acc, - true, - false, + FormatOptions::FORCE_SHORTEN, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), ) .unwrap(); assert_eq!(acc, "Factorial of 5 is 120 \n\n"); - } -} - -#[cfg(test)] -mod test { - use std::{str::FromStr, sync::LazyLock}; - - use factorion_math::rug::Complete; - - use super::*; - - use crate::recommended::FLOAT_PRECISION; - static TOO_BIG_NUMBER: LazyLock = - LazyLock::new(|| Integer::from_str(&format!("1{}", "0".repeat(9999))).unwrap()); - // NOTE: The factorials here might be wrong, but we don't care, we are just testing the formatting - - #[test] - fn test_format_factorial() { let consts = Consts::default(); let fact = Calculation { value: 10.into(), @@ -971,8 +574,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -991,8 +593,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1011,8 +612,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - true, - false, + FormatOptions::FORCE_SHORTEN, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1033,8 +633,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1058,8 +657,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1081,8 +679,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1101,8 +698,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1124,8 +720,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1144,8 +739,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1164,8 +758,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1184,8 +777,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1207,8 +799,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1230,8 +821,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - true, + FormatOptions::AGRESSIVE_SHORTEN, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1254,8 +844,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1278,8 +867,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1306,8 +894,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - true, - false, + FormatOptions::FORCE_SHORTEN, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1334,8 +921,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - true, - false, + FormatOptions::FORCE_SHORTEN, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1364,8 +950,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - true, - false, + FormatOptions::FORCE_SHORTEN, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1391,8 +976,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1415,8 +999,7 @@ mod test { let mut s = String::new(); fact.format( &mut s, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1500,8 +1083,7 @@ mod test { .format( &mut acc, &mut false, - true, - false, + FormatOptions::FORCE_SHORTEN, false, &consts, &locale::NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }), @@ -1516,8 +1098,7 @@ mod test { .format( &mut acc, &mut false, - true, - false, + FormatOptions::FORCE_SHORTEN, false, &consts, &locale::NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }), @@ -1532,8 +1113,7 @@ mod test { .format( &mut acc, &mut false, - true, - false, + FormatOptions::FORCE_SHORTEN, false, &consts, &locale::NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }), @@ -1552,8 +1132,7 @@ mod test { .format( &mut acc, &mut false, - true, - false, + FormatOptions::FORCE_SHORTEN, false, &consts, &locale::NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }), diff --git a/factorion-lib/src/comment.rs b/factorion-lib/src/comment.rs index c5e175f2..f1056b7d 100644 --- a/factorion-lib/src/comment.rs +++ b/factorion-lib/src/comment.rs @@ -7,12 +7,13 @@ use crate::rug::integer::IntegerExt64; use crate::rug::{Complete, Integer}; use crate::Consts; -use crate::calculation_results::Calculation; +use crate::calculation_results::{Calculation, FormatOptions, Number}; 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 { @@ -25,6 +26,7 @@ macro_rules! impl_bitwise { } }; } +#[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),*}) => { @@ -152,6 +154,9 @@ pub struct Commands { /// Disable the beginning note. #[cfg_attr(any(feature = "serde", test), serde(default))] pub no_note: bool, + /// Write out the number as a word if possible. + #[cfg_attr(any(feature = "serde", test), serde(default))] + pub write_out: bool, } impl_all_bitwise!(Commands { shorten, @@ -159,6 +164,7 @@ impl_all_bitwise!(Commands { nested, termial, no_note, + write_out, }); #[allow(dead_code)] impl Commands { @@ -168,6 +174,7 @@ impl Commands { nested: false, termial: false, no_note: false, + write_out: false, }; pub const SHORTEN: Self = Self { shorten: true, @@ -189,6 +196,10 @@ impl Commands { no_note: true, ..Self::NONE }; + pub const WRITE_OUT: Self = Self { + write_out: true, + ..Self::NONE + }; } impl Commands { @@ -211,6 +222,8 @@ impl Commands { || Self::contains_command_format(text, "triangle"), 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_num"), } } pub fn overrides_from_comment_text(text: &str) -> Self { @@ -223,6 +236,8 @@ impl Commands { 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, "normal_num")), } } } @@ -570,6 +585,11 @@ impl CommentCalculated { .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()); @@ -578,6 +598,10 @@ impl CommentCalculated { 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"); } } @@ -588,8 +612,11 @@ impl CommentCalculated { .fold(note.clone(), |mut acc, factorial| { let _ = factorial.format( &mut acc, - self.commands.shorten, - false, + FormatOptions { + force_shorten: self.commands.shorten, + write_out: self.commands.write_out, + ..FormatOptions::NONE + }, too_big_number, consts, &locale.format(), @@ -619,8 +646,10 @@ impl CommentCalculated { .fold(note, |mut acc, factorial| { let _ = factorial.format( &mut acc, - true, - false, + FormatOptions { + write_out: self.commands.write_out, + ..FormatOptions::FORCE_SHORTEN + }, too_big_number, consts, &locale.format(), @@ -644,8 +673,10 @@ impl CommentCalculated { .fold(note, |mut acc, factorial| { let _ = factorial.format( &mut acc, - true, - true, + FormatOptions { + write_out: self.commands.write_out, + ..{ FormatOptions::FORCE_SHORTEN | FormatOptions::AGRESSIVE_SHORTEN } + }, too_big_number, consts, &locale.format(), @@ -668,8 +699,11 @@ impl CommentCalculated { let mut res = String::new(); let _ = fact.format( &mut res, - true, - !self.commands.steps, + FormatOptions { + agressive_shorten: !self.commands.steps, + write_out: self.commands.write_out, + ..FormatOptions::FORCE_SHORTEN + }, too_big_number, consts, &locale.format(), @@ -847,4 +881,17 @@ mod tests { "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))*" + ); + } } diff --git a/factorion-lib/src/format.rs b/factorion-lib/src/format.rs new file mode 100644 index 00000000..35703315 --- /dev/null +++ b/factorion-lib/src/format.rs @@ -0,0 +1,574 @@ +//! This module holds the underlying formatting functions used in [`calculation_result`] +use crate::{Consts, locale}; +use factorion_math::rug::{Float, Integer, ops::Pow}; +use std::{borrow::Cow, fmt::Write}; + +const SINGLES: [&str; 10] = [ + "", "un", "duo", "tre", "quattuor", "quin", "sex", "septen", "octo", "novem", +]; +const SINGLES_LAST: [&str; 10] = [ + "", "un", "du", "tr", "quadr", "quint", "sext", "sept", "oct", "non", +]; +const TENS: [&str; 10] = [ + "", + "dec", + "vigin", + "trigin", + "quadragin", + "quinquagin", + "sexagin", + "septuagin", + "octogin", + "nonagin", +]; +const HUNDREDS: [&str; 10] = [ + "", + "cen", + "ducen", + // Note this differs from the wikipedia list to disambiguate from 103, which continuing the pattern should be trecentuple + "tricen", + "quadringen", + "quingen", + "sescen", + "septingen", + "octingen", + "nongen", +]; +// Note that other than milluple, these are not found in a list, but continue the pattern from mill with different starts +const THOUSANDS: [&str; 10] = [ + "", "mill", "bill", "trill", "quadrill", "quintill", "sextill", "septill", "octill", "nonill", +]; +const TEN_THOUSANDS: [&str; 10] = [ + "", + "decill", + "vigintill", + "trigintill", + "quadragintill", + "quinquagintill", + "sexagintill", + "septuagintill", + "octogintill", + "nonagintill", +]; +const HUNDRED_THOUSANDS: [&str; 10] = [ + "", + "centill", + "ducentill", + "tricentill", + "quadringentill", + "quingentill", + "sescentill", + "septingentill", + "octingentill", + "nongentill", +]; +const BINDING_T: [[bool; 10]; 6] = [ + // Singles + [ + false, false, false, false, false, false, false, false, false, false, + ], + // Tens + [false, false, true, true, true, true, true, true, true, true], + // Hundreds + [false, true, true, true, true, true, true, true, true, true], + // Thousands + [ + false, false, false, false, false, false, false, false, false, false, + ], + // Tenthousands + [ + false, false, false, false, false, false, false, false, false, false, + ], + // Hundredthousands + [ + false, false, false, false, false, false, false, false, false, false, + ], +]; +pub fn get_factorial_level_string<'a>(level: i32, locale: &'a locale::Format<'a>) -> Cow<'a, str> { + if let Some(s) = locale.num_overrides().get(&level) { + return s.as_ref().into(); + } + match level { + 0 => locale.sub().as_ref().into(), + 1 => "{factorial}".into(), + ..=999999 if !locale.force_num() => { + let singles = if level < 10 { SINGLES_LAST } else { SINGLES }; + let mut acc = String::new(); + let mut n = level; + let s = n % 10; + n /= 10; + acc.write_str(singles[s as usize]).unwrap(); + let t = n % 10; + n /= 10; + acc.write_str(TENS[t as usize]).unwrap(); + let h = n % 10; + n /= 10; + acc.write_str(HUNDREDS[h as usize]).unwrap(); + let th = n % 10; + n /= 10; + acc.write_str(THOUSANDS[th as usize]).unwrap(); + let tth = n % 10; + n /= 10; + acc.write_str(TEN_THOUSANDS[tth as usize]).unwrap(); + let hth = n % 10; + acc.write_str(HUNDRED_THOUSANDS[hth as usize]).unwrap(); + // Check if we need tuple not uple + let last_written = [s, t, h, th, tth, hth] + .iter() + .cloned() + .enumerate() + .rev() + .find(|(_, n)| *n != 0) + .unwrap(); + if BINDING_T[last_written.0][last_written.1 as usize] { + acc.write_str("t").unwrap(); + } + acc.write_str(locale.uple()).unwrap(); + + acc.into() + } + _ => { + let mut suffix = String::new(); + write!(&mut suffix, "{level}-{{factorial}}").unwrap(); + suffix.into() + } + } +} +const EN_SINGLES: [&str; 10] = [ + "", "one ", "two ", "three ", "four ", "five ", "six ", "seven ", "eight ", "nine ", +]; +const EN_TENS: [&str; 10] = [ + "", "ten ", "twenty ", "thirty ", "forty ", "fivety ", "sixty ", "seventy ", "eighty ", + "ninety ", +]; +const EN_TENS_SINGLES: [&str; 10] = [ + "ten ", + "eleven ", + "twelve ", + "thirteen ", + "fourteen ", + "fiveteen ", + "sixteen ", + "seventeen ", + "eighteen ", + "nineteen ", +]; +const SINGLES_LAST_ILLION: [&str; 10] = [ + "", "m", "b", "tr", "quadr", "quint", "sext", "sept", "oct", "non", +]; +// TODO: localize (illion, illiard, type of scale, numbers, digit order, thousand) +pub fn write_out_number(acc: &mut String, num: &Integer, consts: &Consts) -> std::fmt::Result { + if num == &0 { + return acc.write_str("zero"); + } + let negative = num < &0; + let num = Float::with_val(consts.float_precision, num).abs(); + let ten = Float::with_val(consts.float_precision, 10); + let digit_blocks = num + .clone() + .log10() + .to_u32_saturating_round(factorion_math::rug::float::Round::Down) + .unwrap() + / 3; + if negative { + acc.write_str("minus ")?; + } + for digit_blocks_left in (digit_blocks.saturating_sub(5)..=digit_blocks).rev() { + let current_digits = Float::to_u32_saturating_round( + &((num.clone() / ten.clone().pow(digit_blocks_left * 3)) % 1000), + factorion_math::rug::float::Round::Down, + ) + .unwrap(); + let mut n = current_digits; + let s = n % 10; + n /= 10; + let t = n % 10; + n /= 10; + let h = n % 10; + acc.write_str(EN_SINGLES[h as usize])?; + if h != 0 { + acc.write_str("hundred ")?; + } + if t == 1 { + acc.write_str(EN_TENS_SINGLES[s as usize])?; + } else { + acc.write_str(EN_TENS[t as usize])?; + acc.write_str(EN_SINGLES[s as usize])?; + } + + if digit_blocks_left > 0 && current_digits != 0 { + let singles = if digit_blocks_left < 10 { + SINGLES_LAST_ILLION + } else { + SINGLES + }; + let mut n = digit_blocks_left - 1; + if n > 0 { + let s = n % 10; + n /= 10; + acc.write_str(singles[s as usize]).unwrap(); + let t = n % 10; + n /= 10; + acc.write_str(TENS[t as usize]).unwrap(); + let h = n % 10; + n /= 10; + acc.write_str(HUNDREDS[h as usize]).unwrap(); + let th = n % 10; + n /= 10; + acc.write_str(THOUSANDS[th as usize]).unwrap(); + let tth = n % 10; + n /= 10; + acc.write_str(TEN_THOUSANDS[tth as usize]).unwrap(); + let hth = n % 10; + acc.write_str(HUNDRED_THOUSANDS[hth as usize]).unwrap(); + // Check if we need tuple not uple + let last_written = [s, t, h, th, tth, hth] + .iter() + .cloned() + .enumerate() + .rev() + .find(|(_, n)| *n != 0) + .unwrap(); + if BINDING_T[last_written.0][last_written.1 as usize] { + acc.write_str("t").unwrap(); + } + acc.write_str("illion ")?; + } else { + acc.write_str("thousand ")?; + } + } + } + acc.pop(); + Ok(()) +} +/// Rounds a base 10 number string. \ +/// Uses the last digit to decide the rounding direction. \ +/// Rounds over 9s. This does **not** keep the length or turn rounded over digits into zeros. \ +/// If the input is all 9s, this will round to 10. \ +/// Stops when a decimal period is encountered, removing it. +/// +/// Returns whether it overflowed. +/// +/// # Panic +/// This function may panic if less than two digits are supplied, or if it contains a non-digit of base 10, that is not a period. +pub fn round(number: &mut String) -> bool { + // Check additional digit if we need to round + if let Some(digit) = number + .pop() + .map(|n| n.to_digit(10).expect("Not a base 10 number")) + && digit >= 5 + { + let mut last_digit = number + .pop() + .and_then(|n| n.to_digit(10)) + .expect("Not a base 10 number"); + // Carry over at 9s + while last_digit == 9 { + let Some(digit) = number.pop() else { + // If we reached the end we get 10 + number.push_str("10"); + return true; + }; + // Stop at decimal + if digit == '.' { + break; + } + let digit = digit.to_digit(10).expect("Not a base 10 number"); + last_digit = digit; + } + // Round up + let _ = write!(number, "{}", last_digit + 1); + } + false +} +pub fn truncate(number: &Integer, consts: &Consts) -> (String, bool) { + let prec = consts.float_precision; + if number == &0 { + return (number.to_string(), false); + } + let negative = number.is_negative(); + let orig_number = number; + let number = number.clone().abs(); + let length = (Float::with_val(prec, &number).ln() / Float::with_val(prec, 10).ln()) + .to_integer_round(crate::rug::float::Round::Down) + .unwrap() + .0; + let truncated_number: Integer = &number + / (Float::with_val(prec, 10) + .pow((length.clone() - consts.number_decimals_scientific - 1u8).max(Integer::ZERO)) + .to_integer() + .unwrap()); + let mut truncated_number = truncated_number.to_string(); + if truncated_number.len() > consts.number_decimals_scientific { + round(&mut truncated_number); + } + if let Some(mut digit) = truncated_number.pop() { + while digit == '0' { + digit = match truncated_number.pop() { + Some(x) => x, + None => break, + } + } + truncated_number.push(digit); + } + // Only add decimal if we have more than one digit + if truncated_number.len() > 1 { + truncated_number.insert(1, '.'); // Decimal point + } + if negative { + truncated_number.insert(0, '-'); + } + if length > consts.number_decimals_scientific + 1 { + (format!("{truncated_number} × 10^{length}"), true) + } else { + (orig_number.to_string(), false) + } +} +pub fn format_float(acc: &mut String, number: &Float, consts: &Consts) -> std::fmt::Result { + // -a.b x 10^c + // - + // a + // .b + // x 10^c + let mut number = number.clone(); + let negative = number.is_sign_negative(); + number = number.abs(); + if number == 0 { + return acc.write_char('0'); + } + let exponent = number + .clone() + .log10() + .to_integer_round(factorion_math::rug::float::Round::Down) + .expect("Could not round exponent") + .0; + if exponent > consts.number_decimals_scientific + || exponent < -(consts.number_decimals_scientific as isize) + { + number = number / Float::with_val(consts.float_precision, &exponent).exp10(); + } + let mut whole_number = number + .to_integer_round(factorion_math::rug::float::Round::Down) + .expect("Could not get integer part") + .0; + let decimal_part = number - &whole_number + 1; + let mut decimal_part = format!("{decimal_part}"); + // Remove "1." + decimal_part.remove(0); + decimal_part.remove(0); + decimal_part.truncate(consts.number_decimals_scientific + 1); + if decimal_part.len() > consts.number_decimals_scientific { + if round(&mut decimal_part) { + decimal_part.clear(); + whole_number += 1; + } + } + if let Some(mut digit) = decimal_part.pop() { + while digit == '0' { + digit = match decimal_part.pop() { + Some(x) => x, + None => break, + } + } + decimal_part.push(digit); + } + if negative { + acc.write_str("-")?; + } + write!(acc, "{whole_number}")?; + if !decimal_part.is_empty() && decimal_part != "0" { + acc.write_str(".")?; + acc.write_str(&decimal_part)?; + } + if exponent > consts.number_decimals_scientific + || exponent < -(consts.number_decimals_scientific as isize) + { + write!(acc, " × 10^{exponent}")?; + } + Ok(()) +} + +pub fn replace(s: &mut String, search_start: usize, from: &str, to: &str) -> usize { + if let Some(start) = s[search_start..].find(from) { + let start = start + search_start; + s.replace_range(start..(start + from.len()), to); + start + } else { + s.len() + } +} +#[cfg(test)] +mod tests { + use super::*; + use crate::rug::Integer; + use std::str::FromStr; + + #[test] + fn test_round_down() { + let mut number = String::from("1929472373"); + round(&mut number); + assert_eq!(number, "192947237"); + } + + #[test] + fn test_round_up() { + let mut number = String::from("74836748625"); + round(&mut number); + assert_eq!(number, "7483674863"); + } + + #[test] + fn test_round_carry() { + let mut number = String::from("24999999995"); + round(&mut number); + assert_eq!(number, "25"); + } + + #[test] + fn test_factorial_level_string() { + let en = locale::get_en(); + assert_eq!(get_factorial_level_string(1, &en.format()), "{factorial}"); + assert_eq!( + get_factorial_level_string(2, &en.format()), + "double-{factorial}" + ); + assert_eq!( + get_factorial_level_string(3, &en.format()), + "triple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(10, &en.format()), + "decuple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(45, &en.format()), + "quinquadragintuple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(50, &en.format()), + "quinquagintuple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(100, &en.format()), + "centuple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(521, &en.format()), + "unviginquingentuple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(1000, &en.format()), + "milluple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(4321, &en.format()), + "unvigintricenquadrilluple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(89342, &en.format()), + "duoquadragintricennonilloctogintilluple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(654321, &en.format()), + "unvigintricenquadrillquinquagintillsescentilluple-{factorial}" + ); + assert_eq!( + get_factorial_level_string(1000000, &en.format()), + "1000000-{factorial}" + ); + let de = locale::get_de(); + assert_eq!(get_factorial_level_string(1, &de.format()), "{factorial}"); + assert_eq!( + get_factorial_level_string(2, &de.format()), + "doppel{factorial}" + ); + assert_eq!( + get_factorial_level_string(3, &de.format()), + "trippel{factorial}" + ); + assert_eq!( + get_factorial_level_string(45, &de.format()), + "quinquadragintupel{factorial}" + ); + } + + #[test] + fn test_write_out_number() { + let consts = Consts::default(); + let mut acc = String::new(); + write_out_number( + &mut acc, + &"1234567890123456789000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + .parse() + .unwrap(), + &consts, + ) + .unwrap(); + assert_eq!( + acc, + "one tredeccentillion two hundred thirty four duodeccentillion five hundred sixty seven undeccentillion eight hundred ninety deccentillion one hundred twenty three novemcentillion four hundred fivety six octocentillion" + ); + let mut acc = String::new(); + write_out_number(&mut acc, &"123456789".parse().unwrap(), &consts).unwrap(); + assert_eq!( + acc, + "one hundred twenty three million four hundred fivety six thousand seven hundred eighty nine" + ); + } + + #[test] + fn test_truncate() { + let consts = Consts::default(); + assert_eq!(truncate(&Integer::from_str("0").unwrap(), &consts,).0, "0"); + assert_eq!( + truncate(&Integer::from_str("-1").unwrap(), &consts,).0, + "-1" + ); + assert_eq!( + truncate( + &Integer::from_str(&format!("1{}", "0".repeat(300))).unwrap(), + &consts + ) + .0, + "1 × 10^300" + ); + assert_eq!( + truncate( + &-Integer::from_str(&format!("1{}", "0".repeat(300))).unwrap(), + &consts + ) + .0, + "-1 × 10^300" + ); + assert_eq!( + truncate( + &Integer::from_str(&format!("1{}", "0".repeat(2000000))).unwrap(), + &consts + ) + .0, + "1 × 10^2000000" + ); + } + + #[test] + fn test_format_float() { + let consts = Consts::default(); + let x = Float::with_val(consts.float_precision, 1.5); + let mut acc = String::new(); + format_float(&mut acc, &x, &consts).unwrap(); + assert_eq!(acc, "1.5"); + let x = Float::with_val(consts.float_precision, -1.5); + let mut acc = String::new(); + format_float(&mut acc, &x, &consts).unwrap(); + assert_eq!(acc, "-1.5"); + let x = Float::with_val(consts.float_precision, 1); + let mut acc = String::new(); + format_float(&mut acc, &x, &consts).unwrap(); + assert_eq!(acc, "1"); + let x = Float::with_val(consts.float_precision, 1.5) + * Float::with_val(consts.float_precision, 50000).exp10(); + let mut acc = String::new(); + format_float(&mut acc, &x, &consts).unwrap(); + assert_eq!(acc, "1.5 × 10^50000"); + } +} diff --git a/factorion-lib/src/lib.rs b/factorion-lib/src/lib.rs index 0b44434a..f6f2730e 100644 --- a/factorion-lib/src/lib.rs +++ b/factorion-lib/src/lib.rs @@ -7,6 +7,7 @@ use rug::Integer; pub mod calculation_results; pub mod calculation_tasks; pub mod comment; +pub(crate) mod format; pub mod locale; pub mod parse; diff --git a/factorion-lib/tests/integration.rs b/factorion-lib/tests/integration.rs index 0300b0c2..5eddbb57 100644 --- a/factorion-lib/tests/integration.rs +++ b/factorion-lib/tests/integration.rs @@ -1751,6 +1751,78 @@ fn test_all_that_on_multiple_calcs() { ) } +#[test] +fn test_command_write_out() { + let consts = Consts::default(); + let comment = Comment::new("176902! !write_out", (), Commands::NONE, MAX_LENGTH, "en") + .extract(&consts) + .calc(&consts); + + let reply = comment.get_reply(&consts); + assert_eq!( + reply, + "Factorial of one hundred seventy six thousand nine hundred two is seventy five quintriginoctingentrilloctogintillducentillillion five hundred forty five quattuortriginoctingentrilloctogintillducentillillion seven hundred sixty six tretriginoctingentrilloctogintillducentillillion three hundred five duotriginoctingentrilloctogintillducentillillion forty seven untriginoctingentrilloctogintillducentillillion one hundred forty three triginoctingentrilloctogintillducentillillion \n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*" + ) +} + +#[test] +fn test_write_out_zero_factorial() { + let consts = Consts::default(); + let comment = Comment::new("0! [write_out]", (), Commands::NONE, MAX_LENGTH, "en") + .extract(&consts) + .calc(&consts); + let reply = comment.get_reply(&consts); + assert_eq!( + reply, + "Factorial of zero is one \n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*" + ); +} + +#[test] +fn test_write_out_minus_one_factorial() { + let consts = Consts::default(); + let comment = Comment::new("(-1)! [write_out]", (), Commands::NONE, MAX_LENGTH, "en") + .extract(&consts) + .calc(&consts); + let reply = comment.get_reply(&consts); + assert_eq!( + reply, + "Factorial of minus one is complex infinity \n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*" + ); +} + +#[test] +fn test_write_out_factorial_digits() { + let consts = Consts::default(); + let comment = Comment::new( + "67839127837442873498364307437846329874293874384739847347394748012940124093748389701473461687364012630527560276507263724678234685360158032147349867349837403928573587255865587234672880756378340253167320767378467507576450878320574087430274607215697523720397460949849834384772847384738474837484774639847374! [write_out]", + (), + Commands::NO_NOTE, + MAX_LENGTH, + "en" + ).extract(&consts) + .calc(&consts); + let reply = comment.get_reply(&consts); + assert_eq!( + reply, + "Factorial of sixty seven novemnonagintillion eight hundred thirty nine octononagintillion one hundred twenty seven septennonagintillion eight hundred thirty seven sexnonagintillion four hundred forty two quinnonagintillion eight hundred seventy three quattuornonagintillion has approximately twenty centillion four hundred forty six novemnonagintillion five hundred twenty two octononagintillion two hundred fiveteen septennonagintillion five hundred sixty four sexnonagintillion two hundred thirty six quinnonagintillion digits \n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*" + ); +} + +#[test] +fn test_command_write_out_non_en() { + let consts = Consts::default(); + let comment = Comment::new("176902! !write_out", (), Commands::NONE, 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 hundred seventy six thousand nine hundred two ist seventy five quintriginoctingentrilloctogintillducentillillion five hundred forty five quattuortriginoctingentrilloctogintillducentillillion seven hundred sixty six tretriginoctingentrilloctogintillducentillillion three hundred five duotriginoctingentrilloctogintillducentillillion forty seven untriginoctingentrilloctogintillducentillillion one hundred forty three triginoctingentrilloctogintillducentillillion \n\n\n*^(Dieser Kommentar wurde automatisch geschrieben | [Quelltext](http://f.r0.fyi))*" + ) +} + #[test] fn test_arbitrary_comment() { let consts = Consts::default(); diff --git a/factorion-math/Cargo.toml b/factorion-math/Cargo.toml index 6fe606a2..ac5aee64 100644 --- a/factorion-math/Cargo.toml +++ b/factorion-math/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "factorion-math" -version = "1.0.1" +version = "1.0.2" edition = "2024" description = "The math (factorials and related functions) used by factorion" license = "MIT" diff --git a/factorion-math/src/lib.rs b/factorion-math/src/lib.rs index 4fc4b1c5..734d564a 100644 --- a/factorion-math/src/lib.rs +++ b/factorion-math/src/lib.rs @@ -347,12 +347,13 @@ pub fn adjust_approximate((x, e): (Float, Integer)) -> (Float, Integer) { (x, total_exponent) } -/// Number of digits of n (log10, but 1 if 0) +/// Number of digits of n (log10 of absolute, but 1 if 0) pub fn length(n: &Integer, prec: u32) -> Integer { if n == &0 { return Integer::ONE.clone(); } - (Float::with_val(prec, n).ln() / Float::with_val(prec, 10).ln()) + + (Float::with_val(prec, n).abs().ln() / Float::with_val(prec, 10).ln()) .to_integer_round(rug::float::Round::Down) .unwrap() .0