From 8a6d3a42d1715bf4d2311454af648c586a57b9cc Mon Sep 17 00:00:00 2001 From: Aras14HD Date: Wed, 14 Jan 2026 16:54:22 +0100 Subject: [PATCH 1/6] print number as word --- factorion-lib/src/calculation_results.rs | 407 ++++++++++++++--------- 1 file changed, 256 insertions(+), 151 deletions(-) diff --git a/factorion-lib/src/calculation_results.rs b/factorion-lib/src/calculation_results.rs index 6b35044..a48d3db 100644 --- a/factorion-lib/src/calculation_results.rs +++ b/factorion-lib/src/calculation_results.rs @@ -362,7 +362,7 @@ impl Calculation { acc, start, "{factorial}", - &Self::get_factorial_level_string(level.abs(), locale), + &get_factorial_level_string(level.abs(), locale), ); replace( @@ -397,139 +397,235 @@ impl Calculation { Ok(()) } +} +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, + ], +]; +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(); - 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(); + 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 ", "fourty ", "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", +]; +fn write_out_number(acc: &mut String, num: &Integer, consts: &Consts) -> std::fmt::Result { + let num = Float::with_val(consts.float_precision, num); + 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; + dbg!(digit_blocks); + for digit_blocks_left in (digit_blocks.saturating_sub(5)..=digit_blocks).rev() { + dbg!(digit_blocks_left); + 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(); + dbg!(current_digits); + 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])?; } - 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() + 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; + 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 ")?; } } + acc.pop(); + Ok(()) } /// Rounds a base 10 number string. \ /// Uses the last digit to decide the rounding direction. \ @@ -721,77 +817,86 @@ mod tests { #[test] fn test_factorial_level_string() { let en = locale::get_en(); + assert_eq!(get_factorial_level_string(1, &en.format()), "{factorial}"); assert_eq!( - Calculation::get_factorial_level_string(1, &en.format()), - "{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(2, &en.format()), + get_factorial_level_string(2, &en.format()), "double-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(3, &en.format()), + get_factorial_level_string(3, &en.format()), "triple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(10, &en.format()), + get_factorial_level_string(10, &en.format()), "decuple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(45, &en.format()), + get_factorial_level_string(45, &en.format()), "quinquadragintuple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(50, &en.format()), + get_factorial_level_string(50, &en.format()), "quinquagintuple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(100, &en.format()), + get_factorial_level_string(100, &en.format()), "centuple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(521, &en.format()), + get_factorial_level_string(521, &en.format()), "unviginquingentuple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(1000, &en.format()), + get_factorial_level_string(1000, &en.format()), "milluple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(4321, &en.format()), + get_factorial_level_string(4321, &en.format()), "unvigintricenquadrilluple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(89342, &en.format()), + get_factorial_level_string(89342, &en.format()), "duoquadragintricennonilloctogintilluple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(654321, &en.format()), + get_factorial_level_string(654321, &en.format()), "unvigintricenquadrillquinquagintillsescentilluple-{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(1000000, &en.format()), + 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!( - Calculation::get_factorial_level_string(1, &de.format()), - "{factorial}" - ); - assert_eq!( - Calculation::get_factorial_level_string(2, &de.format()), + get_factorial_level_string(2, &de.format()), "doppel{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(3, &de.format()), + get_factorial_level_string(3, &de.format()), "trippel{factorial}" ); assert_eq!( - Calculation::get_factorial_level_string(45, &de.format()), + 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, ""); + } + #[test] fn test_truncate() { let consts = Consts::default(); From aab06be06084cc861e1a23547364bf521237a2f6 Mon Sep 17 00:00:00 2001 From: Aras14HD Date: Mon, 9 Feb 2026 09:39:18 +0100 Subject: [PATCH 2/6] add write out command --- factorion-lib/README.md | 4 +- factorion-lib/src/calculation_results.rs | 252 +++++++++++++---------- factorion-lib/src/comment.rs | 48 ++++- factorion-lib/tests/integration.rs | 14 ++ 4 files changed, 199 insertions(+), 119 deletions(-) diff --git a/factorion-lib/README.md b/factorion-lib/README.md index a4c18d0..817c6e9 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 a48d3db..0502026 100644 --- a/factorion-lib/src/calculation_results.rs +++ b/factorion-lib/src/calculation_results.rs @@ -1,7 +1,14 @@ //! This module handles the formatting of the calculations (`The factorial of Subfactorial of 5 is`, etc.) +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}; @@ -110,17 +117,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 +165,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 +179,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(")")?; @@ -152,7 +191,7 @@ impl CalculationResult { if is_value { acc.write_str("10^(")?; } - if shorten { + if opts.force_shorten { acc.write_str(&truncate(digits, consts).0)?; } else { write!(acc, "{digits}")?; @@ -170,7 +209,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 +219,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}")?; @@ -276,6 +316,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 +333,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 +343,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 +366,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 +386,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(), @@ -426,7 +480,7 @@ const HUNDREDS: [&str; 10] = [ "quingen", "sescen", "septingen", - "octingen ", + "octingen", "nongen", ]; // Note that other than milluple, these are not found in a list, but continue the pattern from mill with different starts @@ -551,6 +605,7 @@ const EN_TENS_SINGLES: [&str; 10] = [ 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) fn write_out_number(acc: &mut String, num: &Integer, consts: &Consts) -> std::fmt::Result { let num = Float::with_val(consts.float_precision, num); let ten = Float::with_val(consts.float_precision, 10); @@ -560,15 +615,12 @@ fn write_out_number(acc: &mut String, num: &Integer, consts: &Consts) -> std::fm .to_u32_saturating_round(factorion_math::rug::float::Round::Down) .unwrap() / 3; - dbg!(digit_blocks); for digit_blocks_left in (digit_blocks.saturating_sub(5)..=digit_blocks).rev() { - dbg!(digit_blocks_left); 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(); - dbg!(current_digits); let mut n = current_digits; let s = n % 10; n /= 10; @@ -592,36 +644,40 @@ fn write_out_number(acc: &mut String, num: &Integer, consts: &Consts) -> std::fm } else { SINGLES }; - let mut n = digit_blocks_left; - 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(); + 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.write_str("illion ")?; } } acc.pop(); @@ -894,7 +950,16 @@ mod tests { &consts, ) .unwrap(); - assert_eq!(acc, ""); + 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] @@ -965,8 +1030,7 @@ mod tests { factorial .format( &mut acc, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -983,8 +1047,7 @@ mod tests { factorial .format( &mut acc, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1004,8 +1067,7 @@ mod tests { factorial .format( &mut acc, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1022,8 +1084,7 @@ mod tests { factorial .format( &mut acc, - false, - false, + FormatOptions::NONE, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1040,8 +1101,7 @@ mod tests { factorial .format( &mut acc, - true, - false, + FormatOptions::FORCE_SHORTEN, &TOO_BIG_NUMBER, &consts, &consts.locales.get("en").unwrap().format(), @@ -1076,8 +1136,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(), @@ -1096,8 +1155,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(), @@ -1116,8 +1174,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(), @@ -1138,8 +1195,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(), @@ -1163,8 +1219,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(), @@ -1186,8 +1241,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(), @@ -1206,8 +1260,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(), @@ -1229,8 +1282,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(), @@ -1249,8 +1301,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(), @@ -1269,8 +1320,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(), @@ -1289,8 +1339,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(), @@ -1312,8 +1361,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(), @@ -1335,8 +1383,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(), @@ -1359,8 +1406,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(), @@ -1383,8 +1429,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(), @@ -1411,8 +1456,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(), @@ -1439,8 +1483,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(), @@ -1469,8 +1512,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(), @@ -1496,8 +1538,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(), @@ -1520,8 +1561,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(), @@ -1605,8 +1645,7 @@ mod test { .format( &mut acc, &mut false, - true, - false, + FormatOptions::FORCE_SHORTEN, false, &consts, &locale::NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }), @@ -1621,8 +1660,7 @@ mod test { .format( &mut acc, &mut false, - true, - false, + FormatOptions::FORCE_SHORTEN, false, &consts, &locale::NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }), @@ -1637,8 +1675,7 @@ mod test { .format( &mut acc, &mut false, - true, - false, + FormatOptions::FORCE_SHORTEN, false, &consts, &locale::NumFormat::V1(&locale::v1::NumFormat { decimal: '.' }), @@ -1657,8 +1694,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 e3227a7..98c6107 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()); @@ -588,8 +608,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 +642,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(), @@ -640,8 +665,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(), @@ -660,8 +687,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(), diff --git a/factorion-lib/tests/integration.rs b/factorion-lib/tests/integration.rs index 5e5c67a..3491cb3 100644 --- a/factorion-lib/tests/integration.rs +++ b/factorion-lib/tests/integration.rs @@ -1732,6 +1732,20 @@ 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 fourty five quattuortriginoctingentrilloctogintillducentillillion seven hundred sixty six tretriginoctingentrilloctogintillducentillillion three hundred five duotriginoctingentrilloctogintillducentillillion fourty seven untriginoctingentrilloctogintillducentillillion one hundred fourty three triginoctingentrilloctogintillducentillillion \n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*" + ) +} + #[test] fn test_arbitrary_comment() { let consts = Consts::default(); From e6d7baf9977fcd4b177df9e8f2068a202c20f42f Mon Sep 17 00:00:00 2001 From: Aras14HD Date: Fri, 27 Feb 2026 19:34:04 +0100 Subject: [PATCH 3/6] bump version minor (semver major for lib) --- Cargo.lock | 6 +++--- factorion-bot-discord/Cargo.toml | 4 ++-- factorion-bot-reddit/Cargo.toml | 4 ++-- factorion-lib/Cargo.toml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97f87dd..c0c2e26 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", diff --git a/factorion-bot-discord/Cargo.toml b/factorion-bot-discord/Cargo.toml index dc3805d..960c09c 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 e9e7f72..2cec897 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 f40cb22..d2c12f9 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" From 0618c6957b22c9086279cc78d9915e20a9ee6b29 Mon Sep 17 00:00:00 2001 From: Aras14HD Date: Sun, 1 Mar 2026 17:12:20 +0100 Subject: [PATCH 4/6] add write out support warning --- factorion-lib/src/comment.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/factorion-lib/src/comment.rs b/factorion-lib/src/comment.rs index 98c6107..cfa1547 100644 --- a/factorion-lib/src/comment.rs +++ b/factorion-lib/src/comment.rs @@ -598,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"); } } @@ -869,4 +873,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))*" + ); + } } From d08c65a9a5fc5fc961a4b9c3ead56aaf7473947e Mon Sep 17 00:00:00 2001 From: Aras14HD Date: Sat, 7 Mar 2026 13:22:33 +0100 Subject: [PATCH 5/6] add tests write out non-en, zero, negative and digits, fix/implement them fix factorion_math::length of negative numbers (use abs) add write_out for ApproximateDigits add write_out for ComplexInfinity add minus and zero support to write_out_number fix spelling of forty --- Cargo.lock | 2 +- factorion-lib/Cargo.toml | 2 +- factorion-lib/src/calculation_results.rs | 39 ++++++++++----- factorion-lib/tests/integration.rs | 60 +++++++++++++++++++++++- factorion-math/Cargo.toml | 2 +- factorion-math/src/lib.rs | 5 +- 6 files changed, 92 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0c2e26..cf573d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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-lib/Cargo.toml b/factorion-lib/Cargo.toml index d2c12f9..e2d44df 100644 --- a/factorion-lib/Cargo.toml +++ b/factorion-lib/Cargo.toml @@ -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/src/calculation_results.rs b/factorion-lib/src/calculation_results.rs index 0502026..1e6e286 100644 --- a/factorion-lib/src/calculation_results.rs +++ b/factorion-lib/src/calculation_results.rs @@ -188,16 +188,20 @@ impl CalculationResult { } } CalculationResult::ApproximateDigits(_, digits) => { - if is_value { - acc.write_str("10^(")?; - } - if opts.force_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) => { @@ -247,7 +251,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() != '.' { @@ -587,7 +595,7 @@ const EN_SINGLES: [&str; 10] = [ "", "one ", "two ", "three ", "four ", "five ", "six ", "seven ", "eight ", "nine ", ]; const EN_TENS: [&str; 10] = [ - "", "ten ", "twenty ", "thirty ", "fourty ", "fivety ", "sixty ", "seventy ", "eighty ", + "", "ten ", "twenty ", "thirty ", "forty ", "fivety ", "sixty ", "seventy ", "eighty ", "ninety ", ]; const EN_TENS_SINGLES: [&str; 10] = [ @@ -607,7 +615,11 @@ const SINGLES_LAST_ILLION: [&str; 10] = [ ]; // TODO: localize (illion, illiard, type of scale, numbers, digit order, thousand) fn write_out_number(acc: &mut String, num: &Integer, consts: &Consts) -> std::fmt::Result { - let num = Float::with_val(consts.float_precision, num); + 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() @@ -615,6 +627,9 @@ fn write_out_number(acc: &mut String, num: &Integer, consts: &Consts) -> std::fm .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), diff --git a/factorion-lib/tests/integration.rs b/factorion-lib/tests/integration.rs index 344bdc4..5eddbb5 100644 --- a/factorion-lib/tests/integration.rs +++ b/factorion-lib/tests/integration.rs @@ -1761,7 +1761,65 @@ fn test_command_write_out() { 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 fourty five quattuortriginoctingentrilloctogintillducentillillion seven hundred sixty six tretriginoctingentrilloctogintillducentillillion three hundred five duotriginoctingentrilloctogintillducentillillion fourty seven untriginoctingentrilloctogintillducentillillion one hundred fourty three triginoctingentrilloctogintillducentillillion \n\n\n*^(This action was performed by a bot | [Source code](http://f.r0.fyi))*" + "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))*" ) } diff --git a/factorion-math/Cargo.toml b/factorion-math/Cargo.toml index 6fe606a..ac5aee6 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 4fc4b1c..734d564 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 From d7058000d571afc73c8b5378141bdfacaa812018 Mon Sep 17 00:00:00 2001 From: Aras14HD Date: Sat, 7 Mar 2026 13:41:58 +0100 Subject: [PATCH 6/6] separate out format module (pub(crate)) --- factorion-lib/src/calculation_results.rs | 591 +---------------------- factorion-lib/src/format.rs | 574 ++++++++++++++++++++++ factorion-lib/src/lib.rs | 1 + 3 files changed, 582 insertions(+), 584 deletions(-) create mode 100644 factorion-lib/src/format.rs diff --git a/factorion-lib/src/calculation_results.rs b/factorion-lib/src/calculation_results.rs index 1e6e286..183e152 100644 --- a/factorion-lib/src/calculation_results.rs +++ b/factorion-lib/src/calculation_results.rs @@ -1,5 +1,8 @@ //! 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; @@ -11,10 +14,9 @@ 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; @@ -460,578 +462,17 @@ impl Calculation { Ok(()) } } -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, - ], -]; -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) -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. -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!(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"); - } + // NOTE: The factorials here might be wrong, but we don't care, we are just testing the formatting #[test] fn test_factorial_format() { @@ -1123,25 +564,7 @@ mod tests { ) .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(), diff --git a/factorion-lib/src/format.rs b/factorion-lib/src/format.rs new file mode 100644 index 0000000..3570331 --- /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 0b44434..f6f2730 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;