From 4f398d06e96d3ba603abfa4b8241f7b503ca18a5 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Tue, 26 Mar 2024 01:09:16 -0500 Subject: [PATCH 01/52] No Deps Working Example with i32 --- src/lang/es.rs | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lang/mod.rs | 1 + 2 files changed, 186 insertions(+) create mode 100644 src/lang/es.rs diff --git a/src/lang/es.rs b/src/lang/es.rs new file mode 100644 index 0000000..4d9f132 --- /dev/null +++ b/src/lang/es.rs @@ -0,0 +1,185 @@ +// Reference that can hopefully be implemented seamlessly: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol +const UNIDADES: [&str; 10] = [ + "", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve", +]; +// Decenas que son entre 11 y 19 +const DIECIS: [&str; 10] = [ + "diez", // Needed for cases like 10, 10_000 and 10_000_000 + "once", + "doce", + "trece", + "catorce", + "quince", + "dieciséis", + "diecisiete", + "dieciocho", + "diecinueve", +]; +// Saltos en decenas +const DECENAS: [&str; 10] = [ + "", + "diez", + "veinte", + "treinta", + "cuarenta", + "cincuenta", + "sesenta", + "setenta", + "ochenta", + "noventa", +]; +// Saltos en decenas +// Binary size might see a dozen bytes improvement if we append "ientos" at CENTENAS's callsites +const CENTENAS: [&str; 10] = [ + "", + "ciento", + "doscientos", + "trescientos", + "cuatrocientos", + "quinientos", + "seiscientos", + "setecientos", + "ochocientos", + "novecientos", +]; +// To ensure both arrays doesn't desync +const MILLAR_SIZE: usize = 21; +/// from source: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol +/// Based on https://en.wikipedia.org/wiki/Names_of_large_numbers, each thousands is from the Short Scales, +/// which each thousands can be defined as 10^(3n+3) magnitude, where n is replaced by the index of the Array. For example +/// 10^3 = Thousands (starts at n=1 here) +/// 10^6 = Millions +/// 10^9 = Billions +/// 10^33 = Decillion +// Saltos en Millares +const MILLARES: [&str; MILLAR_SIZE] = [ + "", + "mil", + "millones", + "billones", + "trillones", + "cuatrillones", + "quintillones", + "sextillones", + "octillones", + "nonillones", + "decillones", + "undecillones", + "duodecillones", + "tredecillones", + "cuatrodecillones", + "quindeciollones", + "sexdecillones", + "septendecillones", + "octodecillones", + "novendecillones", + "vigintillones", +]; +// Saltos en Millar +const MILLAR: [&str; MILLAR_SIZE] = [ + "", + "mil", + "millón", + "billón", + "trillón", + "cuatrillón", + "quintillón", + "sextillón", + "octillón", + "nonillón", + "decillón", + "undecillón", + "duodecillón", + "tredecillón", + "cuatrodecillón", + "quindeciollón", + "sexdecillón", + "septendecillón", + "octodecillón", + "novendecillón", + "vigintillón", +]; +pub struct Spanish {} +impl Spanish { + fn en_miles(&self, mut num: i32) -> Vec { + let mut thousands = Vec::new(); + let mil = 1000; + + while num != 0 { + // Insertar en big-endian + thousands.push((num % mil) as u64); + num /= mil; // DivAssign + } + thousands + } + + pub fn to_cardinal(&self, num: i32) -> Result { + match num { + 0 => return Ok(String::from("cero")), + _ => (), + } + + let mut words = vec![]; + for (i, triplet) in self.en_miles(num).iter().enumerate().rev() { + let hundreds = (triplet / 100 % 10) as usize; + let tens = (triplet / 10 % 10) as usize; + let units = (triplet % 10) as usize; + + if hundreds > 0 { + match triplet { + // Edge case when triplet is a hundred + 100 => words.push(String::from("cien")), + _ => words.push(String::from(CENTENAS[hundreds])), + } + } + + if tens != 0 || units != 0 { + // for edge case when unit value is 1 and is not the last triplet + let unit_word = if units == 1 && i != 0 { + "un" + } else { + UNIDADES[units] + }; + match tens { + // case ?_102 => ? ciento dos + 0 => words.push(String::from(unit_word)), + // case `?_119` => `? ciento diecinueve` + // case `?_110` => `? ciento diez` + 1 => words.push(String::from(DIECIS[units])), + _ => { + // case 142 => CENTENAS[x] forty-two + let ten = DECENAS[tens]; + words.push(match units { + 0 => String::from(ten), + _ => format!("{ten} y {unit_word}"), + }); + } + } + } + + if i != 0 && triplet != &0 { + if i > (MILLARES.len() - 1) { + return Err(format!( + "Número demasiado grande: {} - Maximo: {}", + num, + i32::MAX + )); + } + // Boolean that checks if next MEGA/MILES is plural + let plural = !(hundreds == 0 && tens == 0 && units == 1); + match plural { + false => words.push(String::from(MILLAR[i])), + true => words.push(String::from(MILLARES[i])), + } + } + } + Ok(words.join(" ")) + } +} + +pub fn main() { + let es = Spanish {}; + println!("Resultado {:?}", es.to_cardinal(dbg!(1_002_002_031))); + println!("Resultado {:?}", es.to_cardinal(dbg!(1_012_002_031))); + println!("Resultado {:?}", es.to_cardinal(dbg!(1_011_002_031))); +} diff --git a/src/lang/mod.rs b/src/lang/mod.rs index 7eefd8e..9b9e4f3 100644 --- a/src/lang/mod.rs +++ b/src/lang/mod.rs @@ -1,5 +1,6 @@ mod lang; mod en; +mod es; mod fr; mod uk; From 0cbbd3290e35a871fabc6a7bb8208afa9198e2b9 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Thu, 28 Mar 2024 17:39:39 +0000 Subject: [PATCH 02/52] Staging changes on branch for dev-ing --- .devcontainer/Dockerfile | 9 +++++++ .devcontainer/devcontainer.json | 33 +++++++++++++++++++++++ .devcontainer/script.sh | 38 ++++++++++++++++++++++++++ Cargo.toml | 4 +++ rustfmt.toml | 47 +++++++++++++++++++++++++++++++++ src/main.rs | 3 +++ 6 files changed, 134 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/script.sh create mode 100644 rustfmt.toml create mode 100644 src/main.rs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..8180d95 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:22.04 + +WORKDIR /home/ + +COPY . . + +RUN bash ./script.sh + +ENV PATH="/root/.cargo/bin:$PATH" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..d0fb2ef --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,33 @@ +{ + "name": "Codespaces Rust Starter", + "customizations": { + "vscode": { + "extensions": [ + "cschleiden.vscode-github-actions", + "ms-vsliveshare.vsliveshare", + "matklad.rust-analyzer", + "serayuzgur.crates", + "vadimcn.vscode-lldb", + + "GitHub.copilot", + "rust-lang.rust-analyzer", + "serayuzgur.crates", + "zhuangtongfa.material-theme", + "usernamehw.errorlens", + "tamasfe.even-better-toml", + "formulahendry.code-runner" + ], + "settings": { + "workbench.colorTheme": "One Dark Pro Mix", + "editor.formatOnSave": true, + "editor.inlayHints.enabled": "offUnlessPressed", + "terminal.integrated.shell.linux": "/usr/bin/zsh", + "files.exclude": { + "**/CODE_OF_CONDUCT.md": true, + "**/LICENSE": true + } + } + } + }, + "dockerFile": "Dockerfile" +} diff --git a/.devcontainer/script.sh b/.devcontainer/script.sh new file mode 100644 index 0000000..52bdb62 --- /dev/null +++ b/.devcontainer/script.sh @@ -0,0 +1,38 @@ +## update and install some things we should probably have +apt-get update +apt-get install -y \ + curl \ + git \ + gnupg2 \ + jq \ + sudo \ + zsh \ + vim \ + build-essential \ + openssl + +## update and install 2nd level of packages +apt-get install -y pkg-config + +## Install rustup and common components +curl https://sh.rustup.rs -sSf | sh -s -- -y + +export PATH="/root/.cargo/bin/":$PATH + +rustup toolchain install nightly +# rustup component add rustfmt +# rustup component add rustfmt --toolchain nightly +# rustup component add clippy +# rustup component add clippy --toolchain nightly + +# Download cargo-binstall to ~/.cargo/bin directory +curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + +cargo binstall cargo-expand cargo-edit cargo-watch -y + +## setup and install oh-my-zsh +sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" +cp -R /root/.oh-my-zsh /home/$USERNAME +cp /root/.zshrc /home/$USERNAME +sed -i -e "s/\/root\/.oh-my-zsh/\/home\/$USERNAME\/.oh-my-zsh/g" /home/$USERNAME/.zshrc +chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc diff --git a/Cargo.toml b/Cargo.toml index 8e97e78..a7a18b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,5 +20,9 @@ path = "src/lib.rs" name = "num2words" path = "src/bin/bin.rs" +[[bin]] +name = "test_es_num" +path = "src/main.rs" + [dependencies] num-bigfloat = { version = "^1.7.1", default-features = false } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..06f6800 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,47 @@ +# Update to nightly for nightly gated rustfmt fields +# Command: "rustup toolchain install nightly" + +# Add to setting.json of your profile in VSCode +# "rust-analyzer.rustfmt.extraArgs": [ +# "+nightly" +# ], +######################################## + +# I can't rely on contributors using .editorconfig +newline_style = "Unix" +# require the shorthand instead of it being optional +use_field_init_shorthand = true +# outdated default — `?` was unstable at the time +# additionally the `try!` macro is deprecated now +use_try_shorthand = false +# Max to use the 100 char width for everything or Default. See https://rust-lang.github.io/rustfmt/?version=v1.4.38&search=#use_small_heuristics +use_small_heuristics = "Max" +# Unstable features below +unstable_features = true +version = "Two" +## code can be 100 characters, why not comments? +comment_width = 140 +# force contributors to follow the formatting requirement +error_on_line_overflow = true +# error_on_unformatted = true ## Error if unable to get comments or string literals within max_width, or they are left with trailing whitespaces. +# next 4: why not? +format_code_in_doc_comments = true +format_macro_bodies = true ## Format the bodies of macros. +format_macro_matchers = true ## Format the metavariable matching patterns in macros. +## Wraps string when it overflows max_width +format_strings = true +# better grepping +imports_granularity = "Module" +# quicker manual lookup +group_imports = "StdExternalCrate" +# why use an attribute if a normal doc comment would suffice? +normalize_doc_attributes = true +# why not? +wrap_comments = true + +merge_derives = false ## I might need multi-line derives +overflow_delimited_expr = false +## When structs, slices, arrays, and block/array-like macros are used as the last argument in an +## expression list, allow them to overflow (like blocks/closures) instead of being indented on a new line. +reorder_impl_items = true +## Reorder impl items. type and const are put first, then macros and methods. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..75e024c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main(){ + +} \ No newline at end of file From 04bf0ecbfe4151c317b42ffe02600e41d179bb1d Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 29 Mar 2024 06:44:00 +0000 Subject: [PATCH 03/52] Implement Tests for to_cardinal method --- Cargo.toml | 2 +- src/lang/es.rs | 267 ++++++++++++++++++++++++++++++++++++++--------- src/lang/lang.rs | 40 ++++--- src/lang/mod.rs | 2 + src/lib.rs | 2 +- src/main.rs | 77 +++++++++++++- 6 files changed, 311 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a7a18b3..e9fe240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ name = "num2words" path = "src/bin/bin.rs" [[bin]] -name = "test_es_num" +name = "test_es" path = "src/main.rs" [dependencies] diff --git a/src/lang/es.rs b/src/lang/es.rs index 4d9f132..beea483 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -1,7 +1,8 @@ +use core::fmt::{self, Formatter}; +use std::{convert::TryInto, fmt::Display}; // Reference that can hopefully be implemented seamlessly: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol -const UNIDADES: [&str; 10] = [ - "", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve", -]; +const UNIDADES: [&str; 10] = + ["", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]; // Decenas que son entre 11 y 19 const DIECIS: [&str; 10] = [ "diez", // Needed for cases like 10, 10_000 and 10_000_000 @@ -18,7 +19,7 @@ const DIECIS: [&str; 10] = [ // Saltos en decenas const DECENAS: [&str; 10] = [ "", - "diez", + "", // This actually never gets called, but if so, it probably should be "diez" "veinte", "treinta", "cuarenta", @@ -43,11 +44,11 @@ const CENTENAS: [&str; 10] = [ "novecientos", ]; // To ensure both arrays doesn't desync -const MILLAR_SIZE: usize = 21; +const MILLAR_SIZE: usize = 22; /// from source: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol /// Based on https://en.wikipedia.org/wiki/Names_of_large_numbers, each thousands is from the Short Scales, -/// which each thousands can be defined as 10^(3n+3) magnitude, where n is replaced by the index of the Array. For example -/// 10^3 = Thousands (starts at n=1 here) +/// which each thousands can be defined as 10^(3n+3) magnitude, where n is replaced by the index of +/// the Array. For example 10^3 = Thousands (starts at n=1 here) /// 10^6 = Millions /// 10^9 = Billions /// 10^33 = Decillion @@ -61,6 +62,7 @@ const MILLARES: [&str; MILLAR_SIZE] = [ "cuatrillones", "quintillones", "sextillones", + "septillones", "octillones", "nonillones", "decillones", @@ -85,6 +87,7 @@ const MILLAR: [&str; MILLAR_SIZE] = [ "cuatrillón", "quintillón", "sextillón", + "septillón", "octillón", "nonillón", "decillón", @@ -99,30 +102,56 @@ const MILLAR: [&str; MILLAR_SIZE] = [ "novendecillón", "vigintillón", ]; -pub struct Spanish {} +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Spanish { + neg_flavour: NegativeFlavour, +} +#[allow(dead_code)] +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub enum NegativeFlavour { + #[default] + Prepended, // -1 => menos uno + Appended, // -1 => uno negativo + BelowZero, // -1 => uno bajo cero +} + +impl Display for NegativeFlavour { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + NegativeFlavour::Prepended => write!(f, "menos"), + NegativeFlavour::Appended => write!(f, "negativo"), + NegativeFlavour::BelowZero => write!(f, "bajo cero"), + } + } +} + impl Spanish { - fn en_miles(&self, mut num: i32) -> Vec { + pub fn set_neg_flavour(&mut self, flavour: NegativeFlavour) { + self.neg_flavour = flavour; + } + + fn en_miles(&self, mut num: i128) -> Vec { let mut thousands = Vec::new(); let mil = 1000; - + num = num.abs(); while num != 0 { - // Insertar en big-endian - thousands.push((num % mil) as u64); + // Insertar en Low Endian + thousands.push((num % mil).try_into().expect("triplet not under 1000")); num /= mil; // DivAssign } thousands } - pub fn to_cardinal(&self, num: i32) -> Result { - match num { - 0 => return Ok(String::from("cero")), - _ => (), + pub fn to_cardinal(&self, num: i128) -> Result { + // for 0 case + if num == 0 { + return Ok(String::from("cero")); } let mut words = vec![]; for (i, triplet) in self.en_miles(num).iter().enumerate().rev() { - let hundreds = (triplet / 100 % 10) as usize; - let tens = (triplet / 10 % 10) as usize; + let hundreds = ((triplet / 100) % 10) as usize; + let tens = ((triplet / 10) % 10) as usize; let units = (triplet % 10) as usize; if hundreds > 0 { @@ -132,54 +161,188 @@ impl Spanish { _ => words.push(String::from(CENTENAS[hundreds])), } } + 'decenas: { + if tens != 0 || units != 0 { + let unit_word = match (units, i) { + // case `1_100` => `mil cien` instead of `un mil un cien` + // case `1_001_000` => `un millón mil` instead of `un millón un mil` + (_, 1) if triplet == &1 => break 'decenas, + /* + // TODO: uncomment this Match Arm if it's more correct to say "un millón mil" for 1_001_000 + (1, 1) => { + // Early break to avoid "un millón un mil" which personally sounds unnatural + break 'decenas; + }, */ + // case `001_001_100...` => `un billón un millón cien mil...` instead of `uno billón uno millón cien mil...` + (_, index) if index != 0 && triplet == &1 => "un", + _ => UNIDADES[units], + }; - if tens != 0 || units != 0 { - // for edge case when unit value is 1 and is not the last triplet - let unit_word = if units == 1 && i != 0 { - "un" - } else { - UNIDADES[units] - }; - match tens { - // case ?_102 => ? ciento dos - 0 => words.push(String::from(unit_word)), - // case `?_119` => `? ciento diecinueve` - // case `?_110` => `? ciento diez` - 1 => words.push(String::from(DIECIS[units])), - _ => { - // case 142 => CENTENAS[x] forty-two - let ten = DECENAS[tens]; - words.push(match units { - 0 => String::from(ten), - _ => format!("{ten} y {unit_word}"), - }); + match tens { + // case `?_102` => `? ciento dos` + 0 => words.push(String::from(unit_word)), + // case `?_119` => `? ciento diecinueve` + // case `?_110` => `? ciento diez` + 1 => words.push(String::from(DIECIS[units])), + _ => { + // case `?_142 => `? cuarenta y dos` + let ten = DECENAS[tens]; + words.push(match units { + 0 => String::from(ten), + _ => format!("{ten} y {unit_word}"), + }); + } } } } - + // Add the next Milliard if there's any. if i != 0 && triplet != &0 { - if i > (MILLARES.len() - 1) { - return Err(format!( - "Número demasiado grande: {} - Maximo: {}", - num, - i32::MAX - )); + if i > MILLARES.len() - 1 { + return Err(format!("Número demasiado grande: {} - Maximo: {}", num, i32::MAX)); } - // Boolean that checks if next MEGA/MILES is plural - let plural = !(hundreds == 0 && tens == 0 && units == 1); + // Boolean that checks if next Milliard is plural + let plural = *triplet != 1; match plural { false => words.push(String::from(MILLAR[i])), true => words.push(String::from(MILLARES[i])), } } } + // flavour the text when negative + if let (flavour, true) = (&self.neg_flavour, num < 0) { + use NegativeFlavour::*; + let string = flavour.to_string(); + match flavour { + Prepended => words.insert(0, string), + Appended => words.push(string), + BelowZero => words.push(string), + } + } + Ok(words.join(" ")) } } -pub fn main() { - let es = Spanish {}; - println!("Resultado {:?}", es.to_cardinal(dbg!(1_002_002_031))); - println!("Resultado {:?}", es.to_cardinal(dbg!(1_012_002_031))); - println!("Resultado {:?}", es.to_cardinal(dbg!(1_011_002_031))); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lang_es_sub_thousands() { + let es = Spanish::default(); + assert_eq!(es.to_cardinal(000).unwrap(), "cero"); + assert_eq!(es.to_cardinal(010).unwrap(), "diez"); + assert_eq!(es.to_cardinal(100).unwrap(), "cien"); + assert_eq!(es.to_cardinal(101).unwrap(), "ciento uno"); + assert_eq!(es.to_cardinal(110).unwrap(), "ciento diez"); + assert_eq!(es.to_cardinal(111).unwrap(), "ciento once"); + assert_eq!(es.to_cardinal(141).unwrap(), "ciento cuarenta y uno"); + assert_eq!(es.to_cardinal(142).unwrap(), "ciento cuarenta y dos"); + assert_eq!(es.to_cardinal(800).unwrap(), "ochocientos"); + } + + #[test] + fn lang_es_thousands() { + let es = Spanish::default(); + // When thousands triplet is 1 + assert_eq!(es.to_cardinal(001_000).unwrap(), "mil"); + assert_eq!(es.to_cardinal(001_010).unwrap(), "mil diez"); + assert_eq!(es.to_cardinal(001_100).unwrap(), "mil cien"); + assert_eq!(es.to_cardinal(001_101).unwrap(), "mil ciento uno"); + assert_eq!(es.to_cardinal(001_110).unwrap(), "mil ciento diez"); + assert_eq!(es.to_cardinal(001_111).unwrap(), "mil ciento once"); + assert_eq!(es.to_cardinal(001_141).unwrap(), "mil ciento cuarenta y uno"); + // When thousands triplet isn't 1 + assert_eq!(es.to_cardinal(002_000).unwrap(), "dos mil"); + assert_eq!(es.to_cardinal(012_010).unwrap(), "doce mil diez"); + assert_eq!(es.to_cardinal(140_100).unwrap(), "ciento cuarenta mil cien"); + assert_eq!(es.to_cardinal(141_101).unwrap(), "ciento cuarenta y uno mil ciento uno"); + assert_eq!(es.to_cardinal(142_002).unwrap(), "ciento cuarenta y dos mil dos"); + assert_eq!(es.to_cardinal(142_000).unwrap(), "ciento cuarenta y dos mil"); + assert_eq!(es.to_cardinal(888_111).unwrap(), "ochocientos ochenta y ocho mil ciento once"); + assert_eq!(es.to_cardinal(800_000).unwrap(), "ochocientos mil"); + } + + #[test] + fn lang_es_millions() { + let es = Spanish::default(); + // When thousands triplet is 1 + assert_eq!(es.to_cardinal(001_001_000).unwrap(), "un millón mil"); + assert_eq!(es.to_cardinal(010_001_010).unwrap(), "diez millones mil diez"); + assert_eq!(es.to_cardinal(019_001_010).unwrap(), "diecinueve millones mil diez"); + assert_eq!(es.to_cardinal(801_001_001).unwrap(), "ochocientos uno millones mil uno"); + assert_eq!(es.to_cardinal(800_001_001).unwrap(), "ochocientos millones mil uno"); + // when thousands triplet isn't 1 + assert_eq!(es.to_cardinal(001_002_010).unwrap(), "un millón dos mil diez"); + assert_eq!(es.to_cardinal(010_002_010).unwrap(), "diez millones dos mil diez"); + assert_eq!(es.to_cardinal(019_102_010).unwrap(), "diecinueve millones ciento dos mil diez"); + assert_eq!(es.to_cardinal(800_100_001).unwrap(), "ochocientos millones cien mil uno"); + assert_eq!( + es.to_cardinal(801_021_001).unwrap(), + "ochocientos uno millones veinte y uno mil uno" + ); + assert_eq!(es.to_cardinal(001_000_000).unwrap(), "un millón"); + assert_eq!(es.to_cardinal(001_000_000_000).unwrap(), "un billón"); + assert_eq!(es.to_cardinal(001_001_100_001).unwrap(), "un billón un millón cien mil uno"); + } + + #[test] + fn lang_es_negative_prepended() { + let mut es = Spanish::default(); + // Make sure no enums were accidentally missed in tests if flavour ever changes + match es.neg_flavour { + NegativeFlavour::Prepended => (), + NegativeFlavour::Appended => (), + NegativeFlavour::BelowZero => (), + } + + use NegativeFlavour::*; + es.set_neg_flavour(Appended); + assert_eq!(es.to_cardinal(-1).unwrap(), "uno negativo"); + assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "un millón negativo"); + assert_eq!( + es.to_cardinal(-1_020_010_000).unwrap(), + "un billón veinte millones diez mil negativo" + ); + + es.set_neg_flavour(Prepended); + assert_eq!(es.to_cardinal(-1).unwrap(), "menos uno"); + assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "menos un millón"); + assert_eq!( + es.to_cardinal(-1_020_010_000).unwrap(), + "menos un billón veinte millones diez mil" + ); + + es.set_neg_flavour(BelowZero); + assert_eq!(es.to_cardinal(-1).unwrap(), "uno bajo cero"); + assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "un millón bajo cero"); + assert_eq!( + es.to_cardinal(-1_020_010_000).unwrap(), + "un billón veinte millones diez mil bajo cero" + ); + } + #[test] + fn lang_es_positive_is_just_a_substring_of_negative_in_cardinal() { + const VALUES: [i128; 3] = [-1, -1_000_000, -1_020_010_000]; + use NegativeFlavour::*; + let mut es = Spanish::default(); + for flavour in [Prepended, Appended, BelowZero] { + es.set_neg_flavour(flavour); + for value in VALUES.iter().cloned() { + let positive = es.to_cardinal(value.abs()).unwrap(); + let negative = es.to_cardinal(-value.abs()).unwrap(); + assert!( + negative.contains(positive.as_str()), + "{} !contains {}", + negative, + positive + ); + } + } + } + + #[test] + fn lang_es_() { + // unimplemented!() + } } diff --git a/src/lang/lang.rs b/src/lang/lang.rs index eca1e1f..57abb00 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -51,6 +51,8 @@ pub enum Lang { /// ); /// ``` French_CH, + // //TODO: add spanish parity + // Spanish, /// ``` /// use num2words::{Num2Words, Lang}; /// assert_eq!( @@ -88,10 +90,7 @@ impl FromStr for Lang { pub fn to_language(lang: Lang, preferences: Vec) -> Box { match lang { Lang::English => { - let last = preferences - .iter() - .rev() - .find(|v| ["oh", "nil"].contains(&v.as_str())); + let last = preferences.iter().rev().find(|v| ["oh", "nil"].contains(&v.as_str())); if let Some(v) = last { return Box::new(lang::English::new(v == "oh", v == "nil")); @@ -106,7 +105,9 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::FR)) @@ -118,7 +119,9 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::BE)) @@ -130,27 +133,20 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::CH)) } Lang::Ukrainian => { - let declension: lang::uk::Declension = preferences - .iter() - .rev() - .find_map(|d| d.parse().ok()) - .unwrap_or_default(); - let gender: lang::uk::Gender = preferences - .iter() - .rev() - .find_map(|d| d.parse().ok()) - .unwrap_or_default(); - let number: lang::uk::GrammaticalNumber = preferences - .iter() - .rev() - .find_map(|d| d.parse().ok()) - .unwrap_or_default(); + let declension: lang::uk::Declension = + preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); + let gender: lang::uk::Gender = + preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); + let number: lang::uk::GrammaticalNumber = + preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); Box::new(lang::Ukrainian::new(gender, number, declension)) } } diff --git a/src/lang/mod.rs b/src/lang/mod.rs index 9b9e4f3..4ddd6a3 100644 --- a/src/lang/mod.rs +++ b/src/lang/mod.rs @@ -1,3 +1,4 @@ +#[rustfmt::skip] // TODO: Remove attribute before final merge mod lang; mod en; mod es; @@ -5,6 +6,7 @@ mod fr; mod uk; pub use en::English; +pub use es::Spanish; pub use fr::French; pub use uk::Ukrainian; diff --git a/src/lib.rs b/src/lib.rs index c418511..8f6ec14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,7 +114,7 @@ mod num2words; mod currency; -mod lang; +pub mod lang; // TODO: remove pub visibility before merging mod output; pub use crate::num2words::{Num2Err, Num2Words}; diff --git a/src/main.rs b/src/main.rs index 75e024c..bcc2d73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,74 @@ -fn main(){ - -} \ No newline at end of file +use num2words::lang::Spanish; +use std::io::Write; +pub fn main() { + let es = Spanish::default(); + println!("Resultado {:?}", es.to_cardinal(dbg!(1_002_002_031))); + println!("Resultado {:?}", es.to_cardinal(dbg!(-1_010_001_031))); + println!("Resultado {:?}", es.to_cardinal(dbg!(1_001_021_031))); + + let mut input = String::new(); + print!("\nIngrese un número para convertir a palabras\nIngrese `exit` para salir:\n\n"); + fn read_line(input: &mut String) { + input.clear(); + std::io::stdin().read_line(input).unwrap(); + } + + loop { + print!("Ingrese su número: "); + flush(); + read_line(&mut input); + let input = input.trim(); + match input { + "exit" => { + clear_terminal(); + println!("Saliendo..."); + break; + } + "clear" => { + clear_terminal(); + continue; + } + _ => {} + } + if input.is_empty() { + println!("Número inválido {input:?} no puede estar vacío"); + continue; + } + let num = match input.parse::() { + Ok(num) => num, + Err(_) => { + println!("Número inválido {input:?} - no es convertible a un número entero"); + continue; + } + }; + print!("Entrada:"); + pretty_print_int(num); + println!(" => {:?}", es.to_cardinal(num).unwrap()); + } +} +pub fn clear_terminal() { + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); +} +pub fn back_space(amount: usize) { + for _i in 0..amount { + print!("{}", 8u8 as char); + } + flush(); +} +pub fn flush() { + std::io::stdout().flush().unwrap(); +} +pub fn pretty_print_int>(num: T) { + let mut num: i128 = num.into(); + let mut vec = vec![]; + while num > 0 { + vec.push((num % 1000) as i16); + num /= 1000; + } + vec.reverse(); + let prettied = + vec.into_iter().map(|num| format!("{num:03}")).collect::>().join(","); + + print!("{:?}", prettied.trim_start_matches('0')); + flush(); +} From 4a61d9da76717f783e0e1485548547d7297ed523 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 29 Mar 2024 06:44:00 +0000 Subject: [PATCH 04/52] Implement Tests for to_cardinal method --- Cargo.toml | 2 +- src/lang/es.rs | 269 ++++++++++++++++++++++++++++++++++++++--------- src/lang/lang.rs | 40 ++++--- src/lang/mod.rs | 2 + src/lib.rs | 2 +- src/main.rs | 77 +++++++++++++- 6 files changed, 313 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a7a18b3..e9fe240 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ name = "num2words" path = "src/bin/bin.rs" [[bin]] -name = "test_es_num" +name = "test_es" path = "src/main.rs" [dependencies] diff --git a/src/lang/es.rs b/src/lang/es.rs index 4d9f132..fbfeae0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -1,7 +1,9 @@ +use core::fmt::{self, Formatter}; +use std::convert::TryInto; +use std::fmt::Display; // Reference that can hopefully be implemented seamlessly: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol -const UNIDADES: [&str; 10] = [ - "", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve", -]; +const UNIDADES: [&str; 10] = + ["", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]; // Decenas que son entre 11 y 19 const DIECIS: [&str; 10] = [ "diez", // Needed for cases like 10, 10_000 and 10_000_000 @@ -18,7 +20,7 @@ const DIECIS: [&str; 10] = [ // Saltos en decenas const DECENAS: [&str; 10] = [ "", - "diez", + "", // This actually never gets called, but if so, it probably should be "diez" "veinte", "treinta", "cuarenta", @@ -43,11 +45,11 @@ const CENTENAS: [&str; 10] = [ "novecientos", ]; // To ensure both arrays doesn't desync -const MILLAR_SIZE: usize = 21; +const MILLAR_SIZE: usize = 22; /// from source: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol /// Based on https://en.wikipedia.org/wiki/Names_of_large_numbers, each thousands is from the Short Scales, -/// which each thousands can be defined as 10^(3n+3) magnitude, where n is replaced by the index of the Array. For example -/// 10^3 = Thousands (starts at n=1 here) +/// which each thousands can be defined as 10^(3n+3) magnitude, where n is replaced by the index of +/// the Array. For example 10^3 = Thousands (starts at n=1 here) /// 10^6 = Millions /// 10^9 = Billions /// 10^33 = Decillion @@ -61,6 +63,7 @@ const MILLARES: [&str; MILLAR_SIZE] = [ "cuatrillones", "quintillones", "sextillones", + "septillones", "octillones", "nonillones", "decillones", @@ -85,6 +88,7 @@ const MILLAR: [&str; MILLAR_SIZE] = [ "cuatrillón", "quintillón", "sextillón", + "septillón", "octillón", "nonillón", "decillón", @@ -99,30 +103,56 @@ const MILLAR: [&str; MILLAR_SIZE] = [ "novendecillón", "vigintillón", ]; -pub struct Spanish {} +#[derive(Clone, Default, Debug, PartialEq, Eq)] +pub struct Spanish { + neg_flavour: NegativeFlavour, +} +#[allow(dead_code)] +#[derive(Default, Clone, Debug, PartialEq, Eq)] +pub enum NegativeFlavour { + #[default] + Prepended, // -1 => menos uno + Appended, // -1 => uno negativo + BelowZero, // -1 => uno bajo cero +} + +impl Display for NegativeFlavour { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + NegativeFlavour::Prepended => write!(f, "menos"), + NegativeFlavour::Appended => write!(f, "negativo"), + NegativeFlavour::BelowZero => write!(f, "bajo cero"), + } + } +} + impl Spanish { - fn en_miles(&self, mut num: i32) -> Vec { + pub fn set_neg_flavour(&mut self, flavour: NegativeFlavour) { + self.neg_flavour = flavour; + } + + fn en_miles(&self, mut num: i128) -> Vec { let mut thousands = Vec::new(); let mil = 1000; - + num = num.abs(); while num != 0 { - // Insertar en big-endian - thousands.push((num % mil) as u64); + // Insertar en Low Endian + thousands.push((num % mil).try_into().expect("triplet not under 1000")); num /= mil; // DivAssign } thousands } - pub fn to_cardinal(&self, num: i32) -> Result { - match num { - 0 => return Ok(String::from("cero")), - _ => (), + pub fn to_cardinal(&self, num: i128) -> Result { + // for 0 case + if num == 0 { + return Ok(String::from("cero")); } let mut words = vec![]; for (i, triplet) in self.en_miles(num).iter().enumerate().rev() { - let hundreds = (triplet / 100 % 10) as usize; - let tens = (triplet / 10 % 10) as usize; + let hundreds = ((triplet / 100) % 10) as usize; + let tens = ((triplet / 10) % 10) as usize; let units = (triplet % 10) as usize; if hundreds > 0 { @@ -132,54 +162,189 @@ impl Spanish { _ => words.push(String::from(CENTENAS[hundreds])), } } + 'decenas: { + if tens != 0 || units != 0 { + let unit_word = match (units, i) { + // case `1_100` => `mil cien` instead of `un mil un cien` + // case `1_001_000` => `un millón mil` instead of `un millón un mil` + (_, 1) if triplet == &1 => break 'decenas, + /* + // TODO: uncomment this Match Arm if it's more correct to say "un millón mil" for 1_001_000 + (1, 1) => { + // Early break to avoid "un millón un mil" which personally sounds unnatural + break 'decenas; + }, */ + // case `001_001_100...` => `un billón un millón cien mil...` instead of + // `uno billón uno millón cien mil...` + (_, index) if index != 0 && triplet == &1 => "un", + _ => UNIDADES[units], + }; - if tens != 0 || units != 0 { - // for edge case when unit value is 1 and is not the last triplet - let unit_word = if units == 1 && i != 0 { - "un" - } else { - UNIDADES[units] - }; - match tens { - // case ?_102 => ? ciento dos - 0 => words.push(String::from(unit_word)), - // case `?_119` => `? ciento diecinueve` - // case `?_110` => `? ciento diez` - 1 => words.push(String::from(DIECIS[units])), - _ => { - // case 142 => CENTENAS[x] forty-two - let ten = DECENAS[tens]; - words.push(match units { - 0 => String::from(ten), - _ => format!("{ten} y {unit_word}"), - }); + match tens { + // case `?_102` => `? ciento dos` + 0 => words.push(String::from(unit_word)), + // case `?_119` => `? ciento diecinueve` + // case `?_110` => `? ciento diez` + 1 => words.push(String::from(DIECIS[units])), + _ => { + // case `?_142 => `? cuarenta y dos` + let ten = DECENAS[tens]; + words.push(match units { + 0 => String::from(ten), + _ => format!("{ten} y {unit_word}"), + }); + } } } } - + // Add the next Milliard if there's any. if i != 0 && triplet != &0 { - if i > (MILLARES.len() - 1) { - return Err(format!( - "Número demasiado grande: {} - Maximo: {}", - num, - i32::MAX - )); + if i > MILLARES.len() - 1 { + return Err(format!("Número demasiado grande: {} - Maximo: {}", num, i32::MAX)); } - // Boolean that checks if next MEGA/MILES is plural - let plural = !(hundreds == 0 && tens == 0 && units == 1); + // Boolean that checks if next Milliard is plural + let plural = *triplet != 1; match plural { false => words.push(String::from(MILLAR[i])), true => words.push(String::from(MILLARES[i])), } } } + // flavour the text when negative + if let (flavour, true) = (&self.neg_flavour, num < 0) { + use NegativeFlavour::*; + let string = flavour.to_string(); + match flavour { + Prepended => words.insert(0, string), + Appended => words.push(string), + BelowZero => words.push(string), + } + } + Ok(words.join(" ")) } } -pub fn main() { - let es = Spanish {}; - println!("Resultado {:?}", es.to_cardinal(dbg!(1_002_002_031))); - println!("Resultado {:?}", es.to_cardinal(dbg!(1_012_002_031))); - println!("Resultado {:?}", es.to_cardinal(dbg!(1_011_002_031))); +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lang_es_sub_thousands() { + let es = Spanish::default(); + assert_eq!(es.to_cardinal(000).unwrap(), "cero"); + assert_eq!(es.to_cardinal(10).unwrap(), "diez"); + assert_eq!(es.to_cardinal(100).unwrap(), "cien"); + assert_eq!(es.to_cardinal(101).unwrap(), "ciento uno"); + assert_eq!(es.to_cardinal(110).unwrap(), "ciento diez"); + assert_eq!(es.to_cardinal(111).unwrap(), "ciento once"); + assert_eq!(es.to_cardinal(141).unwrap(), "ciento cuarenta y uno"); + assert_eq!(es.to_cardinal(142).unwrap(), "ciento cuarenta y dos"); + assert_eq!(es.to_cardinal(800).unwrap(), "ochocientos"); + } + + #[test] + fn lang_es_thousands() { + let es = Spanish::default(); + // When thousands triplet is 1 + assert_eq!(es.to_cardinal(1_000).unwrap(), "mil"); + assert_eq!(es.to_cardinal(1_010).unwrap(), "mil diez"); + assert_eq!(es.to_cardinal(1_100).unwrap(), "mil cien"); + assert_eq!(es.to_cardinal(1_101).unwrap(), "mil ciento uno"); + assert_eq!(es.to_cardinal(1_110).unwrap(), "mil ciento diez"); + assert_eq!(es.to_cardinal(1_111).unwrap(), "mil ciento once"); + assert_eq!(es.to_cardinal(1_141).unwrap(), "mil ciento cuarenta y uno"); + // When thousands triplet isn't 1 + assert_eq!(es.to_cardinal(2_000).unwrap(), "dos mil"); + assert_eq!(es.to_cardinal(12_010).unwrap(), "doce mil diez"); + assert_eq!(es.to_cardinal(140_100).unwrap(), "ciento cuarenta mil cien"); + assert_eq!(es.to_cardinal(141_101).unwrap(), "ciento cuarenta y uno mil ciento uno"); + assert_eq!(es.to_cardinal(142_002).unwrap(), "ciento cuarenta y dos mil dos"); + assert_eq!(es.to_cardinal(142_000).unwrap(), "ciento cuarenta y dos mil"); + assert_eq!(es.to_cardinal(888_111).unwrap(), "ochocientos ochenta y ocho mil ciento once"); + assert_eq!(es.to_cardinal(800_000).unwrap(), "ochocientos mil"); + } + + #[test] + fn lang_es_millions() { + let es = Spanish::default(); + // When thousands triplet is 1 + assert_eq!(es.to_cardinal(1_001_000).unwrap(), "un millón mil"); + assert_eq!(es.to_cardinal(10_001_010).unwrap(), "diez millones mil diez"); + assert_eq!(es.to_cardinal(19_001_010).unwrap(), "diecinueve millones mil diez"); + assert_eq!(es.to_cardinal(801_001_001).unwrap(), "ochocientos uno millones mil uno"); + assert_eq!(es.to_cardinal(800_001_001).unwrap(), "ochocientos millones mil uno"); + // when thousands triplet isn't 1 + assert_eq!(es.to_cardinal(1_002_010).unwrap(), "un millón dos mil diez"); + assert_eq!(es.to_cardinal(10_002_010).unwrap(), "diez millones dos mil diez"); + assert_eq!(es.to_cardinal(19_102_010).unwrap(), "diecinueve millones ciento dos mil diez"); + assert_eq!(es.to_cardinal(800_100_001).unwrap(), "ochocientos millones cien mil uno"); + assert_eq!( + es.to_cardinal(801_021_001).unwrap(), + "ochocientos uno millones veinte y uno mil uno" + ); + assert_eq!(es.to_cardinal(1_000_000).unwrap(), "un millón"); + assert_eq!(es.to_cardinal(1_000_000_000).unwrap(), "un billón"); + assert_eq!(es.to_cardinal(1_001_100_001).unwrap(), "un billón un millón cien mil uno"); + } + + #[test] + fn lang_es_negative_prepended() { + let mut es = Spanish::default(); + // Make sure no enums were accidentally missed in tests if flavour ever changes + match es.neg_flavour { + NegativeFlavour::Prepended => (), + NegativeFlavour::Appended => (), + NegativeFlavour::BelowZero => (), + } + + use NegativeFlavour::*; + es.set_neg_flavour(Appended); + assert_eq!(es.to_cardinal(-1).unwrap(), "uno negativo"); + assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "un millón negativo"); + assert_eq!( + es.to_cardinal(-1_020_010_000).unwrap(), + "un billón veinte millones diez mil negativo" + ); + + es.set_neg_flavour(Prepended); + assert_eq!(es.to_cardinal(-1).unwrap(), "menos uno"); + assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "menos un millón"); + assert_eq!( + es.to_cardinal(-1_020_010_000).unwrap(), + "menos un billón veinte millones diez mil" + ); + + es.set_neg_flavour(BelowZero); + assert_eq!(es.to_cardinal(-1).unwrap(), "uno bajo cero"); + assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "un millón bajo cero"); + assert_eq!( + es.to_cardinal(-1_020_010_000).unwrap(), + "un billón veinte millones diez mil bajo cero" + ); + } + #[test] + fn lang_es_positive_is_just_a_substring_of_negative_in_cardinal() { + const VALUES: [i128; 3] = [-1, -1_000_000, -1_020_010_000]; + use NegativeFlavour::*; + let mut es = Spanish::default(); + for flavour in [Prepended, Appended, BelowZero] { + es.set_neg_flavour(flavour); + for value in VALUES.iter().cloned() { + let positive = es.to_cardinal(value.abs()).unwrap(); + let negative = es.to_cardinal(-value.abs()).unwrap(); + assert!( + negative.contains(positive.as_str()), + "{} !contains {}", + negative, + positive + ); + } + } + } + + #[test] + fn lang_es_() { + // unimplemented!() + } } diff --git a/src/lang/lang.rs b/src/lang/lang.rs index eca1e1f..57abb00 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -51,6 +51,8 @@ pub enum Lang { /// ); /// ``` French_CH, + // //TODO: add spanish parity + // Spanish, /// ``` /// use num2words::{Num2Words, Lang}; /// assert_eq!( @@ -88,10 +90,7 @@ impl FromStr for Lang { pub fn to_language(lang: Lang, preferences: Vec) -> Box { match lang { Lang::English => { - let last = preferences - .iter() - .rev() - .find(|v| ["oh", "nil"].contains(&v.as_str())); + let last = preferences.iter().rev().find(|v| ["oh", "nil"].contains(&v.as_str())); if let Some(v) = last { return Box::new(lang::English::new(v == "oh", v == "nil")); @@ -106,7 +105,9 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::FR)) @@ -118,7 +119,9 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::BE)) @@ -130,27 +133,20 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::CH)) } Lang::Ukrainian => { - let declension: lang::uk::Declension = preferences - .iter() - .rev() - .find_map(|d| d.parse().ok()) - .unwrap_or_default(); - let gender: lang::uk::Gender = preferences - .iter() - .rev() - .find_map(|d| d.parse().ok()) - .unwrap_or_default(); - let number: lang::uk::GrammaticalNumber = preferences - .iter() - .rev() - .find_map(|d| d.parse().ok()) - .unwrap_or_default(); + let declension: lang::uk::Declension = + preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); + let gender: lang::uk::Gender = + preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); + let number: lang::uk::GrammaticalNumber = + preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); Box::new(lang::Ukrainian::new(gender, number, declension)) } } diff --git a/src/lang/mod.rs b/src/lang/mod.rs index 9b9e4f3..4ddd6a3 100644 --- a/src/lang/mod.rs +++ b/src/lang/mod.rs @@ -1,3 +1,4 @@ +#[rustfmt::skip] // TODO: Remove attribute before final merge mod lang; mod en; mod es; @@ -5,6 +6,7 @@ mod fr; mod uk; pub use en::English; +pub use es::Spanish; pub use fr::French; pub use uk::Ukrainian; diff --git a/src/lib.rs b/src/lib.rs index c418511..8f6ec14 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,7 +114,7 @@ mod num2words; mod currency; -mod lang; +pub mod lang; // TODO: remove pub visibility before merging mod output; pub use crate::num2words::{Num2Err, Num2Words}; diff --git a/src/main.rs b/src/main.rs index 75e024c..bcc2d73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,74 @@ -fn main(){ - -} \ No newline at end of file +use num2words::lang::Spanish; +use std::io::Write; +pub fn main() { + let es = Spanish::default(); + println!("Resultado {:?}", es.to_cardinal(dbg!(1_002_002_031))); + println!("Resultado {:?}", es.to_cardinal(dbg!(-1_010_001_031))); + println!("Resultado {:?}", es.to_cardinal(dbg!(1_001_021_031))); + + let mut input = String::new(); + print!("\nIngrese un número para convertir a palabras\nIngrese `exit` para salir:\n\n"); + fn read_line(input: &mut String) { + input.clear(); + std::io::stdin().read_line(input).unwrap(); + } + + loop { + print!("Ingrese su número: "); + flush(); + read_line(&mut input); + let input = input.trim(); + match input { + "exit" => { + clear_terminal(); + println!("Saliendo..."); + break; + } + "clear" => { + clear_terminal(); + continue; + } + _ => {} + } + if input.is_empty() { + println!("Número inválido {input:?} no puede estar vacío"); + continue; + } + let num = match input.parse::() { + Ok(num) => num, + Err(_) => { + println!("Número inválido {input:?} - no es convertible a un número entero"); + continue; + } + }; + print!("Entrada:"); + pretty_print_int(num); + println!(" => {:?}", es.to_cardinal(num).unwrap()); + } +} +pub fn clear_terminal() { + print!("{esc}[2J{esc}[1;1H", esc = 27 as char); +} +pub fn back_space(amount: usize) { + for _i in 0..amount { + print!("{}", 8u8 as char); + } + flush(); +} +pub fn flush() { + std::io::stdout().flush().unwrap(); +} +pub fn pretty_print_int>(num: T) { + let mut num: i128 = num.into(); + let mut vec = vec![]; + while num > 0 { + vec.push((num % 1000) as i16); + num /= 1000; + } + vec.reverse(); + let prettied = + vec.into_iter().map(|num| format!("{num:03}")).collect::>().join(","); + + print!("{:?}", prettied.trim_start_matches('0')); + flush(); +} From 3e236dd911656bb9cbd50558b973e8ab5be9f0d5 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 29 Mar 2024 10:01:36 -0500 Subject: [PATCH 05/52] Fix weird merging --- src/lang/es.rs | 60 -------------------------------------------------- 1 file changed, 60 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 6f5b177..fbfeae0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -2,8 +2,6 @@ use core::fmt::{self, Formatter}; use std::convert::TryInto; use std::fmt::Display; // Reference that can hopefully be implemented seamlessly: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol -const UNIDADES: [&str; 10] = - ["", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]; const UNIDADES: [&str; 10] = ["", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]; // Decenas que son entre 11 y 19 @@ -23,7 +21,6 @@ const DIECIS: [&str; 10] = [ const DECENAS: [&str; 10] = [ "", "", // This actually never gets called, but if so, it probably should be "diez" - "", // This actually never gets called, but if so, it probably should be "diez" "veinte", "treinta", "cuarenta", @@ -49,13 +46,10 @@ const CENTENAS: [&str; 10] = [ ]; // To ensure both arrays doesn't desync const MILLAR_SIZE: usize = 22; -const MILLAR_SIZE: usize = 22; /// from source: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol /// Based on https://en.wikipedia.org/wiki/Names_of_large_numbers, each thousands is from the Short Scales, /// which each thousands can be defined as 10^(3n+3) magnitude, where n is replaced by the index of /// the Array. For example 10^3 = Thousands (starts at n=1 here) -/// which each thousands can be defined as 10^(3n+3) magnitude, where n is replaced by the index of -/// the Array. For example 10^3 = Thousands (starts at n=1 here) /// 10^6 = Millions /// 10^9 = Billions /// 10^33 = Decillion @@ -70,7 +64,6 @@ const MILLARES: [&str; MILLAR_SIZE] = [ "quintillones", "sextillones", "septillones", - "septillones", "octillones", "nonillones", "decillones", @@ -96,7 +89,6 @@ const MILLAR: [&str; MILLAR_SIZE] = [ "quintillón", "sextillón", "septillón", - "septillón", "octillón", "nonillón", "decillón", @@ -134,47 +126,16 @@ impl Display for NegativeFlavour { } } -#[derive(Clone, Default, Debug, PartialEq, Eq)] -pub struct Spanish { - neg_flavour: NegativeFlavour, -} -#[allow(dead_code)] -#[derive(Default, Clone, Debug, PartialEq, Eq)] -pub enum NegativeFlavour { - #[default] - Prepended, // -1 => menos uno - Appended, // -1 => uno negativo - BelowZero, // -1 => uno bajo cero -} - -impl Display for NegativeFlavour { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - NegativeFlavour::Prepended => write!(f, "menos"), - NegativeFlavour::Appended => write!(f, "negativo"), - NegativeFlavour::BelowZero => write!(f, "bajo cero"), - } - } -} - impl Spanish { pub fn set_neg_flavour(&mut self, flavour: NegativeFlavour) { self.neg_flavour = flavour; } - fn en_miles(&self, mut num: i128) -> Vec { - pub fn set_neg_flavour(&mut self, flavour: NegativeFlavour) { - self.neg_flavour = flavour; - } - fn en_miles(&self, mut num: i128) -> Vec { let mut thousands = Vec::new(); let mil = 1000; num = num.abs(); - num = num.abs(); while num != 0 { - // Insertar en Low Endian - thousands.push((num % mil).try_into().expect("triplet not under 1000")); // Insertar en Low Endian thousands.push((num % mil).try_into().expect("triplet not under 1000")); num /= mil; // DivAssign @@ -182,10 +143,6 @@ impl Spanish { thousands } - pub fn to_cardinal(&self, num: i128) -> Result { - // for 0 case - if num == 0 { - return Ok(String::from("cero")); pub fn to_cardinal(&self, num: i128) -> Result { // for 0 case if num == 0 { @@ -194,8 +151,6 @@ impl Spanish { let mut words = vec![]; for (i, triplet) in self.en_miles(num).iter().enumerate().rev() { - let hundreds = ((triplet / 100) % 10) as usize; - let tens = ((triplet / 10) % 10) as usize; let hundreds = ((triplet / 100) % 10) as usize; let tens = ((triplet / 10) % 10) as usize; let units = (triplet % 10) as usize; @@ -244,15 +199,11 @@ impl Spanish { } // Add the next Milliard if there's any. if i != 0 && triplet != &0 { - if i > MILLARES.len() - 1 { - return Err(format!("Número demasiado grande: {} - Maximo: {}", num, i32::MAX)); if i > MILLARES.len() - 1 { return Err(format!("Número demasiado grande: {} - Maximo: {}", num, i32::MAX)); } // Boolean that checks if next Milliard is plural let plural = *triplet != 1; - // Boolean that checks if next Milliard is plural - let plural = *triplet != 1; match plural { false => words.push(String::from(MILLAR[i])), true => words.push(String::from(MILLARES[i])), @@ -270,17 +221,6 @@ impl Spanish { } } - // flavour the text when negative - if let (flavour, true) = (&self.neg_flavour, num < 0) { - use NegativeFlavour::*; - let string = flavour.to_string(); - match flavour { - Prepended => words.insert(0, string), - Appended => words.push(string), - BelowZero => words.push(string), - } - } - Ok(words.join(" ")) } } From 02195d7f8988b3d6656a58c97498a4ebc583d6a0 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 29 Mar 2024 10:02:34 -0500 Subject: [PATCH 06/52] More unified test for cardinal conversion --- src/lang/es.rs | 97 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index fbfeae0..ca6e5a9 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -162,41 +162,39 @@ impl Spanish { _ => words.push(String::from(CENTENAS[hundreds])), } } - 'decenas: { - if tens != 0 || units != 0 { - let unit_word = match (units, i) { - // case `1_100` => `mil cien` instead of `un mil un cien` - // case `1_001_000` => `un millón mil` instead of `un millón un mil` - (_, 1) if triplet == &1 => break 'decenas, - /* - // TODO: uncomment this Match Arm if it's more correct to say "un millón mil" for 1_001_000 - (1, 1) => { - // Early break to avoid "un millón un mil" which personally sounds unnatural - break 'decenas; - }, */ - // case `001_001_100...` => `un billón un millón cien mil...` instead of - // `uno billón uno millón cien mil...` - (_, index) if index != 0 && triplet == &1 => "un", - _ => UNIDADES[units], - }; - match tens { - // case `?_102` => `? ciento dos` - 0 => words.push(String::from(unit_word)), - // case `?_119` => `? ciento diecinueve` - // case `?_110` => `? ciento diez` - 1 => words.push(String::from(DIECIS[units])), - _ => { - // case `?_142 => `? cuarenta y dos` - let ten = DECENAS[tens]; - words.push(match units { - 0 => String::from(ten), - _ => format!("{ten} y {unit_word}"), - }); - } + if tens != 0 || units != 0 { + let unit_word = match (units, i) { + // case `1_100` => `mil cien` instead of `un mil un cien` + // case `1_001_000` => `un millón mil` instead of `un millón un mil` + // Explanation: Second second triplet is always read as thousand, so we + // don't need to say "un mil" + (_, 1) if triplet == &1 => "", + // case `001_001_100...` => `un billón un millón cien mil...` instead of + // `uno billón uno millón cien mil...` + // All `triplets == 1`` can can be named as "un". except for first or second + // triplet + (_, index) if index != 0 && *triplet == 1 => "un", + _ => UNIDADES[units], + }; + + match tens { + // case `?_102` => `? ciento dos` + 0 => words.push(String::from(unit_word)), + // case `?_119` => `? ciento diecinueve` + // case `?_110` => `? ciento diez` + 1 => words.push(String::from(DIECIS[units])), + _ => { + // case `?_142 => `? cuarenta y dos` + let ten = DECENAS[tens]; + words.push(match units { + 0 => String::from(ten), + _ => format!("{ten} y {unit_word}"), + }); } } } + // Add the next Milliard if there's any. if i != 0 && triplet != &0 { if i > MILLARES.len() - 1 { @@ -221,7 +219,11 @@ impl Spanish { } } - Ok(words.join(" ")) + Ok(words + .into_iter() + .filter_map(|word| (!word.is_empty()).then_some(word)) + .collect::>() + .join(" ")) } } @@ -265,6 +267,37 @@ mod tests { assert_eq!(es.to_cardinal(800_000).unwrap(), "ochocientos mil"); } + #[test] + fn lang_es_test_by_concept_to_cardinal_method() { + // This might make other tests trivial + let es = Spanish::default(); + // Triplet == 1 inserts following milliard in singular + assert_eq!(es.to_cardinal(1_001_001_000).unwrap(), "un billón un millón mil"); + // Triplet != 1 inserts following milliard in plural + assert_eq!(es.to_cardinal(2_002_002_000).unwrap(), "dos billones dos millones dos mil"); + // Thousand's milliard is singular + assert_eq!(es.to_cardinal(1_100).unwrap(), "mil cien"); + // Thousand's milliard is plural + assert_eq!(es.to_cardinal(2_100).unwrap(), "dos mil cien"); + // Cardinal number ending in 1 always ends with "uno" + assert!(es.to_cardinal(12_313_213_556_451_233_521_251).unwrap().ends_with("uno")); + // triplet with value "10" + assert_eq!(es.to_cardinal(110_010_000).unwrap(), "ciento diez millones diez mil"); + // Triplets ending in 1 but higher than 30, is "uno" + // "un" is reserved for triplet == 1 in magnitudes higher than 10^3 like "un millón" + // or "un trillón" + assert_eq!( + es.to_cardinal(171_031_041_031).unwrap(), + "ciento setenta y uno billones treinta y uno millones cuarenta y uno mil treinta y uno" + ); + // Triplets ending in 1 but higher than 30, is never "un" + // consequently should never contain " un " as substring anywhere unless proven otherwise + assert_ne!( + es.to_cardinal(171_031_041_031).unwrap(), + "ciento setenta y un billones treinta y un millones cuarenta y un mil treinta y uno" + ); + assert!(!es.to_cardinal(171_031_041_031).unwrap().contains(" un ")); + } #[test] fn lang_es_millions() { let es = Spanish::default(); From a81b31050e009faea59fbc231089b19f600e5aef Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 29 Mar 2024 12:44:01 -0500 Subject: [PATCH 07/52] Add Veinti Flavor and setters for Spanish fields --- src/lang/es.rs | 46 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index ca6e5a9..ce324db 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -1,6 +1,8 @@ use core::fmt::{self, Formatter}; use std::convert::TryInto; use std::fmt::Display; + +use num_bigfloat::BigFloat; // Reference that can hopefully be implemented seamlessly: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol const UNIDADES: [&str; 10] = ["", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]; @@ -105,9 +107,12 @@ const MILLAR: [&str; MILLAR_SIZE] = [ ]; #[derive(Clone, Default, Debug, PartialEq, Eq)] pub struct Spanish { + /// Negative flavour like "bajo cero", "menos", "negativo" neg_flavour: NegativeFlavour, + // Writes the number as "veintiocho" instead of "veinte y ocho" in case of true + veinti: bool, } -#[allow(dead_code)] + #[derive(Default, Clone, Debug, PartialEq, Eq)] pub enum NegativeFlavour { #[default] @@ -127,8 +132,28 @@ impl Display for NegativeFlavour { } impl Spanish { - pub fn set_neg_flavour(&mut self, flavour: NegativeFlavour) { + pub fn new() -> Self { + Self::default() + } + + pub fn set_veinti(&mut self, veinti: bool) -> &mut Self { + self.veinti = veinti; + self + } + + pub fn set_neg_flavour(&mut self, flavour: NegativeFlavour) -> &mut Self { + self.neg_flavour = flavour; + self + } + + pub fn with_neg_flavour(mut self, flavour: NegativeFlavour) -> Self { self.neg_flavour = flavour; + self + } + + pub fn with_veinti(mut self, veinti: bool) -> Self { + self.veinti = veinti; + self } fn en_miles(&self, mut num: i128) -> Vec { @@ -184,6 +209,12 @@ impl Spanish { // case `?_119` => `? ciento diecinueve` // case `?_110` => `? ciento diez` 1 => words.push(String::from(DIECIS[units])), + 2 if self.veinti && units != 0 => match units { + // TODO:add accent if you can not support ASCII and want to be grammatically + 1 if i != 0 => words.push(String::from("veintiun")), + _ => words.push(String::from("veinti") + unit_word), + }, + // 2 if self.veinti && units == 1 => words.push(String::from("veintiun")), _ => { // case `?_142 => `? cuarenta y dos` let ten = DECENAS[tens]; @@ -294,9 +325,18 @@ mod tests { // consequently should never contain " un " as substring anywhere unless proven otherwise assert_ne!( es.to_cardinal(171_031_041_031).unwrap(), - "ciento setenta y un billones treinta y un millones cuarenta y un mil treinta y uno" + "ciento setenta y un billones treinta y un millones cuarenta y un mil treinta y uno", ); assert!(!es.to_cardinal(171_031_041_031).unwrap().contains(" un ")); + // with veinti flavour + let es = es.with_veinti(true); + + assert_eq!( + es.to_cardinal(21_021_321_021).unwrap(), + "veintiun billones veintiun millones trescientos veintiun mil veintiuno" + ); + assert_eq!(es.to_cardinal(22_000_000).unwrap(), "veintidos millones"); + assert_eq!(es.to_cardinal(20_020_020).unwrap(), "veinte millones veinte mil veinte"); } #[test] fn lang_es_millions() { From d29babecf5b38475388842b467386cbd96b3ba2c Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:12:15 -0500 Subject: [PATCH 08/52] Refactoro with BigFloat --- src/lang/es.rs | 156 +++++++++++++++++++++++++++---------------------- 1 file changed, 87 insertions(+), 69 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index ce324db..252077b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -1,8 +1,11 @@ +#![allow(unused_imports)] // TODO: Remove this attribute use core::fmt::{self, Formatter}; use std::convert::TryInto; use std::fmt::Display; use num_bigfloat::BigFloat; + +use crate::Num2Err; // Reference that can hopefully be implemented seamlessly: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol const UNIDADES: [&str; 10] = ["", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]; @@ -132,45 +135,51 @@ impl Display for NegativeFlavour { } impl Spanish { + #[inline(always)] pub fn new() -> Self { Self::default() } + #[inline(always)] pub fn set_veinti(&mut self, veinti: bool) -> &mut Self { self.veinti = veinti; self } + #[inline(always)] pub fn set_neg_flavour(&mut self, flavour: NegativeFlavour) -> &mut Self { self.neg_flavour = flavour; self } + #[inline(always)] pub fn with_neg_flavour(mut self, flavour: NegativeFlavour) -> Self { self.neg_flavour = flavour; self } + #[inline(always)] pub fn with_veinti(mut self, veinti: bool) -> Self { self.veinti = veinti; self } - fn en_miles(&self, mut num: i128) -> Vec { + #[inline(always)] + fn en_miles(&self, mut num: BigFloat) -> Vec { let mut thousands = Vec::new(); - let mil = 1000; + let mil = 1000.into(); num = num.abs(); - while num != 0 { + while !num.is_zero() { // Insertar en Low Endian - thousands.push((num % mil).try_into().expect("triplet not under 1000")); + thousands.push((num % mil).to_u64().expect("triplet not under 1000")); num /= mil; // DivAssign } thousands } - pub fn to_cardinal(&self, num: i128) -> Result { + pub fn to_cardinal(&self, num: BigFloat) -> Result { // for 0 case - if num == 0 { + if num.is_zero() { return Ok(String::from("cero")); } @@ -229,7 +238,7 @@ impl Spanish { // Add the next Milliard if there's any. if i != 0 && triplet != &0 { if i > MILLARES.len() - 1 { - return Err(format!("Número demasiado grande: {} - Maximo: {}", num, i32::MAX)); + return Err(Num2Err::CannotConvert); } // Boolean that checks if next Milliard is plural let plural = *triplet != 1; @@ -240,7 +249,7 @@ impl Spanish { } } // flavour the text when negative - if let (flavour, true) = (&self.neg_flavour, num < 0) { + if let (flavour, true) = (&self.neg_flavour, num.is_negative()) { use NegativeFlavour::*; let string = flavour.to_string(); match flavour { @@ -261,41 +270,47 @@ impl Spanish { #[cfg(test)] mod tests { use super::*; - + #[inline(always)] + fn to(input: i128) -> BigFloat { + BigFloat::from_i128(input) + } #[test] fn lang_es_sub_thousands() { let es = Spanish::default(); - assert_eq!(es.to_cardinal(000).unwrap(), "cero"); - assert_eq!(es.to_cardinal(10).unwrap(), "diez"); - assert_eq!(es.to_cardinal(100).unwrap(), "cien"); - assert_eq!(es.to_cardinal(101).unwrap(), "ciento uno"); - assert_eq!(es.to_cardinal(110).unwrap(), "ciento diez"); - assert_eq!(es.to_cardinal(111).unwrap(), "ciento once"); - assert_eq!(es.to_cardinal(141).unwrap(), "ciento cuarenta y uno"); - assert_eq!(es.to_cardinal(142).unwrap(), "ciento cuarenta y dos"); - assert_eq!(es.to_cardinal(800).unwrap(), "ochocientos"); + assert_eq!(es.to_cardinal(to(000)).unwrap(), "cero"); + assert_eq!(es.to_cardinal(to(10)).unwrap(), "diez"); + assert_eq!(es.to_cardinal(to(100)).unwrap(), "cien"); + assert_eq!(es.to_cardinal(to(101)).unwrap(), "ciento uno"); + assert_eq!(es.to_cardinal(to(110)).unwrap(), "ciento diez"); + assert_eq!(es.to_cardinal(to(111)).unwrap(), "ciento once"); + assert_eq!(es.to_cardinal(to(141)).unwrap(), "ciento cuarenta y uno"); + assert_eq!(es.to_cardinal(to(142)).unwrap(), "ciento cuarenta y dos"); + assert_eq!(es.to_cardinal(to(800)).unwrap(), "ochocientos"); } #[test] fn lang_es_thousands() { let es = Spanish::default(); // When thousands triplet is 1 - assert_eq!(es.to_cardinal(1_000).unwrap(), "mil"); - assert_eq!(es.to_cardinal(1_010).unwrap(), "mil diez"); - assert_eq!(es.to_cardinal(1_100).unwrap(), "mil cien"); - assert_eq!(es.to_cardinal(1_101).unwrap(), "mil ciento uno"); - assert_eq!(es.to_cardinal(1_110).unwrap(), "mil ciento diez"); - assert_eq!(es.to_cardinal(1_111).unwrap(), "mil ciento once"); - assert_eq!(es.to_cardinal(1_141).unwrap(), "mil ciento cuarenta y uno"); + assert_eq!(es.to_cardinal(to(1_000)).unwrap(), "mil"); + assert_eq!(es.to_cardinal(to(1_010)).unwrap(), "mil diez"); + assert_eq!(es.to_cardinal(to(1_100)).unwrap(), "mil cien"); + assert_eq!(es.to_cardinal(to(1_101)).unwrap(), "mil ciento uno"); + assert_eq!(es.to_cardinal(to(1_110)).unwrap(), "mil ciento diez"); + assert_eq!(es.to_cardinal(to(1_111)).unwrap(), "mil ciento once"); + assert_eq!(es.to_cardinal(to(1_141)).unwrap(), "mil ciento cuarenta y uno"); // When thousands triplet isn't 1 - assert_eq!(es.to_cardinal(2_000).unwrap(), "dos mil"); - assert_eq!(es.to_cardinal(12_010).unwrap(), "doce mil diez"); - assert_eq!(es.to_cardinal(140_100).unwrap(), "ciento cuarenta mil cien"); - assert_eq!(es.to_cardinal(141_101).unwrap(), "ciento cuarenta y uno mil ciento uno"); - assert_eq!(es.to_cardinal(142_002).unwrap(), "ciento cuarenta y dos mil dos"); - assert_eq!(es.to_cardinal(142_000).unwrap(), "ciento cuarenta y dos mil"); - assert_eq!(es.to_cardinal(888_111).unwrap(), "ochocientos ochenta y ocho mil ciento once"); - assert_eq!(es.to_cardinal(800_000).unwrap(), "ochocientos mil"); + assert_eq!(es.to_cardinal(to(2_000)).unwrap(), "dos mil"); + assert_eq!(es.to_cardinal(to(12_010)).unwrap(), "doce mil diez"); + assert_eq!(es.to_cardinal(to(140_100)).unwrap(), "ciento cuarenta mil cien"); + assert_eq!(es.to_cardinal(to(141_101)).unwrap(), "ciento cuarenta y uno mil ciento uno"); + assert_eq!(es.to_cardinal(to(142_002)).unwrap(), "ciento cuarenta y dos mil dos"); + assert_eq!(es.to_cardinal(to(142_000)).unwrap(), "ciento cuarenta y dos mil"); + assert_eq!( + es.to_cardinal(to(888_111)).unwrap(), + "ochocientos ochenta y ocho mil ciento once" + ); + assert_eq!(es.to_cardinal(to(800_000)).unwrap(), "ochocientos mil"); } #[test] @@ -303,62 +318,65 @@ mod tests { // This might make other tests trivial let es = Spanish::default(); // Triplet == 1 inserts following milliard in singular - assert_eq!(es.to_cardinal(1_001_001_000).unwrap(), "un billón un millón mil"); + assert_eq!(es.to_cardinal(to(1_001_001_000)).unwrap(), "un billón un millón mil"); // Triplet != 1 inserts following milliard in plural - assert_eq!(es.to_cardinal(2_002_002_000).unwrap(), "dos billones dos millones dos mil"); + assert_eq!(es.to_cardinal(to(2_002_002_000)).unwrap(), "dos billones dos millones dos mil"); // Thousand's milliard is singular - assert_eq!(es.to_cardinal(1_100).unwrap(), "mil cien"); + assert_eq!(es.to_cardinal(to(1_100)).unwrap(), "mil cien"); // Thousand's milliard is plural - assert_eq!(es.to_cardinal(2_100).unwrap(), "dos mil cien"); + assert_eq!(es.to_cardinal(to(2_100)).unwrap(), "dos mil cien"); // Cardinal number ending in 1 always ends with "uno" - assert!(es.to_cardinal(12_313_213_556_451_233_521_251).unwrap().ends_with("uno")); + assert!(es.to_cardinal(to(12_233_521_251)).unwrap().ends_with("uno")); // triplet with value "10" - assert_eq!(es.to_cardinal(110_010_000).unwrap(), "ciento diez millones diez mil"); + assert_eq!(es.to_cardinal(to(110_010_000)).unwrap(), "ciento diez millones diez mil"); // Triplets ending in 1 but higher than 30, is "uno" // "un" is reserved for triplet == 1 in magnitudes higher than 10^3 like "un millón" // or "un trillón" assert_eq!( - es.to_cardinal(171_031_041_031).unwrap(), + es.to_cardinal(to(171_031_041_031)).unwrap(), "ciento setenta y uno billones treinta y uno millones cuarenta y uno mil treinta y uno" ); // Triplets ending in 1 but higher than 30, is never "un" // consequently should never contain " un " as substring anywhere unless proven otherwise assert_ne!( - es.to_cardinal(171_031_041_031).unwrap(), + es.to_cardinal(to(171_031_041_031)).unwrap(), "ciento setenta y un billones treinta y un millones cuarenta y un mil treinta y uno", ); - assert!(!es.to_cardinal(171_031_041_031).unwrap().contains(" un ")); + assert!(!es.to_cardinal(to(171_031_041_031)).unwrap().contains(" un ")); // with veinti flavour let es = es.with_veinti(true); assert_eq!( - es.to_cardinal(21_021_321_021).unwrap(), + es.to_cardinal(to(21_021_321_021)).unwrap(), "veintiun billones veintiun millones trescientos veintiun mil veintiuno" ); - assert_eq!(es.to_cardinal(22_000_000).unwrap(), "veintidos millones"); - assert_eq!(es.to_cardinal(20_020_020).unwrap(), "veinte millones veinte mil veinte"); + assert_eq!(es.to_cardinal(to(22_000_000)).unwrap(), "veintidos millones"); + assert_eq!(es.to_cardinal(to(20_020_020)).unwrap(), "veinte millones veinte mil veinte"); } #[test] fn lang_es_millions() { let es = Spanish::default(); // When thousands triplet is 1 - assert_eq!(es.to_cardinal(1_001_000).unwrap(), "un millón mil"); - assert_eq!(es.to_cardinal(10_001_010).unwrap(), "diez millones mil diez"); - assert_eq!(es.to_cardinal(19_001_010).unwrap(), "diecinueve millones mil diez"); - assert_eq!(es.to_cardinal(801_001_001).unwrap(), "ochocientos uno millones mil uno"); - assert_eq!(es.to_cardinal(800_001_001).unwrap(), "ochocientos millones mil uno"); + assert_eq!(es.to_cardinal(to(1_001_000)).unwrap(), "un millón mil"); + assert_eq!(es.to_cardinal(to(10_001_010)).unwrap(), "diez millones mil diez"); + assert_eq!(es.to_cardinal(to(19_001_010)).unwrap(), "diecinueve millones mil diez"); + assert_eq!(es.to_cardinal(to(801_001_001)).unwrap(), "ochocientos uno millones mil uno"); + assert_eq!(es.to_cardinal(to(800_001_001)).unwrap(), "ochocientos millones mil uno"); // when thousands triplet isn't 1 - assert_eq!(es.to_cardinal(1_002_010).unwrap(), "un millón dos mil diez"); - assert_eq!(es.to_cardinal(10_002_010).unwrap(), "diez millones dos mil diez"); - assert_eq!(es.to_cardinal(19_102_010).unwrap(), "diecinueve millones ciento dos mil diez"); - assert_eq!(es.to_cardinal(800_100_001).unwrap(), "ochocientos millones cien mil uno"); + assert_eq!(es.to_cardinal(to(1_002_010)).unwrap(), "un millón dos mil diez"); + assert_eq!(es.to_cardinal(to(10_002_010)).unwrap(), "diez millones dos mil diez"); + assert_eq!( + es.to_cardinal(to(19_102_010)).unwrap(), + "diecinueve millones ciento dos mil diez" + ); + assert_eq!(es.to_cardinal(to(800_100_001)).unwrap(), "ochocientos millones cien mil uno"); assert_eq!( - es.to_cardinal(801_021_001).unwrap(), + es.to_cardinal(to(801_021_001)).unwrap(), "ochocientos uno millones veinte y uno mil uno" ); - assert_eq!(es.to_cardinal(1_000_000).unwrap(), "un millón"); - assert_eq!(es.to_cardinal(1_000_000_000).unwrap(), "un billón"); - assert_eq!(es.to_cardinal(1_001_100_001).unwrap(), "un billón un millón cien mil uno"); + assert_eq!(es.to_cardinal(to(1_000_000)).unwrap(), "un millón"); + assert_eq!(es.to_cardinal(to(1_000_000_000)).unwrap(), "un billón"); + assert_eq!(es.to_cardinal(to(1_001_100_001)).unwrap(), "un billón un millón cien mil uno"); } #[test] @@ -373,26 +391,26 @@ mod tests { use NegativeFlavour::*; es.set_neg_flavour(Appended); - assert_eq!(es.to_cardinal(-1).unwrap(), "uno negativo"); - assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "un millón negativo"); + assert_eq!(es.to_cardinal((-1).into()).unwrap(), "uno negativo"); + assert_eq!(es.to_cardinal((-1_000_000).into()).unwrap(), "un millón negativo"); assert_eq!( - es.to_cardinal(-1_020_010_000).unwrap(), + es.to_cardinal((-1_020_010_000).into()).unwrap(), "un billón veinte millones diez mil negativo" ); es.set_neg_flavour(Prepended); - assert_eq!(es.to_cardinal(-1).unwrap(), "menos uno"); - assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "menos un millón"); + assert_eq!(es.to_cardinal((-1).into()).unwrap(), "menos uno"); + assert_eq!(es.to_cardinal((-1_000_000).into()).unwrap(), "menos un millón"); assert_eq!( - es.to_cardinal(-1_020_010_000).unwrap(), + es.to_cardinal((-1_020_010_000).into()).unwrap(), "menos un billón veinte millones diez mil" ); es.set_neg_flavour(BelowZero); - assert_eq!(es.to_cardinal(-1).unwrap(), "uno bajo cero"); - assert_eq!(es.to_cardinal(-1_000_000).unwrap(), "un millón bajo cero"); + assert_eq!(es.to_cardinal((-1).into()).unwrap(), "uno bajo cero"); + assert_eq!(es.to_cardinal((-1_000_000).into()).unwrap(), "un millón bajo cero"); assert_eq!( - es.to_cardinal(-1_020_010_000).unwrap(), + es.to_cardinal((-1_020_010_000).into()).unwrap(), "un billón veinte millones diez mil bajo cero" ); } @@ -404,8 +422,8 @@ mod tests { for flavour in [Prepended, Appended, BelowZero] { es.set_neg_flavour(flavour); for value in VALUES.iter().cloned() { - let positive = es.to_cardinal(value.abs()).unwrap(); - let negative = es.to_cardinal(-value.abs()).unwrap(); + let positive = es.to_cardinal(to(value).abs()).unwrap(); + let negative = es.to_cardinal(-to(value).abs()).unwrap(); assert!( negative.contains(positive.as_str()), "{} !contains {}", From df765fd1d2bc1391e0658c71346ccf281ca761c5 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 29 Mar 2024 13:18:02 -0500 Subject: [PATCH 09/52] Consume Vec of Triplets instead of reading it --- src/lang/es.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 252077b..0310864 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -184,7 +184,7 @@ impl Spanish { } let mut words = vec![]; - for (i, triplet) in self.en_miles(num).iter().enumerate().rev() { + for (i, triplet) in self.en_miles(num).into_iter().enumerate().rev() { let hundreds = ((triplet / 100) % 10) as usize; let tens = ((triplet / 10) % 10) as usize; let units = (triplet % 10) as usize; @@ -203,12 +203,12 @@ impl Spanish { // case `1_001_000` => `un millón mil` instead of `un millón un mil` // Explanation: Second second triplet is always read as thousand, so we // don't need to say "un mil" - (_, 1) if triplet == &1 => "", + (_, 1) if triplet == 1 => "", // case `001_001_100...` => `un billón un millón cien mil...` instead of // `uno billón uno millón cien mil...` // All `triplets == 1`` can can be named as "un". except for first or second // triplet - (_, index) if index != 0 && *triplet == 1 => "un", + (_, index) if index != 0 && triplet == 1 => "un", _ => UNIDADES[units], }; @@ -236,12 +236,12 @@ impl Spanish { } // Add the next Milliard if there's any. - if i != 0 && triplet != &0 { + if i != 0 && triplet != 0 { if i > MILLARES.len() - 1 { return Err(Num2Err::CannotConvert); } // Boolean that checks if next Milliard is plural - let plural = *triplet != 1; + let plural = triplet != 1; match plural { false => words.push(String::from(MILLAR[i])), true => words.push(String::from(MILLARES[i])), From e41e33edb28c3f18196cab07b466c69320914494 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 29 Mar 2024 14:28:21 -0500 Subject: [PATCH 10/52] Refactor out the Integer equivalent of to_cardinal() --- src/lang/es.rs | 180 ++++++++++++++++++++++++++++++------------------- src/main.rs | 92 +++++++++++++------------ 2 files changed, 159 insertions(+), 113 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 0310864..0ea8a9f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -116,7 +116,8 @@ pub struct Spanish { veinti: bool, } -#[derive(Default, Clone, Debug, PartialEq, Eq)] +// TODO: Remove Copy trait if enums can store data +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] pub enum NegativeFlavour { #[default] Prepended, // -1 => menos uno @@ -124,6 +125,10 @@ pub enum NegativeFlavour { BelowZero, // -1 => uno bajo cero } +pub enum DecimalSeparator { + Coma, + Punto, +} impl Display for NegativeFlavour { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { @@ -177,7 +182,7 @@ impl Spanish { thousands } - pub fn to_cardinal(&self, num: BigFloat) -> Result { + pub fn to_int_cardinal(&self, num: BigFloat) -> Result { // for 0 case if num.is_zero() { return Ok(String::from("cero")); @@ -250,21 +255,38 @@ impl Spanish { } // flavour the text when negative if let (flavour, true) = (&self.neg_flavour, num.is_negative()) { - use NegativeFlavour::*; - let string = flavour.to_string(); - match flavour { - Prepended => words.insert(0, string), - Appended => words.push(string), - BelowZero => words.push(string), - } + self.flavourize_with_negative(&mut words, *flavour) } Ok(words .into_iter() .filter_map(|word| (!word.is_empty()).then_some(word)) - .collect::>() + .collect::>() .join(" ")) } + + fn to_cardinal(&self, num: BigFloat) -> Result { + unimplemented!() + } + + fn to_float_cardinal(&self, num: BigFloat) -> Result { + unimplemented!() + } + + fn fractio_cardinal(&self, num: BigFloat) -> Result { + unimplemented!() + } + + // TODO: Refactor away if it only has a single callsite + fn flavourize_with_negative(&self, words: &mut Vec, flavour: NegativeFlavour) { + use NegativeFlavour::*; + let string = flavour.to_string(); + match flavour { + Prepended => words.insert(0, string), + Appended => words.push(string), + BelowZero => words.push(string), + } + } } #[cfg(test)] @@ -277,40 +299,43 @@ mod tests { #[test] fn lang_es_sub_thousands() { let es = Spanish::default(); - assert_eq!(es.to_cardinal(to(000)).unwrap(), "cero"); - assert_eq!(es.to_cardinal(to(10)).unwrap(), "diez"); - assert_eq!(es.to_cardinal(to(100)).unwrap(), "cien"); - assert_eq!(es.to_cardinal(to(101)).unwrap(), "ciento uno"); - assert_eq!(es.to_cardinal(to(110)).unwrap(), "ciento diez"); - assert_eq!(es.to_cardinal(to(111)).unwrap(), "ciento once"); - assert_eq!(es.to_cardinal(to(141)).unwrap(), "ciento cuarenta y uno"); - assert_eq!(es.to_cardinal(to(142)).unwrap(), "ciento cuarenta y dos"); - assert_eq!(es.to_cardinal(to(800)).unwrap(), "ochocientos"); + assert_eq!(es.to_int_cardinal(to(000)).unwrap(), "cero"); + assert_eq!(es.to_int_cardinal(to(10)).unwrap(), "diez"); + assert_eq!(es.to_int_cardinal(to(100)).unwrap(), "cien"); + assert_eq!(es.to_int_cardinal(to(101)).unwrap(), "ciento uno"); + assert_eq!(es.to_int_cardinal(to(110)).unwrap(), "ciento diez"); + assert_eq!(es.to_int_cardinal(to(111)).unwrap(), "ciento once"); + assert_eq!(es.to_int_cardinal(to(141)).unwrap(), "ciento cuarenta y uno"); + assert_eq!(es.to_int_cardinal(to(142)).unwrap(), "ciento cuarenta y dos"); + assert_eq!(es.to_int_cardinal(to(800)).unwrap(), "ochocientos"); } #[test] fn lang_es_thousands() { let es = Spanish::default(); // When thousands triplet is 1 - assert_eq!(es.to_cardinal(to(1_000)).unwrap(), "mil"); - assert_eq!(es.to_cardinal(to(1_010)).unwrap(), "mil diez"); - assert_eq!(es.to_cardinal(to(1_100)).unwrap(), "mil cien"); - assert_eq!(es.to_cardinal(to(1_101)).unwrap(), "mil ciento uno"); - assert_eq!(es.to_cardinal(to(1_110)).unwrap(), "mil ciento diez"); - assert_eq!(es.to_cardinal(to(1_111)).unwrap(), "mil ciento once"); - assert_eq!(es.to_cardinal(to(1_141)).unwrap(), "mil ciento cuarenta y uno"); + assert_eq!(es.to_int_cardinal(to(1_000)).unwrap(), "mil"); + assert_eq!(es.to_int_cardinal(to(1_010)).unwrap(), "mil diez"); + assert_eq!(es.to_int_cardinal(to(1_100)).unwrap(), "mil cien"); + assert_eq!(es.to_int_cardinal(to(1_101)).unwrap(), "mil ciento uno"); + assert_eq!(es.to_int_cardinal(to(1_110)).unwrap(), "mil ciento diez"); + assert_eq!(es.to_int_cardinal(to(1_111)).unwrap(), "mil ciento once"); + assert_eq!(es.to_int_cardinal(to(1_141)).unwrap(), "mil ciento cuarenta y uno"); // When thousands triplet isn't 1 - assert_eq!(es.to_cardinal(to(2_000)).unwrap(), "dos mil"); - assert_eq!(es.to_cardinal(to(12_010)).unwrap(), "doce mil diez"); - assert_eq!(es.to_cardinal(to(140_100)).unwrap(), "ciento cuarenta mil cien"); - assert_eq!(es.to_cardinal(to(141_101)).unwrap(), "ciento cuarenta y uno mil ciento uno"); - assert_eq!(es.to_cardinal(to(142_002)).unwrap(), "ciento cuarenta y dos mil dos"); - assert_eq!(es.to_cardinal(to(142_000)).unwrap(), "ciento cuarenta y dos mil"); + assert_eq!(es.to_int_cardinal(to(2_000)).unwrap(), "dos mil"); + assert_eq!(es.to_int_cardinal(to(12_010)).unwrap(), "doce mil diez"); + assert_eq!(es.to_int_cardinal(to(140_100)).unwrap(), "ciento cuarenta mil cien"); assert_eq!( - es.to_cardinal(to(888_111)).unwrap(), + es.to_int_cardinal(to(141_101)).unwrap(), + "ciento cuarenta y uno mil ciento uno" + ); + assert_eq!(es.to_int_cardinal(to(142_002)).unwrap(), "ciento cuarenta y dos mil dos"); + assert_eq!(es.to_int_cardinal(to(142_000)).unwrap(), "ciento cuarenta y dos mil"); + assert_eq!( + es.to_int_cardinal(to(888_111)).unwrap(), "ochocientos ochenta y ocho mil ciento once" ); - assert_eq!(es.to_cardinal(to(800_000)).unwrap(), "ochocientos mil"); + assert_eq!(es.to_int_cardinal(to(800_000)).unwrap(), "ochocientos mil"); } #[test] @@ -318,65 +343,80 @@ mod tests { // This might make other tests trivial let es = Spanish::default(); // Triplet == 1 inserts following milliard in singular - assert_eq!(es.to_cardinal(to(1_001_001_000)).unwrap(), "un billón un millón mil"); + assert_eq!(es.to_int_cardinal(to(1_001_001_000)).unwrap(), "un billón un millón mil"); // Triplet != 1 inserts following milliard in plural - assert_eq!(es.to_cardinal(to(2_002_002_000)).unwrap(), "dos billones dos millones dos mil"); + assert_eq!( + es.to_int_cardinal(to(2_002_002_000)).unwrap(), + "dos billones dos millones dos mil" + ); // Thousand's milliard is singular - assert_eq!(es.to_cardinal(to(1_100)).unwrap(), "mil cien"); + assert_eq!(es.to_int_cardinal(to(1_100)).unwrap(), "mil cien"); // Thousand's milliard is plural - assert_eq!(es.to_cardinal(to(2_100)).unwrap(), "dos mil cien"); + assert_eq!(es.to_int_cardinal(to(2_100)).unwrap(), "dos mil cien"); // Cardinal number ending in 1 always ends with "uno" - assert!(es.to_cardinal(to(12_233_521_251)).unwrap().ends_with("uno")); + assert!(es.to_int_cardinal(to(12_233_521_251)).unwrap().ends_with("uno")); // triplet with value "10" - assert_eq!(es.to_cardinal(to(110_010_000)).unwrap(), "ciento diez millones diez mil"); + assert_eq!(es.to_int_cardinal(to(110_010_000)).unwrap(), "ciento diez millones diez mil"); // Triplets ending in 1 but higher than 30, is "uno" // "un" is reserved for triplet == 1 in magnitudes higher than 10^3 like "un millón" // or "un trillón" assert_eq!( - es.to_cardinal(to(171_031_041_031)).unwrap(), + es.to_int_cardinal(to(171_031_041_031)).unwrap(), "ciento setenta y uno billones treinta y uno millones cuarenta y uno mil treinta y uno" ); // Triplets ending in 1 but higher than 30, is never "un" // consequently should never contain " un " as substring anywhere unless proven otherwise assert_ne!( - es.to_cardinal(to(171_031_041_031)).unwrap(), + es.to_int_cardinal(to(171_031_041_031)).unwrap(), "ciento setenta y un billones treinta y un millones cuarenta y un mil treinta y uno", ); - assert!(!es.to_cardinal(to(171_031_041_031)).unwrap().contains(" un ")); + assert!(!es.to_int_cardinal(to(171_031_041_031)).unwrap().contains(" un ")); // with veinti flavour let es = es.with_veinti(true); assert_eq!( - es.to_cardinal(to(21_021_321_021)).unwrap(), + es.to_int_cardinal(to(21_021_321_021)).unwrap(), "veintiun billones veintiun millones trescientos veintiun mil veintiuno" ); - assert_eq!(es.to_cardinal(to(22_000_000)).unwrap(), "veintidos millones"); - assert_eq!(es.to_cardinal(to(20_020_020)).unwrap(), "veinte millones veinte mil veinte"); + assert_eq!(es.to_int_cardinal(to(22_000_000)).unwrap(), "veintidos millones"); + assert_eq!( + es.to_int_cardinal(to(20_020_020)).unwrap(), + "veinte millones veinte mil veinte" + ); } #[test] fn lang_es_millions() { let es = Spanish::default(); // When thousands triplet is 1 - assert_eq!(es.to_cardinal(to(1_001_000)).unwrap(), "un millón mil"); - assert_eq!(es.to_cardinal(to(10_001_010)).unwrap(), "diez millones mil diez"); - assert_eq!(es.to_cardinal(to(19_001_010)).unwrap(), "diecinueve millones mil diez"); - assert_eq!(es.to_cardinal(to(801_001_001)).unwrap(), "ochocientos uno millones mil uno"); - assert_eq!(es.to_cardinal(to(800_001_001)).unwrap(), "ochocientos millones mil uno"); + assert_eq!(es.to_int_cardinal(to(1_001_000)).unwrap(), "un millón mil"); + assert_eq!(es.to_int_cardinal(to(10_001_010)).unwrap(), "diez millones mil diez"); + assert_eq!(es.to_int_cardinal(to(19_001_010)).unwrap(), "diecinueve millones mil diez"); + assert_eq!( + es.to_int_cardinal(to(801_001_001)).unwrap(), + "ochocientos uno millones mil uno" + ); + assert_eq!(es.to_int_cardinal(to(800_001_001)).unwrap(), "ochocientos millones mil uno"); // when thousands triplet isn't 1 - assert_eq!(es.to_cardinal(to(1_002_010)).unwrap(), "un millón dos mil diez"); - assert_eq!(es.to_cardinal(to(10_002_010)).unwrap(), "diez millones dos mil diez"); + assert_eq!(es.to_int_cardinal(to(1_002_010)).unwrap(), "un millón dos mil diez"); + assert_eq!(es.to_int_cardinal(to(10_002_010)).unwrap(), "diez millones dos mil diez"); assert_eq!( - es.to_cardinal(to(19_102_010)).unwrap(), + es.to_int_cardinal(to(19_102_010)).unwrap(), "diecinueve millones ciento dos mil diez" ); - assert_eq!(es.to_cardinal(to(800_100_001)).unwrap(), "ochocientos millones cien mil uno"); assert_eq!( - es.to_cardinal(to(801_021_001)).unwrap(), + es.to_int_cardinal(to(800_100_001)).unwrap(), + "ochocientos millones cien mil uno" + ); + assert_eq!( + es.to_int_cardinal(to(801_021_001)).unwrap(), "ochocientos uno millones veinte y uno mil uno" ); - assert_eq!(es.to_cardinal(to(1_000_000)).unwrap(), "un millón"); - assert_eq!(es.to_cardinal(to(1_000_000_000)).unwrap(), "un billón"); - assert_eq!(es.to_cardinal(to(1_001_100_001)).unwrap(), "un billón un millón cien mil uno"); + assert_eq!(es.to_int_cardinal(to(1_000_000)).unwrap(), "un millón"); + assert_eq!(es.to_int_cardinal(to(1_000_000_000)).unwrap(), "un billón"); + assert_eq!( + es.to_int_cardinal(to(1_001_100_001)).unwrap(), + "un billón un millón cien mil uno" + ); } #[test] @@ -391,26 +431,26 @@ mod tests { use NegativeFlavour::*; es.set_neg_flavour(Appended); - assert_eq!(es.to_cardinal((-1).into()).unwrap(), "uno negativo"); - assert_eq!(es.to_cardinal((-1_000_000).into()).unwrap(), "un millón negativo"); + assert_eq!(es.to_int_cardinal((-1).into()).unwrap(), "uno negativo"); + assert_eq!(es.to_int_cardinal((-1_000_000).into()).unwrap(), "un millón negativo"); assert_eq!( - es.to_cardinal((-1_020_010_000).into()).unwrap(), + es.to_int_cardinal((-1_020_010_000).into()).unwrap(), "un billón veinte millones diez mil negativo" ); es.set_neg_flavour(Prepended); - assert_eq!(es.to_cardinal((-1).into()).unwrap(), "menos uno"); - assert_eq!(es.to_cardinal((-1_000_000).into()).unwrap(), "menos un millón"); + assert_eq!(es.to_int_cardinal((-1).into()).unwrap(), "menos uno"); + assert_eq!(es.to_int_cardinal((-1_000_000).into()).unwrap(), "menos un millón"); assert_eq!( - es.to_cardinal((-1_020_010_000).into()).unwrap(), + es.to_int_cardinal((-1_020_010_000).into()).unwrap(), "menos un billón veinte millones diez mil" ); es.set_neg_flavour(BelowZero); - assert_eq!(es.to_cardinal((-1).into()).unwrap(), "uno bajo cero"); - assert_eq!(es.to_cardinal((-1_000_000).into()).unwrap(), "un millón bajo cero"); + assert_eq!(es.to_int_cardinal((-1).into()).unwrap(), "uno bajo cero"); + assert_eq!(es.to_int_cardinal((-1_000_000).into()).unwrap(), "un millón bajo cero"); assert_eq!( - es.to_cardinal((-1_020_010_000).into()).unwrap(), + es.to_int_cardinal((-1_020_010_000).into()).unwrap(), "un billón veinte millones diez mil bajo cero" ); } @@ -422,8 +462,8 @@ mod tests { for flavour in [Prepended, Appended, BelowZero] { es.set_neg_flavour(flavour); for value in VALUES.iter().cloned() { - let positive = es.to_cardinal(to(value).abs()).unwrap(); - let negative = es.to_cardinal(-to(value).abs()).unwrap(); + let positive = es.to_int_cardinal(to(value).abs()).unwrap(); + let negative = es.to_int_cardinal(-to(value).abs()).unwrap(); assert!( negative.contains(positive.as_str()), "{} !contains {}", diff --git a/src/main.rs b/src/main.rs index bcc2d73..073f1e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,50 +1,56 @@ -use num2words::lang::Spanish; use std::io::Write; + +use num2words::lang::Spanish; +use num_bigfloat::BigFloat; pub fn main() { let es = Spanish::default(); - println!("Resultado {:?}", es.to_cardinal(dbg!(1_002_002_031))); - println!("Resultado {:?}", es.to_cardinal(dbg!(-1_010_001_031))); - println!("Resultado {:?}", es.to_cardinal(dbg!(1_001_021_031))); + println!("Resultado {:?}", es.to_int_cardinal(1_002_002_031.into())); + println!("Resultado {:?}", es.to_int_cardinal((-1_010_001_031).into())); + println!("Resultado {:?}", es.to_int_cardinal(1_001_021_031.into())); - let mut input = String::new(); - print!("\nIngrese un número para convertir a palabras\nIngrese `exit` para salir:\n\n"); - fn read_line(input: &mut String) { - input.clear(); - std::io::stdin().read_line(input).unwrap(); - } - - loop { - print!("Ingrese su número: "); - flush(); - read_line(&mut input); - let input = input.trim(); - match input { - "exit" => { - clear_terminal(); - println!("Saliendo..."); - break; - } - "clear" => { - clear_terminal(); - continue; - } - _ => {} - } - if input.is_empty() { - println!("Número inválido {input:?} no puede estar vacío"); - continue; - } - let num = match input.parse::() { - Ok(num) => num, - Err(_) => { - println!("Número inválido {input:?} - no es convertible a un número entero"); - continue; - } - }; - print!("Entrada:"); - pretty_print_int(num); - println!(" => {:?}", es.to_cardinal(num).unwrap()); - } + let e = BigFloat::from(215.25f64); + // println!("{:?}\n{:?}\n{:?}", e, e.frac(), e.int()); + println!("\n\n{}\nfrac: {}\nint : {}\n\n", e, e.frac(), e.int()); + println!("{}", e.frac().rem(&(10.into()))); + println!("{}", e.frac().rem(&(100.into()))); + // let mut input = String::new(); + // print!("\nIngrese un número para convertir a palabras\nIngrese `exit` para salir:\n\n"); + // fn read_line(input: &mut String) { + // input.clear(); + // std::io::stdin().read_line(input).unwrap(); + // } + // loop { + // print!("Ingrese su número: "); + // flush(); + // read_line(&mut input); + // let input = input.trim(); + // match input { + // "exit" => { + // clear_terminal(); + // println!("Saliendo..."); + // break; + // } + // "clear" => { + // clear_terminal(); + // continue; + // } + // _ => {} + // } + // if input.is_empty() { + // println!("Número inválido {input:?} no puede estar vacío"); + // continue; + // } + // let num = match input.parse::() { + // Ok(num) => num, + // Err(_) => { + // println!("Número inválido {input:?} - no es convertible a un número entero"); + // continue; + // } + // }; + // print!("Entrada:"); + // pretty_print_int(num); + // println!(" => {:?}", es.to_int_cardinal(num.into()).unwrap()); + // } } pub fn clear_terminal() { print!("{esc}[2J{esc}[1;1H", esc = 27 as char); From fe3638c00cd76557952ad05f220c321e7be8dff5 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sat, 30 Mar 2024 01:44:55 -0500 Subject: [PATCH 11/52] Conversion for fractional number --- src/lang/es.rs | 289 +++++++++++++++++++++++++++++++++++-------------- src/main.rs | 15 ++- 2 files changed, 217 insertions(+), 87 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 0ea8a9f..040c13e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -114,6 +114,7 @@ pub struct Spanish { neg_flavour: NegativeFlavour, // Writes the number as "veintiocho" instead of "veinte y ocho" in case of true veinti: bool, + decimal_char: DecimalChar, } // TODO: Remove Copy trait if enums can store data @@ -124,11 +125,6 @@ pub enum NegativeFlavour { Appended, // -1 => uno negativo BelowZero, // -1 => uno bajo cero } - -pub enum DecimalSeparator { - Coma, - Punto, -} impl Display for NegativeFlavour { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { @@ -139,6 +135,30 @@ impl Display for NegativeFlavour { } } +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] +pub enum DecimalChar { + #[default] + Punto, + Coma, +} + +impl DecimalChar { + #[inline(always)] + pub fn to_word(&self) -> &'static str { + match self { + DecimalChar::Punto => "punto", + DecimalChar::Coma => "coma", + } + } + + #[inline(always)] + pub fn to_char(self) -> char { + match self { + DecimalChar::Punto => '.', + DecimalChar::Coma => ',', + } + } +} impl Spanish { #[inline(always)] pub fn new() -> Self { @@ -151,6 +171,11 @@ impl Spanish { self } + #[inline(always)] + pub fn with_veinti(self, veinti: bool) -> Self { + Self { veinti, ..self } + } + #[inline(always)] pub fn set_neg_flavour(&mut self, flavour: NegativeFlavour) -> &mut Self { self.neg_flavour = flavour; @@ -158,18 +183,33 @@ impl Spanish { } #[inline(always)] - pub fn with_neg_flavour(mut self, flavour: NegativeFlavour) -> Self { - self.neg_flavour = flavour; - self + pub fn with_neg_flavour(self, flavour: NegativeFlavour) -> Self { + Self { neg_flavour: flavour, ..self } } #[inline(always)] - pub fn with_veinti(mut self, veinti: bool) -> Self { - self.veinti = veinti; + pub fn set_decimal_char(&mut self, decimal_char: DecimalChar) -> &mut Self { + self.decimal_char = decimal_char; self } #[inline(always)] + pub fn with_decimal_char(self, decimal_char: DecimalChar) -> Self { + Self { decimal_char, ..self } + } + + pub fn to_cardinal(&self, num: BigFloat) -> Result { + if num.is_inf() { + self.inf_to_cardinal(&num) + } else if num.frac().is_zero() { + self.int_to_cardinal(num) + } else { + self.float_to_cardinal(&num) + } + } + + #[inline(always)] + // Converts Integer BigFloat to a vector of u64 fn en_miles(&self, mut num: BigFloat) -> Vec { let mut thousands = Vec::new(); let mil = 1000.into(); @@ -182,14 +222,14 @@ impl Spanish { thousands } - pub fn to_int_cardinal(&self, num: BigFloat) -> Result { - // for 0 case + // Only should be called if you're sure the number has no fraction + fn int_to_cardinal(&self, num: BigFloat) -> Result { if num.is_zero() { return Ok(String::from("cero")); } let mut words = vec![]; - for (i, triplet) in self.en_miles(num).into_iter().enumerate().rev() { + for (i, triplet) in self.en_miles(num.int()).into_iter().enumerate().rev() { let hundreds = ((triplet / 100) % 10) as usize; let tens = ((triplet / 10) % 10) as usize; let units = (triplet % 10) as usize; @@ -204,9 +244,9 @@ impl Spanish { if tens != 0 || units != 0 { let unit_word = match (units, i) { - // case `1_100` => `mil cien` instead of `un mil un cien` + // case `1_100` => `mil cien` instead of `un mil cien` // case `1_001_000` => `un millón mil` instead of `un millón un mil` - // Explanation: Second second triplet is always read as thousand, so we + // Explanation: Second triplet is always read as thousand, so we // don't need to say "un mil" (_, 1) if triplet == 1 => "", // case `001_001_100...` => `un billón un millón cien mil...` instead of @@ -265,16 +305,47 @@ impl Spanish { .join(" ")) } - fn to_cardinal(&self, num: BigFloat) -> Result { - unimplemented!() - } + fn float_to_cardinal(&self, num: &BigFloat) -> Result { + let mut words = vec![]; + let is_negative = num.is_negative(); + let num = num.abs(); + let integral_word = self.int_to_cardinal(num.int())?; + words.push(integral_word); - fn to_float_cardinal(&self, num: BigFloat) -> Result { - unimplemented!() + let mut fraction_part = num.frac(); + if !fraction_part.is_zero() { + // Inserts decimal separator + words.push(self.decimal_char.to_word().to_string()); + } + + while !fraction_part.is_zero() { + let digit = (fraction_part * BigFloat::from(10)).int(); + fraction_part = (fraction_part * BigFloat::from(10)).frac(); + words.push(match digit.to_u64().unwrap() { + 0 => String::from("cero"), + i => String::from(UNIDADES[i as usize]), + }); + } + if is_negative { + self.flavourize_with_negative(&mut words, self.neg_flavour); + } + Ok(words.join(" ")) } - fn fractio_cardinal(&self, num: BigFloat) -> Result { - unimplemented!() + #[inline(always)] + fn inf_to_cardinal(&self, num: &BigFloat) -> Result { + if !num.is_inf() { + Err(Num2Err::CannotConvert) + } else if num.is_inf_pos() { + Ok(String::from("infinito")) + } else { + Ok(match self.neg_flavour { + NegativeFlavour::Prepended => String::from("menos infinito"), + NegativeFlavour::Appended => String::from("infinito negativo"), + // Defaults to menos because it doesn't make sense to call `infinito bajo cero` + NegativeFlavour::BelowZero => String::from("menos infinito"), + }) + } } // TODO: Refactor away if it only has a single callsite @@ -299,43 +370,43 @@ mod tests { #[test] fn lang_es_sub_thousands() { let es = Spanish::default(); - assert_eq!(es.to_int_cardinal(to(000)).unwrap(), "cero"); - assert_eq!(es.to_int_cardinal(to(10)).unwrap(), "diez"); - assert_eq!(es.to_int_cardinal(to(100)).unwrap(), "cien"); - assert_eq!(es.to_int_cardinal(to(101)).unwrap(), "ciento uno"); - assert_eq!(es.to_int_cardinal(to(110)).unwrap(), "ciento diez"); - assert_eq!(es.to_int_cardinal(to(111)).unwrap(), "ciento once"); - assert_eq!(es.to_int_cardinal(to(141)).unwrap(), "ciento cuarenta y uno"); - assert_eq!(es.to_int_cardinal(to(142)).unwrap(), "ciento cuarenta y dos"); - assert_eq!(es.to_int_cardinal(to(800)).unwrap(), "ochocientos"); + assert_eq!(es.int_to_cardinal(to(000)).unwrap(), "cero"); + assert_eq!(es.int_to_cardinal(to(10)).unwrap(), "diez"); + assert_eq!(es.int_to_cardinal(to(100)).unwrap(), "cien"); + assert_eq!(es.int_to_cardinal(to(101)).unwrap(), "ciento uno"); + assert_eq!(es.int_to_cardinal(to(110)).unwrap(), "ciento diez"); + assert_eq!(es.int_to_cardinal(to(111)).unwrap(), "ciento once"); + assert_eq!(es.int_to_cardinal(to(141)).unwrap(), "ciento cuarenta y uno"); + assert_eq!(es.int_to_cardinal(to(142)).unwrap(), "ciento cuarenta y dos"); + assert_eq!(es.int_to_cardinal(to(800)).unwrap(), "ochocientos"); } #[test] fn lang_es_thousands() { let es = Spanish::default(); // When thousands triplet is 1 - assert_eq!(es.to_int_cardinal(to(1_000)).unwrap(), "mil"); - assert_eq!(es.to_int_cardinal(to(1_010)).unwrap(), "mil diez"); - assert_eq!(es.to_int_cardinal(to(1_100)).unwrap(), "mil cien"); - assert_eq!(es.to_int_cardinal(to(1_101)).unwrap(), "mil ciento uno"); - assert_eq!(es.to_int_cardinal(to(1_110)).unwrap(), "mil ciento diez"); - assert_eq!(es.to_int_cardinal(to(1_111)).unwrap(), "mil ciento once"); - assert_eq!(es.to_int_cardinal(to(1_141)).unwrap(), "mil ciento cuarenta y uno"); + assert_eq!(es.int_to_cardinal(to(1_000)).unwrap(), "mil"); + assert_eq!(es.int_to_cardinal(to(1_010)).unwrap(), "mil diez"); + assert_eq!(es.int_to_cardinal(to(1_100)).unwrap(), "mil cien"); + assert_eq!(es.int_to_cardinal(to(1_101)).unwrap(), "mil ciento uno"); + assert_eq!(es.int_to_cardinal(to(1_110)).unwrap(), "mil ciento diez"); + assert_eq!(es.int_to_cardinal(to(1_111)).unwrap(), "mil ciento once"); + assert_eq!(es.int_to_cardinal(to(1_141)).unwrap(), "mil ciento cuarenta y uno"); // When thousands triplet isn't 1 - assert_eq!(es.to_int_cardinal(to(2_000)).unwrap(), "dos mil"); - assert_eq!(es.to_int_cardinal(to(12_010)).unwrap(), "doce mil diez"); - assert_eq!(es.to_int_cardinal(to(140_100)).unwrap(), "ciento cuarenta mil cien"); + assert_eq!(es.int_to_cardinal(to(2_000)).unwrap(), "dos mil"); + assert_eq!(es.int_to_cardinal(to(12_010)).unwrap(), "doce mil diez"); + assert_eq!(es.int_to_cardinal(to(140_100)).unwrap(), "ciento cuarenta mil cien"); assert_eq!( - es.to_int_cardinal(to(141_101)).unwrap(), + es.int_to_cardinal(to(141_101)).unwrap(), "ciento cuarenta y uno mil ciento uno" ); - assert_eq!(es.to_int_cardinal(to(142_002)).unwrap(), "ciento cuarenta y dos mil dos"); - assert_eq!(es.to_int_cardinal(to(142_000)).unwrap(), "ciento cuarenta y dos mil"); + assert_eq!(es.int_to_cardinal(to(142_002)).unwrap(), "ciento cuarenta y dos mil dos"); + assert_eq!(es.int_to_cardinal(to(142_000)).unwrap(), "ciento cuarenta y dos mil"); assert_eq!( - es.to_int_cardinal(to(888_111)).unwrap(), + es.int_to_cardinal(to(888_111)).unwrap(), "ochocientos ochenta y ocho mil ciento once" ); - assert_eq!(es.to_int_cardinal(to(800_000)).unwrap(), "ochocientos mil"); + assert_eq!(es.int_to_cardinal(to(800_000)).unwrap(), "ochocientos mil"); } #[test] @@ -343,78 +414,132 @@ mod tests { // This might make other tests trivial let es = Spanish::default(); // Triplet == 1 inserts following milliard in singular - assert_eq!(es.to_int_cardinal(to(1_001_001_000)).unwrap(), "un billón un millón mil"); + assert_eq!(es.int_to_cardinal(to(1_001_001_000)).unwrap(), "un billón un millón mil"); // Triplet != 1 inserts following milliard in plural assert_eq!( - es.to_int_cardinal(to(2_002_002_000)).unwrap(), + es.int_to_cardinal(to(2_002_002_000)).unwrap(), "dos billones dos millones dos mil" ); // Thousand's milliard is singular - assert_eq!(es.to_int_cardinal(to(1_100)).unwrap(), "mil cien"); + assert_eq!(es.int_to_cardinal(to(1_100)).unwrap(), "mil cien"); // Thousand's milliard is plural - assert_eq!(es.to_int_cardinal(to(2_100)).unwrap(), "dos mil cien"); + assert_eq!(es.int_to_cardinal(to(2_100)).unwrap(), "dos mil cien"); // Cardinal number ending in 1 always ends with "uno" - assert!(es.to_int_cardinal(to(12_233_521_251)).unwrap().ends_with("uno")); + assert!(es.int_to_cardinal(to(12_233_521_251)).unwrap().ends_with("uno")); // triplet with value "10" - assert_eq!(es.to_int_cardinal(to(110_010_000)).unwrap(), "ciento diez millones diez mil"); + assert_eq!(es.int_to_cardinal(to(110_010_000)).unwrap(), "ciento diez millones diez mil"); // Triplets ending in 1 but higher than 30, is "uno" // "un" is reserved for triplet == 1 in magnitudes higher than 10^3 like "un millón" // or "un trillón" assert_eq!( - es.to_int_cardinal(to(171_031_041_031)).unwrap(), + es.int_to_cardinal(to(171_031_041_031)).unwrap(), "ciento setenta y uno billones treinta y uno millones cuarenta y uno mil treinta y uno" ); // Triplets ending in 1 but higher than 30, is never "un" // consequently should never contain " un " as substring anywhere unless proven otherwise assert_ne!( - es.to_int_cardinal(to(171_031_041_031)).unwrap(), + es.int_to_cardinal(to(171_031_041_031)).unwrap(), "ciento setenta y un billones treinta y un millones cuarenta y un mil treinta y uno", ); - assert!(!es.to_int_cardinal(to(171_031_041_031)).unwrap().contains(" un ")); + assert!(!es.int_to_cardinal(to(171_031_041_031)).unwrap().contains(" un ")); // with veinti flavour let es = es.with_veinti(true); assert_eq!( - es.to_int_cardinal(to(21_021_321_021)).unwrap(), + es.int_to_cardinal(to(21_021_321_021)).unwrap(), "veintiun billones veintiun millones trescientos veintiun mil veintiuno" ); - assert_eq!(es.to_int_cardinal(to(22_000_000)).unwrap(), "veintidos millones"); + assert_eq!(es.int_to_cardinal(to(22_000_000)).unwrap(), "veintidos millones"); assert_eq!( - es.to_int_cardinal(to(20_020_020)).unwrap(), + es.int_to_cardinal(to(20_020_020)).unwrap(), "veinte millones veinte mil veinte" ); } #[test] + fn lang_es_with_fraction() { + use DecimalChar::{Coma, Punto}; + let es = Spanish::default().with_decimal_char(Punto); + assert_eq!( + es.to_cardinal(BigFloat::from(1.0123456789)).unwrap(), + "uno punto cero uno dos tres cuatro cinco seis siete ocho nueve" + ); + let es = es.with_decimal_char(Coma); + assert_eq!( + es.to_cardinal(BigFloat::from(0.0123456789)).unwrap(), + "cero coma cero uno dos tres cuatro cinco seis siete ocho nueve" + ); + use NegativeFlavour::{Appended, BelowZero, Prepended}; + let es = es.with_neg_flavour(Appended); + assert_eq!( + es.to_cardinal(BigFloat::from(-0.0123456789)).unwrap(), + "cero coma cero uno dos tres cuatro cinco seis siete ocho nueve negativo" + ); + let es = es.with_neg_flavour(Prepended); + assert_eq!( + es.to_cardinal(BigFloat::from(-0.0123456789)).unwrap(), + "menos cero coma cero uno dos tres cuatro cinco seis siete ocho nueve" + ); + let es = es.with_neg_flavour(BelowZero); + assert_eq!( + es.to_cardinal(BigFloat::from(-0.0123456789)).unwrap(), + "cero coma cero uno dos tres cuatro cinco seis siete ocho nueve bajo cero" + ); + } + #[test] + fn lang_es_infinity_and_negatives() { + use NegativeFlavour::*; + let flavours: [NegativeFlavour; 3] = [Prepended, Appended, BelowZero]; + let neg = f64::NEG_INFINITY; + let pos = f64::INFINITY; + for flavour in flavours.iter().cloned() { + let es = Spanish::default().with_neg_flavour(flavour); + match flavour { + Prepended => { + assert_eq!(es.to_cardinal(neg.into()).unwrap(), "menos infinito"); + assert_eq!(es.to_cardinal(pos.into()).unwrap(), "infinito"); + } + Appended => { + assert_eq!(es.to_cardinal(neg.into()).unwrap(), "infinito negativo"); + assert_eq!(es.to_cardinal(pos.into()).unwrap(), "infinito"); + } + BelowZero => { + assert_eq!(es.to_cardinal(neg.into()).unwrap(), "menos infinito"); + assert_eq!(es.to_cardinal(pos.into()).unwrap(), "infinito"); + } + } + } + } + #[test] fn lang_es_millions() { let es = Spanish::default(); // When thousands triplet is 1 - assert_eq!(es.to_int_cardinal(to(1_001_000)).unwrap(), "un millón mil"); - assert_eq!(es.to_int_cardinal(to(10_001_010)).unwrap(), "diez millones mil diez"); - assert_eq!(es.to_int_cardinal(to(19_001_010)).unwrap(), "diecinueve millones mil diez"); + assert_eq!(es.int_to_cardinal(to(1_001_000)).unwrap(), "un millón mil"); + assert_eq!(es.int_to_cardinal(to(10_001_010)).unwrap(), "diez millones mil diez"); + assert_eq!(es.int_to_cardinal(to(19_001_010)).unwrap(), "diecinueve millones mil diez"); assert_eq!( - es.to_int_cardinal(to(801_001_001)).unwrap(), + es.int_to_cardinal(to(801_001_001)).unwrap(), "ochocientos uno millones mil uno" ); - assert_eq!(es.to_int_cardinal(to(800_001_001)).unwrap(), "ochocientos millones mil uno"); + assert_eq!(es.int_to_cardinal(to(800_001_001)).unwrap(), "ochocientos millones mil uno"); // when thousands triplet isn't 1 - assert_eq!(es.to_int_cardinal(to(1_002_010)).unwrap(), "un millón dos mil diez"); - assert_eq!(es.to_int_cardinal(to(10_002_010)).unwrap(), "diez millones dos mil diez"); + assert_eq!(es.int_to_cardinal(to(1_002_010)).unwrap(), "un millón dos mil diez"); + assert_eq!(es.int_to_cardinal(to(10_002_010)).unwrap(), "diez millones dos mil diez"); assert_eq!( - es.to_int_cardinal(to(19_102_010)).unwrap(), + es.int_to_cardinal(to(19_102_010)).unwrap(), "diecinueve millones ciento dos mil diez" ); assert_eq!( - es.to_int_cardinal(to(800_100_001)).unwrap(), + es.int_to_cardinal(to(800_100_001)).unwrap(), "ochocientos millones cien mil uno" ); assert_eq!( - es.to_int_cardinal(to(801_021_001)).unwrap(), + es.int_to_cardinal(to(801_021_001)).unwrap(), "ochocientos uno millones veinte y uno mil uno" ); - assert_eq!(es.to_int_cardinal(to(1_000_000)).unwrap(), "un millón"); - assert_eq!(es.to_int_cardinal(to(1_000_000_000)).unwrap(), "un billón"); + assert_eq!(es.int_to_cardinal(to(1_000_000)).unwrap(), "un millón"); + assert_eq!(es.int_to_cardinal(to(1_000_000_000)).unwrap(), "un billón"); assert_eq!( - es.to_int_cardinal(to(1_001_100_001)).unwrap(), + es.int_to_cardinal(to(1_001_100_001)).unwrap(), "un billón un millón cien mil uno" ); } @@ -431,26 +556,26 @@ mod tests { use NegativeFlavour::*; es.set_neg_flavour(Appended); - assert_eq!(es.to_int_cardinal((-1).into()).unwrap(), "uno negativo"); - assert_eq!(es.to_int_cardinal((-1_000_000).into()).unwrap(), "un millón negativo"); + assert_eq!(es.int_to_cardinal((-1).into()).unwrap(), "uno negativo"); + assert_eq!(es.int_to_cardinal((-1_000_000).into()).unwrap(), "un millón negativo"); assert_eq!( - es.to_int_cardinal((-1_020_010_000).into()).unwrap(), + es.int_to_cardinal((-1_020_010_000).into()).unwrap(), "un billón veinte millones diez mil negativo" ); es.set_neg_flavour(Prepended); - assert_eq!(es.to_int_cardinal((-1).into()).unwrap(), "menos uno"); - assert_eq!(es.to_int_cardinal((-1_000_000).into()).unwrap(), "menos un millón"); + assert_eq!(es.int_to_cardinal((-1).into()).unwrap(), "menos uno"); + assert_eq!(es.int_to_cardinal((-1_000_000).into()).unwrap(), "menos un millón"); assert_eq!( - es.to_int_cardinal((-1_020_010_000).into()).unwrap(), + es.int_to_cardinal((-1_020_010_000).into()).unwrap(), "menos un billón veinte millones diez mil" ); es.set_neg_flavour(BelowZero); - assert_eq!(es.to_int_cardinal((-1).into()).unwrap(), "uno bajo cero"); - assert_eq!(es.to_int_cardinal((-1_000_000).into()).unwrap(), "un millón bajo cero"); + assert_eq!(es.int_to_cardinal((-1).into()).unwrap(), "uno bajo cero"); + assert_eq!(es.int_to_cardinal((-1_000_000).into()).unwrap(), "un millón bajo cero"); assert_eq!( - es.to_int_cardinal((-1_020_010_000).into()).unwrap(), + es.int_to_cardinal((-1_020_010_000).into()).unwrap(), "un billón veinte millones diez mil bajo cero" ); } @@ -462,8 +587,8 @@ mod tests { for flavour in [Prepended, Appended, BelowZero] { es.set_neg_flavour(flavour); for value in VALUES.iter().cloned() { - let positive = es.to_int_cardinal(to(value).abs()).unwrap(); - let negative = es.to_int_cardinal(-to(value).abs()).unwrap(); + let positive = es.int_to_cardinal(to(value).abs()).unwrap(); + let negative = es.int_to_cardinal(-to(value).abs()).unwrap(); assert!( negative.contains(positive.as_str()), "{} !contains {}", diff --git a/src/main.rs b/src/main.rs index 073f1e7..cd22b7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,13 +4,18 @@ use num2words::lang::Spanish; use num_bigfloat::BigFloat; pub fn main() { let es = Spanish::default(); - println!("Resultado {:?}", es.to_int_cardinal(1_002_002_031.into())); - println!("Resultado {:?}", es.to_int_cardinal((-1_010_001_031).into())); - println!("Resultado {:?}", es.to_int_cardinal(1_001_021_031.into())); + println!("Resultado {:?}", es.to_cardinal(1_002_002_031.into())); + println!("Resultado {:?}", es.to_cardinal((-1_010_001_031).into())); + println!("Resultado {:?}", es.to_cardinal((1_001_021_031.512).into())); - let e = BigFloat::from(215.25f64); + let mut e = BigFloat::from(215.2512f64); // println!("{:?}\n{:?}\n{:?}", e, e.frac(), e.int()); - println!("\n\n{}\nfrac: {}\nint : {}\n\n", e, e.frac(), e.int()); + let mut frac = e.frac(); + e *= BigFloat::from(100); + frac *= BigFloat::from(10); + println!("\n\n{}\nfrac: {}\nint : {}\n", e, e.frac(), e.int()); + println!("{}\nfrac: {}\nint : {}\n\n", frac, frac, frac.int()); + println!("{}", e.frac().rem(&(10.into()))); println!("{}", e.frac().rem(&(100.into()))); // let mut input = String::new(); From 496f7a2a16060bef67a3f1807c5d80c8e97a80b3 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Mon, 1 Apr 2024 01:00:13 -0500 Subject: [PATCH 12/52] Tweak comments and error checking --- src/lang/es.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 040c13e..f698cd4 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -211,6 +211,7 @@ impl Spanish { #[inline(always)] // Converts Integer BigFloat to a vector of u64 fn en_miles(&self, mut num: BigFloat) -> Vec { + // Doesn't check if BigFloat is Integer only let mut thousands = Vec::new(); let mil = 1000.into(); num = num.abs(); @@ -224,6 +225,11 @@ impl Spanish { // Only should be called if you're sure the number has no fraction fn int_to_cardinal(&self, num: BigFloat) -> Result { + // Don't convert a number with fraction, NaN or Infinity + if !num.frac().is_zero() || num.is_nan() || num.is_inf() { + return Err(Num2Err::CannotConvert); + } + if num.is_zero() { return Ok(String::from("cero")); } @@ -309,8 +315,8 @@ impl Spanish { let mut words = vec![]; let is_negative = num.is_negative(); let num = num.abs(); - let integral_word = self.int_to_cardinal(num.int())?; - words.push(integral_word); + let positive_int_word = self.int_to_cardinal(num.int())?; + words.push(positive_int_word); let mut fraction_part = num.frac(); if !fraction_part.is_zero() { From 8de42c23eae396a80e637c55200c09fdf1ca74fa Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:15:45 -0500 Subject: [PATCH 13/52] Staging: to_ordinal implementation --- src/lang/es.rs | 226 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 179 insertions(+), 47 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index f698cd4..1ce503c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -5,6 +5,7 @@ use std::fmt::Display; use num_bigfloat::BigFloat; +use super::Language; use crate::Num2Err; // Reference that can hopefully be implemented seamlessly: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol const UNIDADES: [&str; 10] = @@ -108,6 +109,72 @@ const MILLAR: [&str; MILLAR_SIZE] = [ "novendecillón", "vigintillón", ]; +pub mod ordinal { + // Gender must be added at callsite + pub(super) const UNIDADES: [&str; 10] = + ["", "primer", "segund", "tercer", "cuart", "quint", "sext", "séptim", "octav", "noven"]; + pub(super) const DECENAS: [&str; 10] = [ + "", + "", // expects DIECIS to be called instead + "vigésimo", + "trigésimo", + "cuadragésimo", + "quincuagésimo", + "sexagésimo", + "septuagésimo", + "octogésimo", + "nonagésimo", + ]; + // Gender must be added at callsite + pub(super) const DIECIS: [&str; 10] = [ + "décim", + "undécim", // `decimoprimero` is a valid word + "duodécim", // `décimosegundo` is a valid word + "decimotercer", + "decimocuart", + "decimoquint", + "decimosext", + "decimoséptim", + "decimooctav", + "decimonoven", + ]; + pub(super) const CENTENAS: [&str; 10] = [ + "", + "centésimo", + "ducentésimo", + "tricentésimo", + "cuadringentésimo", + "quingentésimo", + "sexcentésimo", + "septingentésimo", + "octingentésimo", + "noningentésimo", + ]; + pub(super) const MILLARES: [&str; 22] = [ + "", + "milésimo", + "millonésimo", + "billonésimo", + "trillonésimo", + "cuatrillonésimo", + "quintillonésimo", + "sextillonésimo", + "septillonésimo", + "octillonésimo", + "nonillonésimo", + "decillonésimo", + "undecillonésimo", + "duodecillonésimo", + "tredecillonésimo", + "cuatrodecillonésimo", + "quindeciollonésimo", + "sexdecillonésimo", + "septendecillonésimo", + "octodecillonésimo", + "novendecillonésimo", + "vigintillonésimo", + ]; +} #[derive(Clone, Default, Debug, PartialEq, Eq)] pub struct Spanish { /// Negative flavour like "bajo cero", "menos", "negativo" @@ -115,50 +182,10 @@ pub struct Spanish { // Writes the number as "veintiocho" instead of "veinte y ocho" in case of true veinti: bool, decimal_char: DecimalChar, + // Gender for ordinal numbers + feminine: bool, } -// TODO: Remove Copy trait if enums can store data -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] -pub enum NegativeFlavour { - #[default] - Prepended, // -1 => menos uno - Appended, // -1 => uno negativo - BelowZero, // -1 => uno bajo cero -} -impl Display for NegativeFlavour { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - NegativeFlavour::Prepended => write!(f, "menos"), - NegativeFlavour::Appended => write!(f, "negativo"), - NegativeFlavour::BelowZero => write!(f, "bajo cero"), - } - } -} - -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] -pub enum DecimalChar { - #[default] - Punto, - Coma, -} - -impl DecimalChar { - #[inline(always)] - pub fn to_word(&self) -> &'static str { - match self { - DecimalChar::Punto => "punto", - DecimalChar::Coma => "coma", - } - } - - #[inline(always)] - pub fn to_char(self) -> char { - match self { - DecimalChar::Punto => '.', - DecimalChar::Coma => ',', - } - } -} impl Spanish { #[inline(always)] pub fn new() -> Self { @@ -198,6 +225,7 @@ impl Spanish { Self { decimal_char, ..self } } + #[inline(always)] pub fn to_cardinal(&self, num: BigFloat) -> Result { if num.is_inf() { self.inf_to_cardinal(&num) @@ -270,13 +298,13 @@ impl Spanish { // case `?_110` => `? ciento diez` 1 => words.push(String::from(DIECIS[units])), 2 if self.veinti && units != 0 => match units { - // TODO:add accent if you can not support ASCII and want to be grammatically - 1 if i != 0 => words.push(String::from("veintiun")), + // case `?_021` => `? veintiuno` + // case `021_...` => `? veintiún...` + 1 if i != 0 => words.push(String::from("veintiún")), _ => words.push(String::from("veinti") + unit_word), }, - // 2 if self.veinti && units == 1 => words.push(String::from("veintiun")), _ => { - // case `?_142 => `? cuarenta y dos` + // case `?_142 => `? ciento cuarenta y dos` let ten = DECENAS[tens]; words.push(match units { 0 => String::from(ten), @@ -354,7 +382,7 @@ impl Spanish { } } - // TODO: Refactor away if it only has a single callsite + #[inline(always)] fn flavourize_with_negative(&self, words: &mut Vec, flavour: NegativeFlavour) { use NegativeFlavour::*; let string = flavour.to_string(); @@ -365,7 +393,111 @@ impl Spanish { } } } +impl Language for Spanish { + fn to_cardinal(&self, num: BigFloat) -> Result { + self.to_cardinal(num) + } + fn to_ordinal(&self, num: BigFloat) -> Result { + // Important to keep so it doesn't conflict with the main module's constants + use ordinal::{CENTENAS, DECENAS, DIECIS, MILLARES, UNIDADES}; + if num.is_inf() || num.is_negative() || !num.frac().is_zero() { + return Err(Num2Err::CannotConvert); + } + let is_feminine = self.feminine; + let mut words = vec![]; + for (i, triplet) in self.en_miles(num.int()).into_iter().enumerate().rev() { + let hundreds = ((triplet / 100) % 10) as usize; + let tens = ((triplet / 10) % 10) as usize; + let units = (triplet % 10) as usize; + + if hundreds > 0 { + words.push(String::from(CENTENAS[hundreds])) + } + + if tens != 0 || units != 0 { + let gender = || -> &str { if is_feminine { "a" } else { "o" } }; + let unit_word = String::from(UNIDADES[units]); + + todo!("Finish the logic behind tens match statement"); + match tens { + // case `?_119` => `? ciento diecinueve` + // case `?_110` => `? ciento diez` + 1 => words.push(String::from(DIECIS[units]) + gender()), + _ => { + // case `?_142 => `? ciento cuarenta y dos` + let ten = DECENAS[tens]; + words.push(match units { + 0 => String::from(ten), + _ => format!("{ten} y {unit_word}"), + }); + } + } + } + + // Add the next Milliard if there's any. + if i != 0 && triplet != 0 { + if i > MILLARES.len() - 1 { + return Err(Num2Err::CannotConvert); + } + // Boolean that checks if next Milliard is plural + let plural = triplet != 1; + match plural { + false => words.push(String::from(MILLAR[i])), + true => words.push(String::from(MILLARES[i])), + } + } + } + + todo!() + } + + fn to_ordinal_num(&self, num: BigFloat) -> Result { + todo!() + } + + fn to_year(&self, num: BigFloat) -> Result { + todo!() + } + + fn to_currency(&self, num: BigFloat, currency: crate::Currency) -> Result { + todo!() + } +} +// TODO: Remove Copy trait if enums can store data +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] +pub enum NegativeFlavour { + #[default] + Prepended, // -1 => menos uno + Appended, // -1 => uno negativo + BelowZero, // -1 => uno bajo cero +} +impl Display for NegativeFlavour { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + NegativeFlavour::Prepended => write!(f, "menos"), + NegativeFlavour::Appended => write!(f, "negativo"), + NegativeFlavour::BelowZero => write!(f, "bajo cero"), + } + } +} + +#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] +pub enum DecimalChar { + #[default] + Punto, + Coma, +} + +impl DecimalChar { + #[inline(always)] + pub fn to_word(self) -> &'static str { + match self { + DecimalChar::Punto => "punto", + DecimalChar::Coma => "coma", + } + } +} #[cfg(test)] mod tests { use super::*; From c547a3ad2016758387ef35047b0fd01750169fff Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:56:33 +0000 Subject: [PATCH 14/52] defaults to using "veinti..." flavor instead of "veinte y..." --- src/lang/es.rs | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 1ce503c..955a90e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -179,8 +179,8 @@ pub mod ordinal { pub struct Spanish { /// Negative flavour like "bajo cero", "menos", "negativo" neg_flavour: NegativeFlavour, - // Writes the number as "veintiocho" instead of "veinte y ocho" in case of true - veinti: bool, + // Writes the number as "veinte y ocho" instead of "veintiocho" in case of true + prefer_veinte: bool, decimal_char: DecimalChar, // Gender for ordinal numbers feminine: bool, @@ -188,19 +188,19 @@ pub struct Spanish { impl Spanish { #[inline(always)] - pub fn new() -> Self { - Self::default() + pub fn new(decimal_char: DecimalChar, feminine: bool) -> Self { + Self { decimal_char, feminine, ..Default::default() } } #[inline(always)] - pub fn set_veinti(&mut self, veinti: bool) -> &mut Self { - self.veinti = veinti; + pub fn set_veinte(&mut self, veinte: bool) -> &mut Self { + self.prefer_veinte = veinte; self } #[inline(always)] - pub fn with_veinti(self, veinti: bool) -> Self { - Self { veinti, ..self } + pub fn with_veinte(self, veinte: bool) -> Self { + Self { prefer_veinte: veinte, ..self } } #[inline(always)] @@ -297,12 +297,17 @@ impl Spanish { // case `?_119` => `? ciento diecinueve` // case `?_110` => `? ciento diez` 1 => words.push(String::from(DIECIS[units])), - 2 if self.veinti && units != 0 => match units { - // case `?_021` => `? veintiuno` + 2 if self.prefer_veinte && units != 0 => { + let unit_word = if units == 1 && i != 0 { "un" } else { unit_word }; + words.push(format!("veinte y {unit_word}")); + } + 2 => words.push(match units { + 0 => String::from(DECENAS[tens]), // case `021_...` => `? veintiún...` - 1 if i != 0 => words.push(String::from("veintiún")), - _ => words.push(String::from("veinti") + unit_word), - }, + 1 if i != 0 => String::from("veintiún"), + // case `?_021` => `? veintiuno` + _ => format!("veinti{unit_word}"), + }), _ => { // case `?_142 => `? ciento cuarenta y dos` let ten = DECENAS[tens]; @@ -416,7 +421,13 @@ impl Language for Spanish { } if tens != 0 || units != 0 { - let gender = || -> &str { if is_feminine { "a" } else { "o" } }; + let gender = || -> &str { + if is_feminine { + "a" + } else { + "o" + } + }; let unit_word = String::from(UNIDADES[units]); todo!("Finish the logic behind tens match statement"); @@ -580,14 +591,14 @@ mod tests { "ciento setenta y un billones treinta y un millones cuarenta y un mil treinta y uno", ); assert!(!es.int_to_cardinal(to(171_031_041_031)).unwrap().contains(" un ")); - // with veinti flavour - let es = es.with_veinti(true); + // with veinte flavour + let es = es.with_veinte(true); assert_eq!( es.int_to_cardinal(to(21_021_321_021)).unwrap(), - "veintiun billones veintiun millones trescientos veintiun mil veintiuno" + "veinte y un billones veinte y un millones trescientos veinte y un mil veinte y uno" ); - assert_eq!(es.int_to_cardinal(to(22_000_000)).unwrap(), "veintidos millones"); + assert_eq!(es.int_to_cardinal(to(22_000_000)).unwrap(), "veinte y dos millones"); assert_eq!( es.int_to_cardinal(to(20_020_020)).unwrap(), "veinte millones veinte mil veinte" @@ -606,6 +617,7 @@ mod tests { es.to_cardinal(BigFloat::from(0.0123456789)).unwrap(), "cero coma cero uno dos tres cuatro cinco seis siete ocho nueve" ); + // Negative flavours use NegativeFlavour::{Appended, BelowZero, Prepended}; let es = es.with_neg_flavour(Appended); assert_eq!( @@ -672,7 +684,7 @@ mod tests { ); assert_eq!( es.int_to_cardinal(to(801_021_001)).unwrap(), - "ochocientos uno millones veinte y uno mil uno" + "ochocientos uno millones veintiún mil uno" ); assert_eq!(es.int_to_cardinal(to(1_000_000)).unwrap(), "un millón"); assert_eq!(es.int_to_cardinal(to(1_000_000_000)).unwrap(), "un billón"); From 8f51aa64a46980b256815026e6ff916ee4ed49b2 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Wed, 3 Apr 2024 02:01:34 +0000 Subject: [PATCH 15/52] Stage Changes: to_ordinal progress --- src/lang/es.rs | 149 ++++++++++++++++++++++++++++--------------------- src/main.rs | 18 +----- 2 files changed, 87 insertions(+), 80 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 955a90e..135f555 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -140,39 +140,39 @@ pub mod ordinal { ]; pub(super) const CENTENAS: [&str; 10] = [ "", - "centésimo", - "ducentésimo", - "tricentésimo", - "cuadringentésimo", - "quingentésimo", - "sexcentésimo", - "septingentésimo", - "octingentésimo", - "noningentésimo", + "centésim", + "ducentésim", + "tricentésim", + "cuadringentésim", + "quingentésim", + "sexcentésim", + "septingentésim", + "octingentésim", + "noningentésim", ]; pub(super) const MILLARES: [&str; 22] = [ "", - "milésimo", - "millonésimo", - "billonésimo", - "trillonésimo", - "cuatrillonésimo", - "quintillonésimo", - "sextillonésimo", - "septillonésimo", - "octillonésimo", - "nonillonésimo", - "decillonésimo", - "undecillonésimo", - "duodecillonésimo", - "tredecillonésimo", - "cuatrodecillonésimo", - "quindeciollonésimo", - "sexdecillonésimo", - "septendecillonésimo", - "octodecillonésimo", - "novendecillonésimo", - "vigintillonésimo", + "milésim", + "millonésim", + "billonésim", + "trillonésim", + "cuatrillonésim", + "quintillonésim", + "sextillonésim", + "septillonésim", + "octillonésim", + "nonillonésim", + "decillonésim", + "undecillonésim", + "duodecillonésim", + "tredecillonésim", + "cuatrodecillonésim", + "quindeciollonésim", + "sexdecillonésim", + "septendecillonésim", + "octodecillonésim", + "novendecillonésim", + "vigintillonésim", ]; } #[derive(Clone, Default, Debug, PartialEq, Eq)] @@ -184,6 +184,8 @@ pub struct Spanish { decimal_char: DecimalChar, // Gender for ordinal numbers feminine: bool, + // Plural for ordinal numbers + plural: bool, } impl Spanish { @@ -337,11 +339,7 @@ impl Spanish { self.flavourize_with_negative(&mut words, *flavour) } - Ok(words - .into_iter() - .filter_map(|word| (!word.is_empty()).then_some(word)) - .collect::>() - .join(" ")) + Ok(words.into_iter().filter(|word| !word.is_empty()).collect::>().join(" ")) } fn float_to_cardinal(&self, num: &BigFloat) -> Result { @@ -403,34 +401,47 @@ impl Language for Spanish { self.to_cardinal(num) } + /// Ordinal numbers above 10 are unnatural for Spanish speakers. Don't rely on these to convey meanings fn to_ordinal(&self, num: BigFloat) -> Result { // Important to keep so it doesn't conflict with the main module's constants use ordinal::{CENTENAS, DECENAS, DIECIS, MILLARES, UNIDADES}; - if num.is_inf() || num.is_negative() || !num.frac().is_zero() { - return Err(Num2Err::CannotConvert); + match (num.is_inf(), num.is_negative(), num.frac().is_zero()) { + (true, _, _) => return Err(Num2Err::InfiniteOrdinal), + (_, true, _) => return Err(Num2Err::NegativeOrdinal), + (_, _, false) => return Err(Num2Err::FloatingOrdinal), + _ => (), /* Nothing Happens */ } - let is_feminine = self.feminine; let mut words = vec![]; + + let gender = || -> &'static str { + if self.feminine { + "a" + } else { + "o" + } + }; for (i, triplet) in self.en_miles(num.int()).into_iter().enumerate().rev() { let hundreds = ((triplet / 100) % 10) as usize; let tens = ((triplet / 10) % 10) as usize; let units = (triplet % 10) as usize; if hundreds > 0 { - words.push(String::from(CENTENAS[hundreds])) + words.push(String::from(CENTENAS[hundreds]) + gender()) } if tens != 0 || units != 0 { - let gender = || -> &str { - if is_feminine { - "a" - } else { - "o" - } - }; let unit_word = String::from(UNIDADES[units]); + let unit_word = match (units, i) { + // case `1_100` => `milésimo centésimo` instead of `primero milésimo centésimo` + (_, 1) if triplet == 1 => "", + // case `001_001_100...` => `un billón un millón cien mil...` instead of + // `uno billón uno millón cien mil...` + // All `triplets == 1`` can can be named as "un". except for first or second + // triplet + (_, index) if index != 0 && triplet == 1 => "un", + _ => UNIDADES[units], + }; - todo!("Finish the logic behind tens match statement"); match tens { // case `?_119` => `? ciento diecinueve` // case `?_110` => `? ciento diez` @@ -438,10 +449,12 @@ impl Language for Spanish { _ => { // case `?_142 => `? ciento cuarenta y dos` let ten = DECENAS[tens]; - words.push(match units { - 0 => String::from(ten), - _ => format!("{ten} y {unit_word}"), - }); + let word = match units { + // case `?_120 => `? ciento cuarenta y dos` + 0 => String::from(ten.trim_end_matches('o')), + _ => format!("{ten} {unit_word}"), + }; + words.push(word + gender()); } } } @@ -451,20 +464,20 @@ impl Language for Spanish { if i > MILLARES.len() - 1 { return Err(Num2Err::CannotConvert); } - // Boolean that checks if next Milliard is plural - let plural = triplet != 1; - match plural { - false => words.push(String::from(MILLAR[i])), - true => words.push(String::from(MILLARES[i])), - } + words.push(String::from(MILLARES[i]) + gender()); } } - todo!() + if self.plural { + words.last_mut().map(|word| { + word.push_str("s"); + }); + } + Ok(words.into_iter().filter(|word| !word.is_empty()).collect::>().join(" ")) } fn to_ordinal_num(&self, num: BigFloat) -> Result { - todo!() + unimplemented!() } fn to_year(&self, num: BigFloat) -> Result { @@ -584,16 +597,22 @@ mod tests { es.int_to_cardinal(to(171_031_041_031)).unwrap(), "ciento setenta y uno billones treinta y uno millones cuarenta y uno mil treinta y uno" ); + } + #[test] + fn lang_es_un_is_for_single_unit() { // Triplets ending in 1 but higher than 30, is never "un" // consequently should never contain " un " as substring anywhere unless proven otherwise - assert_ne!( - es.int_to_cardinal(to(171_031_041_031)).unwrap(), - "ciento setenta y un billones treinta y un millones cuarenta y un mil treinta y uno", + let es = Spanish::default(); + assert_eq!( + es.int_to_cardinal(to(171_031_091_031)).unwrap(), + "ciento setenta y uno billones treinta y uno millones noventa y uno mil treinta y uno", ); - assert!(!es.int_to_cardinal(to(171_031_041_031)).unwrap().contains(" un ")); + assert!(!es.int_to_cardinal(to(171_031_091_031)).unwrap().contains(" un ")); + } + #[test] + fn lang_es_with_veinte_flavor() { // with veinte flavour - let es = es.with_veinte(true); - + let es = Spanish::default().with_veinte(true); assert_eq!( es.int_to_cardinal(to(21_021_321_021)).unwrap(), "veinte y un billones veinte y un millones trescientos veinte y un mil veinte y uno" diff --git a/src/main.rs b/src/main.rs index cd22b7a..9cc11a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,11 @@ use std::io::Write; -use num2words::lang::Spanish; +use num2words::lang::{Language, Spanish}; use num_bigfloat::BigFloat; pub fn main() { let es = Spanish::default(); - println!("Resultado {:?}", es.to_cardinal(1_002_002_031.into())); - println!("Resultado {:?}", es.to_cardinal((-1_010_001_031).into())); - println!("Resultado {:?}", es.to_cardinal((1_001_021_031.512).into())); - - let mut e = BigFloat::from(215.2512f64); - // println!("{:?}\n{:?}\n{:?}", e, e.frac(), e.int()); - let mut frac = e.frac(); - e *= BigFloat::from(100); - frac *= BigFloat::from(10); - println!("\n\n{}\nfrac: {}\nint : {}\n", e, e.frac(), e.int()); - println!("{}\nfrac: {}\nint : {}\n\n", frac, frac, frac.int()); - - println!("{}", e.frac().rem(&(10.into()))); - println!("{}", e.frac().rem(&(100.into()))); + let result = es.to_ordinal(BigFloat::from(1215)); + println!("{:?}", result); // let mut input = String::new(); // print!("\nIngrese un número para convertir a palabras\nIngrese `exit` para salir:\n\n"); // fn read_line(input: &mut String) { From ff4e5a1c4ab459a111c18f3fbe45d622d51fba8b Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Wed, 3 Apr 2024 00:42:12 -0500 Subject: [PATCH 16/52] Complete ordinal implementation --- src/lang/es.rs | 136 ++++++++++++++++++++++++++++++++----------------- src/main.rs | 18 +++++-- 2 files changed, 104 insertions(+), 50 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 135f555..794f8d8 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -1,5 +1,6 @@ #![allow(unused_imports)] // TODO: Remove this attribute use core::fmt::{self, Formatter}; +use std::borrow::BorrowMut; use std::convert::TryInto; use std::fmt::Display; @@ -116,14 +117,14 @@ pub mod ordinal { pub(super) const DECENAS: [&str; 10] = [ "", "", // expects DIECIS to be called instead - "vigésimo", - "trigésimo", - "cuadragésimo", - "quincuagésimo", - "sexagésimo", - "septuagésimo", - "octogésimo", - "nonagésimo", + "vigésim", + "trigésim", + "cuadragésim", + "quincuagésim", + "sexagésim", + "septuagésim", + "octogésim", + "nonagésim", ]; // Gender must be added at callsite pub(super) const DIECIS: [&str; 10] = [ @@ -194,6 +195,28 @@ impl Spanish { Self { decimal_char, feminine, ..Default::default() } } + #[inline(always)] + pub fn set_feminine(&mut self, feminine: bool) -> &mut Self { + self.feminine = feminine; + self + } + + #[inline(always)] + pub fn with_feminine(self, feminine: bool) -> Self { + Self { feminine, ..self } + } + + #[inline(always)] + pub fn set_plural(&mut self, plural: bool) -> &mut Self { + self.plural = plural; + self + } + + #[inline(always)] + pub fn with_plural(self, plural: bool) -> Self { + Self { plural, ..self } + } + #[inline(always)] pub fn set_veinte(&mut self, veinte: bool) -> &mut Self { self.prefer_veinte = veinte; @@ -245,7 +268,7 @@ impl Spanish { let mut thousands = Vec::new(); let mil = 1000.into(); num = num.abs(); - while !num.is_zero() { + while !num.int().is_zero() { // Insertar en Low Endian thousands.push((num % mil).to_u64().expect("triplet not under 1000")); num /= mil; // DivAssign @@ -401,7 +424,8 @@ impl Language for Spanish { self.to_cardinal(num) } - /// Ordinal numbers above 10 are unnatural for Spanish speakers. Don't rely on these to convey meanings + /// Ordinal numbers above 10 are unnatural for Spanish speakers. Don't rely on these to convey + /// meanings fn to_ordinal(&self, num: BigFloat) -> Result { // Important to keep so it doesn't conflict with the main module's constants use ordinal::{CENTENAS, DECENAS, DIECIS, MILLARES, UNIDADES}; @@ -412,53 +436,45 @@ impl Language for Spanish { _ => (), /* Nothing Happens */ } let mut words = vec![]; - - let gender = || -> &'static str { - if self.feminine { - "a" - } else { - "o" - } - }; - for (i, triplet) in self.en_miles(num.int()).into_iter().enumerate().rev() { + let gender = || -> &'static str { if self.feminine { "a" } else { "o" } }; + for (i, triplet) in self + .en_miles(num.int()) + .into_iter() + .enumerate() + .rev() + .filter(|(_, triplet)| *triplet != 0) + { let hundreds = ((triplet / 100) % 10) as usize; let tens = ((triplet / 10) % 10) as usize; let units = (triplet % 10) as usize; if hundreds > 0 { - words.push(String::from(CENTENAS[hundreds]) + gender()) + // case `500` => `quingentesim@` + words.push(String::from(CENTENAS[hundreds]) + gender()); } if tens != 0 || units != 0 { - let unit_word = String::from(UNIDADES[units]); - let unit_word = match (units, i) { - // case `1_100` => `milésimo centésimo` instead of `primero milésimo centésimo` - (_, 1) if triplet == 1 => "", - // case `001_001_100...` => `un billón un millón cien mil...` instead of - // `uno billón uno millón cien mil...` - // All `triplets == 1`` can can be named as "un". except for first or second - // triplet - (_, index) if index != 0 && triplet == 1 => "un", - _ => UNIDADES[units], - }; - + let unit_word = UNIDADES[units]; match tens { - // case `?_119` => `? ciento diecinueve` - // case `?_110` => `? ciento diez` + // case `?_001` => `? primer` + 0 if triplet == 1 && i > 0 => words.push(String::from("primer")), + 0 => words.push(String::from(unit_word) + gender()), + // case `?_119` => `? centésim@ decimonoven@` + // case `?_110` => `? centésim@ decim@` 1 => words.push(String::from(DIECIS[units]) + gender()), _ => { - // case `?_142 => `? ciento cuarenta y dos` let ten = DECENAS[tens]; let word = match units { - // case `?_120 => `? ciento cuarenta y dos` - 0 => String::from(ten.trim_end_matches('o')), - _ => format!("{ten} {unit_word}"), + // case `?_120 => `? centésim@ vigésim@` + 0 => String::from(ten), + // case `?_122 => `? centésim@ vigésim@ segund@` + _ => format!("{ten}{} {unit_word}", gender()), }; + words.push(word + gender()); } } } - // Add the next Milliard if there's any. if i != 0 && triplet != 0 { if i > MILLARES.len() - 1 { @@ -467,17 +483,25 @@ impl Language for Spanish { words.push(String::from(MILLARES[i]) + gender()); } } - - if self.plural { - words.last_mut().map(|word| { - word.push_str("s"); - }); + if !self.plural { + if let Some(word) = words.last_mut() { + word.push('s'); + } } Ok(words.into_iter().filter(|word| !word.is_empty()).collect::>().join(" ")) } fn to_ordinal_num(&self, num: BigFloat) -> Result { - unimplemented!() + match (num.is_inf(), num.is_negative(), num.frac().is_zero()) { + (true, _, _) => return Err(Num2Err::InfiniteOrdinal), + (_, true, _) => return Err(Num2Err::NegativeOrdinal), + (_, _, false) => return Err(Num2Err::FloatingOrdinal), + _ => (), /* Nothing Happens */ + } + + let mut word = num.to_i128().ok_or(Num2Err::CannotConvert)?.to_string(); + word.push(if self.feminine { 'ª' } else { 'º' }); + Ok(word) } fn to_year(&self, num: BigFloat) -> Result { @@ -542,7 +566,6 @@ mod tests { assert_eq!(es.int_to_cardinal(to(142)).unwrap(), "ciento cuarenta y dos"); assert_eq!(es.int_to_cardinal(to(800)).unwrap(), "ochocientos"); } - #[test] fn lang_es_thousands() { let es = Spanish::default(); @@ -570,7 +593,6 @@ mod tests { ); assert_eq!(es.int_to_cardinal(to(800_000)).unwrap(), "ochocientos mil"); } - #[test] fn lang_es_test_by_concept_to_cardinal_method() { // This might make other tests trivial @@ -624,6 +646,26 @@ mod tests { ); } #[test] + fn lang_es_ordinal() { + let es = Spanish::default().with_feminine(true).with_plural(true); + let ordinal = |num: i128| es.to_ordinal(to(num)).unwrap(); + assert_eq!(ordinal(1_101_001), "primer millonésima centésima primera milésima primera"); + assert_eq!(ordinal(2_001_022), "segunda millonésima primer milésima vigésima segunda"); + assert_eq!( + ordinal(12_114_011), + "duodécima millonésima centésima decimocuarta milésima undécima" + ); + assert_eq!( + ordinal(124_121_091), + "centésima vigésima cuarta millonésima centésima vigésima primera milésima nonagésima \ + primera" + ); + assert_eq!( + ordinal(124_001_091), + "centésima vigésima cuarta millonésima primer milésima nonagésima primera" + ); + } + #[test] fn lang_es_with_fraction() { use DecimalChar::{Coma, Punto}; let es = Spanish::default().with_decimal_char(Punto); diff --git a/src/main.rs b/src/main.rs index 9cc11a5..a02fbd2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,23 @@ -use std::io::Write; +use std::io::{stdin, Write}; use num2words::lang::{Language, Spanish}; use num_bigfloat::BigFloat; pub fn main() { let es = Spanish::default(); - let result = es.to_ordinal(BigFloat::from(1215)); - println!("{:?}", result); + let mut string = String::new(); + loop { + string.clear(); + stdin().read_line(&mut string).unwrap(); + let num = string.trim().parse::(); + if num.is_err() { + println!("Número inválido"); + continue; + } + let num = num.unwrap(); + let result = es.to_ordinal(BigFloat::from(num)); + pretty_print_int(num); + println!("\n{:?}", result); + } // let mut input = String::new(); // print!("\nIngrese un número para convertir a palabras\nIngrese `exit` para salir:\n\n"); // fn read_line(input: &mut String) { From 57c722cc7cb4cba14d5d5fc83d4c172dd52cf02a Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Wed, 3 Apr 2024 01:00:16 -0500 Subject: [PATCH 17/52] to_year implementation --- src/lang/es.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 794f8d8..8cef72a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -421,6 +421,9 @@ impl Spanish { } impl Language for Spanish { fn to_cardinal(&self, num: BigFloat) -> Result { + if num.is_nan() { + return Err(Num2Err::CannotConvert); + } self.to_cardinal(num) } @@ -433,6 +436,7 @@ impl Language for Spanish { (true, _, _) => return Err(Num2Err::InfiniteOrdinal), (_, true, _) => return Err(Num2Err::NegativeOrdinal), (_, _, false) => return Err(Num2Err::FloatingOrdinal), + _ if num.is_nan() => return Err(Num2Err::CannotConvert), _ => (), /* Nothing Happens */ } let mut words = vec![]; @@ -496,6 +500,7 @@ impl Language for Spanish { (true, _, _) => return Err(Num2Err::InfiniteOrdinal), (_, true, _) => return Err(Num2Err::NegativeOrdinal), (_, _, false) => return Err(Num2Err::FloatingOrdinal), + _ if num.is_nan() => return Err(Num2Err::CannotConvert), _ => (), /* Nothing Happens */ } @@ -505,7 +510,27 @@ impl Language for Spanish { } fn to_year(&self, num: BigFloat) -> Result { - todo!() + match (num.is_inf(), num.frac().is_zero(), num.int().is_zero()) { + (true, _, _) => return Err(Num2Err::InfiniteYear), + (_, false, _) => return Err(Num2Err::FloatingYear), + (_, _, true) => return Err(Num2Err::CannotConvert), // Year 0 is not a thing + _ if num.is_nan() => return Err(Num2Err::CannotConvert), + _ => (/* Nothing Happens */), + } + + let mut num = num; + + let suffix = if num.is_negative() { + num = num.inv_sign(); + " a. C." + } else { + "" + }; + + // Years in spanish are read the same as cardinal numbers....(?) + // src:https://twitter.com/RAEinforma/status/1761725275736334625?lang=en + let year_word = self.int_to_cardinal(num)?; + Ok(format!("{}{}", year_word, suffix)) } fn to_currency(&self, num: BigFloat, currency: crate::Currency) -> Result { From 3d9b15e23ddabdaf87728e48326da383a4ded9b3 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Thu, 4 Apr 2024 17:39:39 +0000 Subject: [PATCH 18/52] Expand tests --- src/lang/es.rs | 63 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 8cef72a..50c0ff4 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -433,10 +433,10 @@ impl Language for Spanish { // Important to keep so it doesn't conflict with the main module's constants use ordinal::{CENTENAS, DECENAS, DIECIS, MILLARES, UNIDADES}; match (num.is_inf(), num.is_negative(), num.frac().is_zero()) { + _ if num.is_nan() => return Err(Num2Err::CannotConvert), (true, _, _) => return Err(Num2Err::InfiniteOrdinal), (_, true, _) => return Err(Num2Err::NegativeOrdinal), (_, _, false) => return Err(Num2Err::FloatingOrdinal), - _ if num.is_nan() => return Err(Num2Err::CannotConvert), _ => (), /* Nothing Happens */ } let mut words = vec![]; @@ -497,10 +497,10 @@ impl Language for Spanish { fn to_ordinal_num(&self, num: BigFloat) -> Result { match (num.is_inf(), num.is_negative(), num.frac().is_zero()) { + _ if num.is_nan() => return Err(Num2Err::CannotConvert), (true, _, _) => return Err(Num2Err::InfiniteOrdinal), (_, true, _) => return Err(Num2Err::NegativeOrdinal), (_, _, false) => return Err(Num2Err::FloatingOrdinal), - _ if num.is_nan() => return Err(Num2Err::CannotConvert), _ => (), /* Nothing Happens */ } @@ -511,10 +511,10 @@ impl Language for Spanish { fn to_year(&self, num: BigFloat) -> Result { match (num.is_inf(), num.frac().is_zero(), num.int().is_zero()) { + _ if num.is_nan() => return Err(Num2Err::CannotConvert), (true, _, _) => return Err(Num2Err::InfiniteYear), (_, false, _) => return Err(Num2Err::FloatingYear), (_, _, true) => return Err(Num2Err::CannotConvert), // Year 0 is not a thing - _ if num.is_nan() => return Err(Num2Err::CannotConvert), _ => (/* Nothing Happens */), } @@ -575,8 +575,8 @@ impl DecimalChar { mod tests { use super::*; #[inline(always)] - fn to(input: i128) -> BigFloat { - BigFloat::from_i128(input) + fn to>(input: T) -> BigFloat { + BigFloat::from(input.into()) } #[test] fn lang_es_sub_thousands() { @@ -634,34 +634,77 @@ mod tests { // Thousand's milliard is plural assert_eq!(es.int_to_cardinal(to(2_100)).unwrap(), "dos mil cien"); // Cardinal number ending in 1 always ends with "uno" - assert!(es.int_to_cardinal(to(12_233_521_251)).unwrap().ends_with("uno")); + assert!(es.int_to_cardinal(to(12_233_521_251.0)).unwrap().ends_with("uno")); // triplet with value "10" assert_eq!(es.int_to_cardinal(to(110_010_000)).unwrap(), "ciento diez millones diez mil"); // Triplets ending in 1 but higher than 30, is "uno" // "un" is reserved for triplet == 1 in magnitudes higher than 10^3 like "un millón" // or "un trillón" assert_eq!( - es.int_to_cardinal(to(171_031_041_031)).unwrap(), + es.int_to_cardinal(to(171_031_041_031.0)).unwrap(), "ciento setenta y uno billones treinta y uno millones cuarenta y uno mil treinta y uno" ); } #[test] + fn lang_es_lang_trait_methods_fails_on() { + let es = Spanish::default(); + let to_cardinal = Language::to_cardinal; + assert_eq!(to_cardinal(&es, to(f64::NAN)).unwrap_err(), Num2Err::CannotConvert); + // Vigintillion supposedly has 63 zeroes, so anything beyond ~66 digits should fail with + // current impl + let some_big_num = BigFloat::from_u8(2).pow(&BigFloat::from_u8(230)); + assert_eq!(to_cardinal(&es, to(some_big_num)).unwrap_err(), Num2Err::CannotConvert); + + let to_ordinal = Language::to_ordinal; + assert_eq!(to_ordinal(&es, to(0.001)).unwrap_err(), Num2Err::FloatingOrdinal); + assert_eq!(to_ordinal(&es, to(-0.01)).unwrap_err(), Num2Err::NegativeOrdinal); + assert_eq!(to_ordinal(&es, to(f64::NAN)).unwrap_err(), Num2Err::CannotConvert); + assert_eq!(to_ordinal(&es, to(f64::INFINITY)).unwrap_err(), Num2Err::InfiniteOrdinal); + assert_eq!(to_ordinal(&es, to(f64::NEG_INFINITY)).unwrap_err(), Num2Err::InfiniteOrdinal); + + let to_ord_num = Language::to_ordinal_num; + assert_eq!(to_ord_num(&es, to(0.001)).unwrap_err(), Num2Err::FloatingOrdinal); + assert_eq!(to_ord_num(&es, to(-0.01)).unwrap_err(), Num2Err::NegativeOrdinal); + assert_eq!(to_ord_num(&es, to(f64::NAN)).unwrap_err(), Num2Err::CannotConvert); + assert_eq!(to_ord_num(&es, to(f64::INFINITY)).unwrap_err(), Num2Err::InfiniteOrdinal); + assert_eq!(to_ord_num(&es, to(f64::NEG_INFINITY)).unwrap_err(), Num2Err::InfiniteOrdinal); + + // Year is the same as cardinal. Except when negative, it is appended with " a. C." + let to_year = Language::to_year; + assert_eq!(to_year(&es, to(0.001)).unwrap_err(), Num2Err::FloatingYear); + assert_eq!(to_year(&es, to(f64::INFINITY)).unwrap_err(), Num2Err::InfiniteYear); + assert_eq!(to_year(&es, to(f64::NEG_INFINITY)).unwrap_err(), Num2Err::InfiniteYear); + assert_eq!(to_year(&es, to(f64::NAN)).unwrap_err(), Num2Err::CannotConvert); + assert_eq!(to_year(&es, to(0)).unwrap_err(), Num2Err::CannotConvert); // Year 0 is not a thing afaik + } + #[test] + fn lang_es_year_is_similar_to_cardinal() { + let es = Spanish::default(); + + assert_eq!(es.to_year(to(2021)).unwrap(), "dos mil veintiuno"); + assert_eq!(es.to_year(to(-2021)).unwrap(), "dos mil veintiuno a. C."); + let two = BigFloat::from(2); + for num in (3u64..).take(60).map(|num| two.pow(&to(num))) { + assert_eq!(es.to_year(num).unwrap(), es.to_cardinal(num).unwrap()) + } + } + #[test] fn lang_es_un_is_for_single_unit() { // Triplets ending in 1 but higher than 30, is never "un" // consequently should never contain " un " as substring anywhere unless proven otherwise let es = Spanish::default(); assert_eq!( - es.int_to_cardinal(to(171_031_091_031)).unwrap(), + es.int_to_cardinal(to(171_031_091_031.0)).unwrap(), "ciento setenta y uno billones treinta y uno millones noventa y uno mil treinta y uno", ); - assert!(!es.int_to_cardinal(to(171_031_091_031)).unwrap().contains(" un ")); + assert!(!es.int_to_cardinal(to(171_031_091_031.0)).unwrap().contains(" un ")); } #[test] fn lang_es_with_veinte_flavor() { // with veinte flavour let es = Spanish::default().with_veinte(true); assert_eq!( - es.int_to_cardinal(to(21_021_321_021)).unwrap(), + es.int_to_cardinal(to(21_021_321_021.0)).unwrap(), "veinte y un billones veinte y un millones trescientos veinte y un mil veinte y uno" ); assert_eq!(es.int_to_cardinal(to(22_000_000)).unwrap(), "veinte y dos millones"); From 04d2e01e9e4e24198f763dbb259da8bd8c5171fe Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Thu, 4 Apr 2024 17:40:14 +0000 Subject: [PATCH 19/52] update container settings --- .devcontainer/devcontainer.json | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d0fb2ef..0caf314 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,6 @@ "extensions": [ "cschleiden.vscode-github-actions", "ms-vsliveshare.vsliveshare", - "matklad.rust-analyzer", "serayuzgur.crates", "vadimcn.vscode-lldb", @@ -22,12 +21,36 @@ "editor.formatOnSave": true, "editor.inlayHints.enabled": "offUnlessPressed", "terminal.integrated.shell.linux": "/usr/bin/zsh", + "rust-analyzer.rustfmt.extraArgs": [ + "+nightly" // I personally love nightly rustfmt + ], "files.exclude": { "**/CODE_OF_CONDUCT.md": true, "**/LICENSE": true } - } + }, + "keybindings": // Place your key bindings in this file to override the defaults + [ + { + "key": "alt+q", + "command": "workbench.action.openQuickChat.copilot" + }, + { + "key": "alt+a", + "command": "github.copilot.ghpr.applySuggestion" + }, + { + "key": "alt+`", + "command": "editor.action.showHover", + "when": "editorTextFocus" + }, + { + "key": "ctrl+k ctrl+i", + "command": "-editor.action.showHover", + "when": "editorTextFocus" + } + ] } }, "dockerFile": "Dockerfile" -} +} \ No newline at end of file From 5d0a21fec5d2c64e134a147b11f529b216d7ba41 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:59:14 +0000 Subject: [PATCH 20/52] Currency Implementation --- src/lang/es.rs | 76 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 50c0ff4..334409a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -399,12 +399,14 @@ impl Spanish { } else if num.is_inf_pos() { Ok(String::from("infinito")) } else { - Ok(match self.neg_flavour { - NegativeFlavour::Prepended => String::from("menos infinito"), - NegativeFlavour::Appended => String::from("infinito negativo"), - // Defaults to menos because it doesn't make sense to call `infinito bajo cero` - NegativeFlavour::BelowZero => String::from("menos infinito"), - }) + let word = match self.neg_flavour { + NegativeFlavour::Prepended => "{} infinito", + NegativeFlavour::Appended => "infinito {}", + // Defaults to `menos` because it doesn't make sense to call `infinito bajo cero` + NegativeFlavour::BelowZero => "menos infinito", + } + .replace("{}", self.neg_flavour.as_str()); + Ok(word) } } @@ -534,7 +536,34 @@ impl Language for Spanish { } fn to_currency(&self, num: BigFloat, currency: crate::Currency) -> Result { - todo!() + if num.is_nan() { + Err(Num2Err::CannotConvert) + } else if num.is_inf() { + let currency = currency.default_string(true); + let inf = self.inf_to_cardinal(&num)? + "de {}"; + let word = inf.replace("{}", ¤cy); + return Ok(word); + } else if num.frac().is_zero() { + let is_plural = num.int() != 1.into(); + let currency = currency.default_string(is_plural); + let cardinal = self.int_to_cardinal(num)?; + return Ok(format!("{cardinal} {currency}")); + } else { + let hundred: BigFloat = 100.into(); + let (integral, cents) = (num.int(), num.mul(&hundred).int().rem(&hundred)); + let (int_words, cent_words) = + (self.to_currency(integral, currency)?, self.int_to_cardinal(cents)?); + let cents_is_plural = cents != 1.into(); + let cents_suffix = currency.default_subunit_string("centavo{}", cents_is_plural); + + if cents.is_zero() { + return Ok(int_words); + } else if integral.is_zero() { + return Ok(format!("{cent_words} {cents_suffix}")); + } else { + return Ok(format!("{} con {} {cents_suffix}", int_words, cent_words)); + } + } } } // TODO: Remove Copy trait if enums can store data @@ -545,15 +574,20 @@ pub enum NegativeFlavour { Appended, // -1 => uno negativo BelowZero, // -1 => uno bajo cero } -impl Display for NegativeFlavour { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { +impl NegativeFlavour { + pub fn as_str(&self) -> &'static str { match self { - NegativeFlavour::Prepended => write!(f, "menos"), - NegativeFlavour::Appended => write!(f, "negativo"), - NegativeFlavour::BelowZero => write!(f, "bajo cero"), + NegativeFlavour::Prepended => "menos", + NegativeFlavour::Appended => "negativo", + NegativeFlavour::BelowZero => "bajo cero", } } } +impl Display for NegativeFlavour { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{str}", str = self.as_str()) + } +} #[derive(Default, Clone, Copy, Debug, PartialEq, Eq)] pub enum DecimalChar { @@ -576,8 +610,9 @@ mod tests { use super::*; #[inline(always)] fn to>(input: T) -> BigFloat { - BigFloat::from(input.into()) + input.into() } + #[test] fn lang_es_sub_thousands() { let es = Spanish::default(); @@ -591,6 +626,7 @@ mod tests { assert_eq!(es.int_to_cardinal(to(142)).unwrap(), "ciento cuarenta y dos"); assert_eq!(es.int_to_cardinal(to(800)).unwrap(), "ochocientos"); } + #[test] fn lang_es_thousands() { let es = Spanish::default(); @@ -618,6 +654,7 @@ mod tests { ); assert_eq!(es.int_to_cardinal(to(800_000)).unwrap(), "ochocientos mil"); } + #[test] fn lang_es_test_by_concept_to_cardinal_method() { // This might make other tests trivial @@ -645,6 +682,7 @@ mod tests { "ciento setenta y uno billones treinta y uno millones cuarenta y uno mil treinta y uno" ); } + #[test] fn lang_es_lang_trait_methods_fails_on() { let es = Spanish::default(); @@ -677,6 +715,7 @@ mod tests { assert_eq!(to_year(&es, to(f64::NAN)).unwrap_err(), Num2Err::CannotConvert); assert_eq!(to_year(&es, to(0)).unwrap_err(), Num2Err::CannotConvert); // Year 0 is not a thing afaik } + #[test] fn lang_es_year_is_similar_to_cardinal() { let es = Spanish::default(); @@ -688,6 +727,7 @@ mod tests { assert_eq!(es.to_year(num).unwrap(), es.to_cardinal(num).unwrap()) } } + #[test] fn lang_es_un_is_for_single_unit() { // Triplets ending in 1 but higher than 30, is never "un" @@ -713,6 +753,7 @@ mod tests { "veinte millones veinte mil veinte" ); } + #[test] fn lang_es_ordinal() { let es = Spanish::default().with_feminine(true).with_plural(true); @@ -733,6 +774,7 @@ mod tests { "centésima vigésima cuarta millonésima primer milésima nonagésima primera" ); } + #[test] fn lang_es_with_fraction() { use DecimalChar::{Coma, Punto}; @@ -764,6 +806,7 @@ mod tests { "cero coma cero uno dos tres cuatro cinco seis siete ocho nueve bajo cero" ); } + #[test] fn lang_es_infinity_and_negatives() { use NegativeFlavour::*; @@ -788,6 +831,7 @@ mod tests { } } } + #[test] fn lang_es_millions() { let es = Spanish::default(); @@ -858,6 +902,7 @@ mod tests { "un billón veinte millones diez mil bajo cero" ); } + #[test] fn lang_es_positive_is_just_a_substring_of_negative_in_cardinal() { const VALUES: [i128; 3] = [-1, -1_000_000, -1_020_010_000]; @@ -877,9 +922,4 @@ mod tests { } } } - - #[test] - fn lang_es_() { - // unimplemented!() - } } From fad8cebeb68187d7c395e78e1deca500bf073598 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sat, 6 Apr 2024 09:56:28 -0500 Subject: [PATCH 21/52] Add Spanish implementation to lang::Language trait --- src/lang/es.rs | 31 ++++++++++++++++++++++++++++++- src/lang/lang.rs | 36 +++++++++++++++++++++++++++++++++--- src/lang/mod.rs | 2 +- src/main.rs | 14 ++++++++++++++ 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 334409a..bfd410c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -3,6 +3,8 @@ use core::fmt::{self, Formatter}; use std::borrow::BorrowMut; use std::convert::TryInto; use std::fmt::Display; +use std::ops::Neg; +use std::str::FromStr; use num_bigfloat::BigFloat; @@ -583,6 +585,21 @@ impl NegativeFlavour { } } } +impl FromStr for NegativeFlavour { + type Err = (); + + fn from_str(s: &str) -> Result { + let result = match s { + "menos" => NegativeFlavour::Prepended, + "negativo" => NegativeFlavour::Appended, + "bajo cero" => NegativeFlavour::BelowZero, + _ => return Err(()), + }; + debug_assert!(result.as_str() == s, "NegativeFlavour::from_str() is incorrect"); + Ok(result) + } +} + impl Display for NegativeFlavour { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "{str}", str = self.as_str()) @@ -595,7 +612,19 @@ pub enum DecimalChar { Punto, Coma, } - +impl FromStr for DecimalChar { + type Err = (); + + fn from_str(s: &str) -> Result { + let result = match s { + "punto" => DecimalChar::Punto, + "coma" => DecimalChar::Coma, + _ => return Err(()), + }; + debug_assert!(result.to_word() == s, "DecimalChar::from_str() is incorrect"); + Ok(result) + } +} impl DecimalChar { #[inline(always)] pub fn to_word(self) -> &'static str { diff --git a/src/lang/lang.rs b/src/lang/lang.rs index 57abb00..47d152f 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -1,3 +1,5 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(clippy::search_is_some)] use crate::lang; use crate::num2words::Num2Err; use crate::Currency; @@ -51,8 +53,14 @@ pub enum Lang { /// ); /// ``` French_CH, - // //TODO: add spanish parity - // Spanish, + /// ``` + /// use num2words::{Num2Words, Lang}; + /// assert_eq!( + /// Num2Words::new(42).lang(Lang::Spanish).to_words(), + /// Ok(String::from("cuarenta y dos")) + /// ); + /// ``` + Spanish, /// ``` /// use num2words::{Num2Words, Lang}; /// assert_eq!( @@ -78,6 +86,7 @@ impl FromStr for Lang { fn from_str(input: &str) -> Result { match input { "en" => Ok(Self::English), + "es" => Ok(Self::Spanish), "fr" => Ok(Self::French), "fr_BE" => Ok(Self::French_BE), "fr_CH" => Ok(Self::French_CH), @@ -139,7 +148,28 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::CH)) - } + }, + Lang::Spanish => { + use crate::lang::es::DecimalChar; + use super::es::NegativeFlavour; + let neg_flavour = preferences + .iter() + .find_map(|v| NegativeFlavour::from_str(v).ok()).unwrap_or_default(); + let prefer_veinte = preferences + .iter() + .any(|v| ["veinte"].binary_search(&v.as_str()).is_ok()); + let decimal_char = preferences + .iter() + .find_map(|v| DecimalChar::from_str(v).ok()).unwrap_or_default(); + let feminine = preferences + .iter() + .any(|v| ["f", "femenino", "feminine"].binary_search(&v.as_str()).is_ok()); + let plural = preferences + .iter() + .any(|v| ["plural"].binary_search(&v.as_str()).is_ok()); + let lang = lang::Spanish::new(decimal_char, feminine).with_plural(plural).with_veinte(prefer_veinte).with_neg_flavour(neg_flavour); + Box::new(lang) + }, Lang::Ukrainian => { let declension: lang::uk::Declension = preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); diff --git a/src/lang/mod.rs b/src/lang/mod.rs index 4ddd6a3..3f43732 100644 --- a/src/lang/mod.rs +++ b/src/lang/mod.rs @@ -1,4 +1,4 @@ -#[rustfmt::skip] // TODO: Remove attribute before final merge +#![cfg_attr(rustfmt, rustfmt_skip)] // TODO: Remove attribute before final merge mod lang; mod en; mod es; diff --git a/src/main.rs b/src/main.rs index a02fbd2..a507ed5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,20 @@ use num_bigfloat::BigFloat; pub fn main() { let es = Spanish::default(); let mut string = String::new(); + // Sort slice + let mut slice = ["bajo cero", "negativo", "menos"]; + slice.sort(); + println!("{:?}", slice); + let feminine = ["f", "femi", "feminino"].iter().find(|preference| { + let result = slice.binary_search(preference); + println!("{:?} := {preference:?}", result); + false + }); + println!("{:?}", feminine); + /* { + let found = slice.binary_search(preference); + println!("{:?}", found); + } */ loop { string.clear(); stdin().read_line(&mut string).unwrap(); From 66c56aaa45d6e2e8438bb2e8da97b249f3565144 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sat, 6 Apr 2024 10:05:42 -0500 Subject: [PATCH 22/52] try to ensure enum integrity safety --- src/lang/es.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lang/es.rs b/src/lang/es.rs index bfd410c..90989fd 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -636,12 +636,29 @@ impl DecimalChar { } #[cfg(test)] mod tests { + use core::panic; + use super::*; #[inline(always)] fn to>(input: T) -> BigFloat { input.into() } + #[test] + fn decimal_char_enum_integrity() { + // Test if the enum can be converted to string and back + assert_eq!(DecimalChar::from_str("punto").unwrap(), DecimalChar::Punto); + assert_eq!(DecimalChar::from_str("coma").unwrap(), DecimalChar::Coma); + } + + #[test] + fn negative_flavour_enum_integrity() { + // Test if the enum can be converted to string and back + assert_eq!(NegativeFlavour::from_str("menos").unwrap(), NegativeFlavour::Prepended); + assert_eq!(NegativeFlavour::from_str("negativo").unwrap(), NegativeFlavour::Appended); + assert_eq!(NegativeFlavour::from_str("bajo cero").unwrap(), NegativeFlavour::BelowZero); + } + #[test] fn lang_es_sub_thousands() { let es = Spanish::default(); From b11912e1cf6798a17f8f0191eadd3c16653149e6 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 13:43:10 +0000 Subject: [PATCH 23/52] add rustfmt_skip --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 8f6ec14..592da07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] #![crate_type = "lib"] #![crate_name = "num2words"] From a057344183cfd35513317a34eb58627cda2e0edd Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 09:13:51 -0500 Subject: [PATCH 24/52] derive basic traits for Lang enum The Lang enum should be really small since it doesn't contain any inherent data. So it should be dirt cheap to copy --- src/lang/lang.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lang/lang.rs b/src/lang/lang.rs index 47d152f..8e4fe9a 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -17,6 +17,7 @@ pub trait Language { /// Languages available in `num2words` #[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy)] pub enum Lang { /// ``` /// use num2words::{Num2Words, Lang}; From 0c62c2cb0695c0d78ef6c64b0ff455cd32a6698e Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:00:33 +0000 Subject: [PATCH 25/52] Add more str parsing and invert bool logic --- src/lang/es.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 90989fd..fd2f02d 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -491,7 +491,7 @@ impl Language for Spanish { words.push(String::from(MILLARES[i]) + gender()); } } - if !self.plural { + if self.plural { if let Some(word) = words.last_mut() { word.push('s'); } @@ -590,12 +590,13 @@ impl FromStr for NegativeFlavour { fn from_str(s: &str) -> Result { let result = match s { + "prepended" => NegativeFlavour::Prepended, + "appended" => NegativeFlavour::Appended, "menos" => NegativeFlavour::Prepended, "negativo" => NegativeFlavour::Appended, "bajo cero" => NegativeFlavour::BelowZero, _ => return Err(()), }; - debug_assert!(result.as_str() == s, "NegativeFlavour::from_str() is incorrect"); Ok(result) } } @@ -802,7 +803,7 @@ mod tests { #[test] fn lang_es_ordinal() { - let es = Spanish::default().with_feminine(true).with_plural(true); + let es = Spanish::default().with_feminine(true); let ordinal = |num: i128| es.to_ordinal(to(num)).unwrap(); assert_eq!(ordinal(1_101_001), "primer millonésima centésima primera milésima primera"); assert_eq!(ordinal(2_001_022), "segunda millonésima primer milésima vigésima segunda"); @@ -815,9 +816,11 @@ mod tests { "centésima vigésima cuarta millonésima centésima vigésima primera milésima nonagésima \ primera" ); + let es = Spanish::default().with_plural(true); + let ordinal = |num: i128| es.to_ordinal(to(num)).unwrap(); assert_eq!( ordinal(124_001_091), - "centésima vigésima cuarta millonésima primer milésima nonagésima primera" + "centésimo vigésimo cuarto millonésimo primer milésimo nonagésimo primeros" ); } From 52024f434d5dba8abbd49bc0544e241cb9552b0f Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 16:01:36 +0000 Subject: [PATCH 26/52] add Integration Test for spanish --- tests/lang_es_test.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/lang_es_test.rs diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs new file mode 100644 index 0000000..9898fe1 --- /dev/null +++ b/tests/lang_es_test.rs @@ -0,0 +1,41 @@ +use num2words::lang::to_language; +use num2words::Lang; +use num_bigfloat::BigFloat; + +#[test] +fn test_lang_es() { + let prefs_basics: Vec = + vec!["negativo" /* , "veinte", "menos", "prepended", "appended", "bajo cero" */] + .into_iter() + .map(String::from) + .collect(); + let prefs_for_ordinals: Vec = + vec!["femenino", /* "f", "feminine", */ "plural"].into_iter().map(String::from).collect(); + let prefs_for_decimal_char: Vec = vec!["coma"].into_iter().map(String::from).collect(); + + let driver = to_language( + Lang::Spanish, + prefs_basics.iter().chain(&prefs_for_decimal_char).cloned().collect(), + ); + let word = driver.to_cardinal(BigFloat::from(-821_442_524.69)).unwrap(); + #[rustfmt::skip] + assert_eq!(word, "ochocientos veintiún millones cuatrocientos cuarenta y dos mil quinientos veinticuatro coma seis nueve negativo"); + let word = driver.to_ordinal(BigFloat::from(-484)); + assert!(word.is_err()); // You can't get the ordinal of a negative number + + let driver = to_language(Lang::Spanish, prefs_for_ordinals.clone()); + assert_eq!(driver.to_ordinal(14.into()).unwrap(), "decimocuartas"); + assert_eq!(driver.to_ordinal(1.into()).unwrap(), "primeras"); + assert_eq!(driver.to_ordinal(2.into()).unwrap(), "segundas"); + + let driver = to_language(Lang::Spanish, vec![]); + assert_eq!( + driver.to_ordinal(141_100_211_021u64.into()).unwrap(), + "centésimo cuadragésimo primero billonésimo centésimo millonésimo ducentésimo undécimo \ + milésimo vigésimo primero" + ); + assert_eq!(driver.to_ordinal(14.into()).unwrap(), "decimocuarto"); + assert_eq!(driver.to_ordinal(1.into()).unwrap(), "primero"); + assert_eq!(driver.to_ordinal(2.into()).unwrap(), "segundo"); + assert_eq!(driver.to_ordinal(3.into()).unwrap(), "tercero"); +} From 1fd78168dd2d130952c8e207dc371b3835aede3d Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:02:39 +0000 Subject: [PATCH 27/52] Update Docs format and info --- src/lib.rs | 1 + src/num2words.rs | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 592da07..f85efdb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,7 @@ * | 🇧🇪🇨🇩 | `Lang::French_BE` | `fr_BE` | French (BE) | quarante-deux | * | 🇨🇭 | `Lang::French_CH` | `fr_CH` | French (CH) | quarante-deux | * | 🇺🇦 | `Lang::Ukrainian` | `uk` | Ukrainian | сорок два | + * | 🇪🇸 | `Lang::Spanish` | `es` | Spanish | cuarenta y dos| * * This list can be expanded! Contributions are welcomed. * diff --git a/src/num2words.rs b/src/num2words.rs index dd19a10..4cf523e 100644 --- a/src/num2words.rs +++ b/src/num2words.rs @@ -1,3 +1,4 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] use crate::{lang, Currency, Lang, Output}; use num_bigfloat::BigFloat; @@ -255,19 +256,26 @@ impl Num2Words { /// Adds a preference parameter /// /// # English language accepts: - /// oh and/or nil as replacements for "zero" + /// * oh and/or nil as replacements for "zero" /// /// # French language accepts: - /// feminine/f/féminin/feminin + /// * feminine/f/féminin/feminin /// - /// reformed/1990/rectifié/rectification + /// * reformed/1990/rectifié/rectification + /// + /// # Spanish language accepts: + /// * negativo/menos/bajo cero/prepended/appended + /// * veinte + /// * coma/punto + /// * f/femenino/feminine + /// * plural /// /// # Ukrainian language supports grammatical categories (bold - default): - /// Number: **singular/sing/однина/од**, plural/pl/множина/мн + /// * Number: **singular/sing/однина/од**, plural/pl/множина/мн /// - /// Gender: **masculine/m/чоловічий/чол/ч**, feminine/f/жіночий/жін/ж, neuter/n/середній/сер/с + /// * Gender: **masculine/m/чоловічий/чол/ч**, feminine/f/жіночий/жін/ж, neuter/n/середній/сер/с /// - /// Declension: **nominative/nom/називний/н**, genitive/gen/родовий/р, dative/dat/давальний/д, + /// * Declension: **nominative/nom/називний/н**, genitive/gen/родовий/р, dative/dat/давальний/д,\ /// accusative/acc/знахідний/з, instrumental/inc/орудний/о, locative/loc/місцевий/м /// /// Examples: @@ -282,6 +290,10 @@ impl Num2Words { /// Ok(String::from("cent-soixante-et-une")) /// ); /// assert_eq!( + /// Num2Words::new(122.04).lang(Lang::Spanish).prefer("coma").prefer("veinte").to_words(), + /// Ok(String::from("ciento veinte y dos coma cero cuatro")) + /// ); + /// assert_eq!( /// Num2Words::new(51).lang(Lang::Ukrainian).prefer("орудний").to_words(), /// Ok(String::from("пʼятдесятьма одним")) /// ); From 6d5e5052db2f133ca4d73e7432abf495498db132 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 18:03:47 +0000 Subject: [PATCH 28/52] add Spanish Docs and fix currency logic --- src/lang/es.rs | 96 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 16 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index fd2f02d..9918ed0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -252,17 +252,6 @@ impl Spanish { Self { decimal_char, ..self } } - #[inline(always)] - pub fn to_cardinal(&self, num: BigFloat) -> Result { - if num.is_inf() { - self.inf_to_cardinal(&num) - } else if num.frac().is_zero() { - self.int_to_cardinal(num) - } else { - self.float_to_cardinal(&num) - } - } - #[inline(always)] // Converts Integer BigFloat to a vector of u64 fn en_miles(&self, mut num: BigFloat) -> Vec { @@ -424,15 +413,40 @@ impl Spanish { } } impl Language for Spanish { + /// Converts a BigFloat to a cardinal number in Spanish + /// ```rust + /// use num2words::lang::{to_language, Lang, Language}; + /// use num_bigfloat::BigFloat; + /// + /// let es = to_language(Lang::Spanish, vec!["negativo".to_string()]); + /// let words = es.to_cardinal(BigFloat::from(-123456.789)).unwrap(); + /// assert_eq!( + /// words, + /// "ciento veintitres mil cuatrocientos cincuenta y seis punto siete ocho nueve negativo" + /// ); + /// ``` fn to_cardinal(&self, num: BigFloat) -> Result { if num.is_nan() { return Err(Num2Err::CannotConvert); + } else if num.is_inf() { + self.inf_to_cardinal(&num) + } else if num.frac().is_zero() { + self.int_to_cardinal(num) + } else { + self.float_to_cardinal(&num) } - self.to_cardinal(num) } /// Ordinal numbers above 10 are unnatural for Spanish speakers. Don't rely on these to convey /// meanings + /// ```rust + /// use num2words::lang::{to_language, Lang, Language}; + /// use num_bigfloat::BigFloat; + /// + /// let es = to_language(Lang::Spanish, vec![]); + /// let words = es.to_ordinal(BigFloat::from(11)).unwrap(); + /// assert_eq!(words, "undécimo"); + /// ``` fn to_ordinal(&self, num: BigFloat) -> Result { // Important to keep so it doesn't conflict with the main module's constants use ordinal::{CENTENAS, DECENAS, DIECIS, MILLARES, UNIDADES}; @@ -499,6 +513,19 @@ impl Language for Spanish { Ok(words.into_iter().filter(|word| !word.is_empty()).collect::>().join(" ")) } + /// A numeric number which has a `ª` or `º` appended at the end + /// ```rust + /// use num2words::lang::{to_language, Lang, Language}; + /// use num_bigfloat::BigFloat; + /// + /// let num = BigFloat::from(8); + /// + /// let es_male = to_language(Lang::Spanish, vec![]); + /// assert_eq!(es_male.to_ordinal_num(num).unwrap(), "8º"); + /// + /// let es_female = to_language(Lang::Spanish, vec!["feminine".to_string()]); + /// assert_eq!(es_female.to_ordinal_num(num).unwrap(), "8ª"); + /// ``` fn to_ordinal_num(&self, num: BigFloat) -> Result { match (num.is_inf(), num.is_negative(), num.frac().is_zero()) { _ if num.is_nan() => return Err(Num2Err::CannotConvert), @@ -513,6 +540,18 @@ impl Language for Spanish { Ok(word) } + /// A year is just a Cardinal number. When the BigFloat input is negative, it appends "a.C." to + /// the positive Cardinal representation + /// ```rust + /// use num2words::lang::{to_language, Lang, Language}; + /// use num_bigfloat::BigFloat; + /// + /// let num = BigFloat::from(2021); + /// let es = to_language(Lang::Spanish, vec![]); + /// + /// assert_eq!(es.to_year(num).unwrap(), "dos mil veintiuno"); + /// assert_eq!(es.to_year(-num).unwrap(), "dos mil veintiuno a. C."); + /// ``` fn to_year(&self, num: BigFloat) -> Result { match (num.is_inf(), num.frac().is_zero(), num.int().is_zero()) { _ if num.is_nan() => return Err(Num2Err::CannotConvert), @@ -537,6 +576,26 @@ impl Language for Spanish { Ok(format!("{}{}", year_word, suffix)) } + /// A Cardinal number which then the currency word representation is appended at the end. + /// `1` is the only exception to the rule. + /// The extra decimals are truncated instead of rounded + /// ```rust + /// use num2words::lang::{to_language, Lang, Language}; + /// use num2words::Currency; + /// use num_bigfloat::BigFloat; + /// + /// let es = to_language(Lang::Spanish, vec![]); + /// + /// assert_eq!( + /// es.to_currency(BigFloat::from(-2021), Currency::USD).unwrap(), + /// "menos dos mil veintiuno US dollars" + /// ); + /// assert_eq!( + /// es.to_currency(BigFloat::from(1.01), Currency::USD).unwrap(), + /// "un US dollar con un centavo" + /// ); + /// assert_eq!(es.to_currency(BigFloat::from(1), Currency::USD).unwrap(), "un US dollar"); + /// ``` fn to_currency(&self, num: BigFloat, currency: crate::Currency) -> Result { if num.is_nan() { Err(Num2Err::CannotConvert) @@ -548,14 +607,19 @@ impl Language for Spanish { } else if num.frac().is_zero() { let is_plural = num.int() != 1.into(); let currency = currency.default_string(is_plural); - let cardinal = self.int_to_cardinal(num)?; - return Ok(format!("{cardinal} {currency}")); + let cardinal = if is_plural { self.int_to_cardinal(num)? } else { "un".to_string() }; + return Ok(match cardinal.as_str() { + "uno" => format!("un {currency}"), + _ => format!("{cardinal} {currency}"), + }); } else { let hundred: BigFloat = 100.into(); let (integral, cents) = (num.int(), num.mul(&hundred).int().rem(&hundred)); - let (int_words, cent_words) = - (self.to_currency(integral, currency)?, self.int_to_cardinal(cents)?); let cents_is_plural = cents != 1.into(); + let (int_words, cent_words) = ( + self.to_currency(integral, currency)?, + if cents_is_plural { self.int_to_cardinal(cents)? } else { "un".to_string() }, + ); let cents_suffix = currency.default_subunit_string("centavo{}", cents_is_plural); if cents.is_zero() { From d197359265fba4d040f5dca3218e2a62bb2197ba Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 20:43:04 +0000 Subject: [PATCH 29/52] run Rustfmt on most files and add derives --- src/bin/bin.rs | 3 +- src/currency.rs | 2 +- src/lang/es.rs | 5 +-- src/lang/lang.rs | 40 +++++++++++------------ src/lang/mod.rs | 8 ++--- src/lib.rs | 16 +++------ src/output.rs | 1 + tests/lang_es_test.rs | 75 ++++++++++++++++++++++--------------------- 8 files changed, 69 insertions(+), 81 deletions(-) diff --git a/src/bin/bin.rs b/src/bin/bin.rs index d3394c5..4a5db8b 100644 --- a/src/bin/bin.rs +++ b/src/bin/bin.rs @@ -1,7 +1,8 @@ -use ::num2words::{Currency, Lang, Num2Words}; use std::env; use std::str::FromStr; +use ::num2words::{Currency, Lang, Num2Words}; + const HELP: &str = r#"NAME: num2words - convert numbers into words diff --git a/src/currency.rs b/src/currency.rs index 7a5dffe..d0427c7 100644 --- a/src/currency.rs +++ b/src/currency.rs @@ -5,7 +5,7 @@ use std::str::FromStr; /// Every three-letter variant is a valid ISO 4217 currency code. The only /// exceptions are `DINAR`, `DOLLAR`, `PESO` and `RIYAL`, which are generic /// terminology for the respective currencies. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] #[non_exhaustive] pub enum Currency { /// Dirham diff --git a/src/lang/es.rs b/src/lang/es.rs index 9918ed0..00538a1 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -1,9 +1,6 @@ -#![allow(unused_imports)] // TODO: Remove this attribute + use core::fmt::{self, Formatter}; -use std::borrow::BorrowMut; -use std::convert::TryInto; use std::fmt::Display; -use std::ops::Neg; use std::str::FromStr; use num_bigfloat::BigFloat; diff --git a/src/lang/lang.rs b/src/lang/lang.rs index 8e4fe9a..2b496ca 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -1,11 +1,10 @@ -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(clippy::search_is_some)] -use crate::lang; -use crate::num2words::Num2Err; -use crate::Currency; -use num_bigfloat::BigFloat; use std::str::FromStr; +use num_bigfloat::BigFloat; + +use crate::num2words::Num2Err; +use crate::{lang, Currency}; + /// Defines what is a language pub trait Language { fn to_cardinal(&self, num: BigFloat) -> Result; @@ -149,28 +148,27 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::CH)) - }, + } Lang::Spanish => { - use crate::lang::es::DecimalChar; - use super::es::NegativeFlavour; + use super::es::{DecimalChar, NegativeFlavour}; let neg_flavour = preferences .iter() - .find_map(|v| NegativeFlavour::from_str(v).ok()).unwrap_or_default(); - let prefer_veinte = preferences - .iter() - .any(|v| ["veinte"].binary_search(&v.as_str()).is_ok()); - let decimal_char = preferences - .iter() - .find_map(|v| DecimalChar::from_str(v).ok()).unwrap_or_default(); + .find_map(|v| NegativeFlavour::from_str(v).ok()) + .unwrap_or_default(); + let prefer_veinte = + preferences.iter().any(|v| ["veinte"].binary_search(&v.as_str()).is_ok()); + let decimal_char = + preferences.iter().find_map(|v| DecimalChar::from_str(v).ok()).unwrap_or_default(); let feminine = preferences .iter() .any(|v| ["f", "femenino", "feminine"].binary_search(&v.as_str()).is_ok()); - let plural = preferences - .iter() - .any(|v| ["plural"].binary_search(&v.as_str()).is_ok()); - let lang = lang::Spanish::new(decimal_char, feminine).with_plural(plural).with_veinte(prefer_veinte).with_neg_flavour(neg_flavour); + let plural = preferences.iter().any(|v| ["plural"].binary_search(&v.as_str()).is_ok()); + let lang = lang::Spanish::new(decimal_char, feminine) + .with_plural(plural) + .with_veinte(prefer_veinte) + .with_neg_flavour(neg_flavour); Box::new(lang) - }, + } Lang::Ukrainian => { let declension: lang::uk::Declension = preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); diff --git a/src/lang/mod.rs b/src/lang/mod.rs index 3f43732..b16d0f1 100644 --- a/src/lang/mod.rs +++ b/src/lang/mod.rs @@ -1,15 +1,11 @@ -#![cfg_attr(rustfmt, rustfmt_skip)] // TODO: Remove attribute before final merge -mod lang; mod en; mod es; mod fr; +mod lang; mod uk; pub use en::English; pub use es::Spanish; pub use fr::French; +pub use lang::{to_language, Lang, Language}; pub use uk::Ukrainian; - -pub use lang::to_language; -pub use lang::Lang; -pub use lang::Language; diff --git a/src/lib.rs b/src/lib.rs index f85efdb..61bdea1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,3 @@ -#![cfg_attr(rustfmt, rustfmt_skip)] #![crate_type = "lib"] #![crate_name = "num2words"] @@ -23,14 +22,8 @@ * * ```rust * use num2words::*; - * assert_eq!( - * Num2Words::new(42).lang(Lang::French).to_words(), - * Ok(String::from("quarante-deux")) - * ); - * assert_eq!( - * Num2Words::new(42).ordinal().to_words(), - * Ok(String::from("forty-second")) - * ); + * assert_eq!(Num2Words::new(42).lang(Lang::French).to_words(), Ok(String::from("quarante-deux"))); + * assert_eq!(Num2Words::new(42).ordinal().to_words(), Ok(String::from("forty-second"))); * assert_eq!( * Num2Words::new(42.01).currency(Currency::DOLLAR).to_words(), * Ok(String::from("forty-two dollars and one cent")) @@ -116,11 +109,12 @@ mod num2words; mod currency; -pub mod lang; // TODO: remove pub visibility before merging +mod lang; mod output; -pub use crate::num2words::{Num2Err, Num2Words}; pub use currency::Currency; pub use lang::Lang; use lang::Language; use output::Output; + +pub use crate::num2words::{Num2Err, Num2Words}; diff --git a/src/output.rs b/src/output.rs index f78e072..a05f86a 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,6 +1,7 @@ use std::str::FromStr; /// Type of the output `num2words` give +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Output { /// Number in cardinal form, e.g., `forty-two` Cardinal, diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index 9898fe1..cc7b8f8 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -1,41 +1,42 @@ -use num2words::lang::to_language; -use num2words::Lang; -use num_bigfloat::BigFloat; +// use num2words::lang::to_language; +// use num2words::Lang; +// use num_bigfloat::BigFloat; -#[test] -fn test_lang_es() { - let prefs_basics: Vec = - vec!["negativo" /* , "veinte", "menos", "prepended", "appended", "bajo cero" */] - .into_iter() - .map(String::from) - .collect(); - let prefs_for_ordinals: Vec = - vec!["femenino", /* "f", "feminine", */ "plural"].into_iter().map(String::from).collect(); - let prefs_for_decimal_char: Vec = vec!["coma"].into_iter().map(String::from).collect(); +// #[test] +// fn test_lang_es() { +// let prefs_basics: Vec = +// vec!["negativo" /* , "veinte", "menos", "prepended", "appended", "bajo cero" */] +// .into_iter() +// .map(String::from) +// .collect(); +// let prefs_for_ordinals: Vec = +// vec!["femenino", /* "f", "feminine", */ +// "plural"].into_iter().map(String::from).collect(); let prefs_for_decimal_char: Vec = +// vec!["coma"].into_iter().map(String::from).collect(); - let driver = to_language( - Lang::Spanish, - prefs_basics.iter().chain(&prefs_for_decimal_char).cloned().collect(), - ); - let word = driver.to_cardinal(BigFloat::from(-821_442_524.69)).unwrap(); - #[rustfmt::skip] - assert_eq!(word, "ochocientos veintiún millones cuatrocientos cuarenta y dos mil quinientos veinticuatro coma seis nueve negativo"); - let word = driver.to_ordinal(BigFloat::from(-484)); - assert!(word.is_err()); // You can't get the ordinal of a negative number +// let driver = to_language( +// Lang::Spanish, +// prefs_basics.iter().chain(&prefs_for_decimal_char).cloned().collect(), +// ); +// let word = driver.to_cardinal(BigFloat::from(-821_442_524.69)).unwrap(); +// #[rustfmt::skip] +// assert_eq!(word, "ochocientos veintiún millones cuatrocientos cuarenta y dos mil quinientos +// veinticuatro coma seis nueve negativo"); let word = driver.to_ordinal(BigFloat::from(-484)); +// assert!(word.is_err()); // You can't get the ordinal of a negative number - let driver = to_language(Lang::Spanish, prefs_for_ordinals.clone()); - assert_eq!(driver.to_ordinal(14.into()).unwrap(), "decimocuartas"); - assert_eq!(driver.to_ordinal(1.into()).unwrap(), "primeras"); - assert_eq!(driver.to_ordinal(2.into()).unwrap(), "segundas"); +// let driver = to_language(Lang::Spanish, prefs_for_ordinals.clone()); +// assert_eq!(driver.to_ordinal(14.into()).unwrap(), "decimocuartas"); +// assert_eq!(driver.to_ordinal(1.into()).unwrap(), "primeras"); +// assert_eq!(driver.to_ordinal(2.into()).unwrap(), "segundas"); - let driver = to_language(Lang::Spanish, vec![]); - assert_eq!( - driver.to_ordinal(141_100_211_021u64.into()).unwrap(), - "centésimo cuadragésimo primero billonésimo centésimo millonésimo ducentésimo undécimo \ - milésimo vigésimo primero" - ); - assert_eq!(driver.to_ordinal(14.into()).unwrap(), "decimocuarto"); - assert_eq!(driver.to_ordinal(1.into()).unwrap(), "primero"); - assert_eq!(driver.to_ordinal(2.into()).unwrap(), "segundo"); - assert_eq!(driver.to_ordinal(3.into()).unwrap(), "tercero"); -} +// let driver = to_language(Lang::Spanish, vec![]); +// assert_eq!( +// driver.to_ordinal(141_100_211_021u64.into()).unwrap(), +// "centésimo cuadragésimo primero billonésimo centésimo millonésimo ducentésimo undécimo \ +// milésimo vigésimo primero" +// ); +// assert_eq!(driver.to_ordinal(14.into()).unwrap(), "decimocuarto"); +// assert_eq!(driver.to_ordinal(1.into()).unwrap(), "primero"); +// assert_eq!(driver.to_ordinal(2.into()).unwrap(), "segundo"); +// assert_eq!(driver.to_ordinal(3.into()).unwrap(), "tercero"); +// } From 611a17af20e5829bd30270786b2d1796438449a4 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:12:25 -0500 Subject: [PATCH 30/52] testing DocTests --- src/lang/es.rs | 89 +++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 00538a1..dae7dec 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -1,4 +1,3 @@ - use core::fmt::{self, Formatter}; use std::fmt::Display; use std::str::FromStr; @@ -187,7 +186,7 @@ pub struct Spanish { // Plural for ordinal numbers plural: bool, } - +#[allow(unused)] impl Spanish { #[inline(always)] pub fn new(decimal_char: DecimalChar, feminine: bool) -> Self { @@ -412,11 +411,15 @@ impl Spanish { impl Language for Spanish { /// Converts a BigFloat to a cardinal number in Spanish /// ```rust - /// use num2words::lang::{to_language, Lang, Language}; + /// use num2words::{Lang, Num2Words}; /// use num_bigfloat::BigFloat; /// - /// let es = to_language(Lang::Spanish, vec!["negativo".to_string()]); - /// let words = es.to_cardinal(BigFloat::from(-123456.789)).unwrap(); + /// let words = Num2Words::new(-123_456.789) + /// .lang(Lang::Spanish) + /// .cardinal() + /// .prefer("negativo") + /// .to_words() + /// .unwrap(); /// assert_eq!( /// words, /// "ciento veintitres mil cuatrocientos cincuenta y seis punto siete ocho nueve negativo" @@ -424,7 +427,7 @@ impl Language for Spanish { /// ``` fn to_cardinal(&self, num: BigFloat) -> Result { if num.is_nan() { - return Err(Num2Err::CannotConvert); + Err(Num2Err::CannotConvert) } else if num.is_inf() { self.inf_to_cardinal(&num) } else if num.frac().is_zero() { @@ -437,11 +440,10 @@ impl Language for Spanish { /// Ordinal numbers above 10 are unnatural for Spanish speakers. Don't rely on these to convey /// meanings /// ```rust - /// use num2words::lang::{to_language, Lang, Language}; + /// use num2words::{Lang, Num2Words}; /// use num_bigfloat::BigFloat; /// - /// let es = to_language(Lang::Spanish, vec![]); - /// let words = es.to_ordinal(BigFloat::from(11)).unwrap(); + /// let words = Num2Words::new(11).lang(Lang::Spanish).ordinal().to_words().unwrap(); /// assert_eq!(words, "undécimo"); /// ``` fn to_ordinal(&self, num: BigFloat) -> Result { @@ -512,16 +514,14 @@ impl Language for Spanish { /// A numeric number which has a `ª` or `º` appended at the end /// ```rust - /// use num2words::lang::{to_language, Lang, Language}; + /// use num2words::{Lang, Num2Words}; /// use num_bigfloat::BigFloat; /// - /// let num = BigFloat::from(8); + /// let words = Num2Words::new(8).lang(Lang::Spanish).ordinal_num().to_words().unwrap(); + /// assert_eq!(words, "8º", "some mismatch"); /// - /// let es_male = to_language(Lang::Spanish, vec![]); - /// assert_eq!(es_male.to_ordinal_num(num).unwrap(), "8º"); - /// - /// let es_female = to_language(Lang::Spanish, vec!["feminine".to_string()]); - /// assert_eq!(es_female.to_ordinal_num(num).unwrap(), "8ª"); + /// let words = Num2Words::new(8).lang(Lang::Spanish).ordinal_num().prefer("femenino"); + /// assert_eq!(words.to_words().unwrap(), "8ª", "some mismatch2"); /// ``` fn to_ordinal_num(&self, num: BigFloat) -> Result { match (num.is_inf(), num.is_negative(), num.frac().is_zero()) { @@ -540,14 +540,14 @@ impl Language for Spanish { /// A year is just a Cardinal number. When the BigFloat input is negative, it appends "a.C." to /// the positive Cardinal representation /// ```rust - /// use num2words::lang::{to_language, Lang, Language}; + /// use num2words::{Lang, Num2Words}; /// use num_bigfloat::BigFloat; /// - /// let num = BigFloat::from(2021); - /// let es = to_language(Lang::Spanish, vec![]); + /// let words = Num2Words::new(2021).lang(Lang::Spanish).year().to_words().unwrap(); + /// assert_eq!(words, "dos mil veintiuno"); /// - /// assert_eq!(es.to_year(num).unwrap(), "dos mil veintiuno"); - /// assert_eq!(es.to_year(-num).unwrap(), "dos mil veintiuno a. C."); + /// let words = Num2Words::new(-2021).lang(Lang::Spanish).year().to_words().unwrap(); + /// assert_eq!(words, "dos mil veintiuno a. C."); /// ``` fn to_year(&self, num: BigFloat) -> Result { match (num.is_inf(), num.frac().is_zero(), num.int().is_zero()) { @@ -574,26 +574,38 @@ impl Language for Spanish { } /// A Cardinal number which then the currency word representation is appended at the end. - /// `1` is the only exception to the rule. + /// Any cardinal that ends in "uno" is the only exception to the rule. For example 41, 21 and 1 /// The extra decimals are truncated instead of rounded /// ```rust - /// use num2words::lang::{to_language, Lang, Language}; - /// use num2words::Currency; + /// use num2words::{Currency, Lang, Num2Words}; /// use num_bigfloat::BigFloat; /// - /// let es = to_language(Lang::Spanish, vec![]); + /// let words = + /// Num2Words::new(-2021).lang(Lang::Spanish).currency(Currency::USD).to_words().unwrap(); + /// assert_eq!(words, "menos dos mil veintiún US dollars"); /// - /// assert_eq!( - /// es.to_currency(BigFloat::from(-2021), Currency::USD).unwrap(), - /// "menos dos mil veintiuno US dollars" - /// ); - /// assert_eq!( - /// es.to_currency(BigFloat::from(1.01), Currency::USD).unwrap(), - /// "un US dollar con un centavo" - /// ); - /// assert_eq!(es.to_currency(BigFloat::from(1), Currency::USD).unwrap(), "un US dollar"); + /// let words = + /// Num2Words::new(81.21).lang(Lang::Spanish).currency(Currency::USD).to_words().unwrap(); + /// assert_eq!(words, "ochenta y un US dollars con veintiún centavos"); + /// + /// let words = + /// Num2Words::new(1.01).lang(Lang::Spanish).currency(Currency::USD).to_words().unwrap(); + /// assert_eq!(words, "un US dollar con un centavo"); + /// + /// let words = Num2Words::new(1).lang(Lang::Spanish).currency(Currency::USD).to_words().unwrap(); + /// assert_eq!(words, "un US dollar"); /// ``` fn to_currency(&self, num: BigFloat, currency: crate::Currency) -> Result { + let strip_uno_into_un = |string: String| -> String { + let len = string.len(); + if string.ends_with("iuno") { + string[..len - 3].to_string() + "ún" + } else if string.ends_with("uno") { + string[..len - 1].to_string() + } else { + string + } + }; if num.is_nan() { Err(Num2Err::CannotConvert) } else if num.is_inf() { @@ -604,18 +616,15 @@ impl Language for Spanish { } else if num.frac().is_zero() { let is_plural = num.int() != 1.into(); let currency = currency.default_string(is_plural); - let cardinal = if is_plural { self.int_to_cardinal(num)? } else { "un".to_string() }; - return Ok(match cardinal.as_str() { - "uno" => format!("un {currency}"), - _ => format!("{cardinal} {currency}"), - }); + let cardinal = strip_uno_into_un(self.int_to_cardinal(num)?); + return Ok(format!("{cardinal} {currency}")); } else { let hundred: BigFloat = 100.into(); let (integral, cents) = (num.int(), num.mul(&hundred).int().rem(&hundred)); let cents_is_plural = cents != 1.into(); let (int_words, cent_words) = ( self.to_currency(integral, currency)?, - if cents_is_plural { self.int_to_cardinal(cents)? } else { "un".to_string() }, + strip_uno_into_un(self.int_to_cardinal(cents)?), ); let cents_suffix = currency.default_subunit_string("centavo{}", cents_is_plural); From f86b6ae3c916fa05a7cda5e9892ca535152b2cac Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:12:50 -0500 Subject: [PATCH 31/52] Try to improve Integration Test --- tests/lang_es_test.rs | 130 ++++++++++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 42 deletions(-) diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index cc7b8f8..f4c5952 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -1,42 +1,88 @@ -// use num2words::lang::to_language; -// use num2words::Lang; -// use num_bigfloat::BigFloat; - -// #[test] -// fn test_lang_es() { -// let prefs_basics: Vec = -// vec!["negativo" /* , "veinte", "menos", "prepended", "appended", "bajo cero" */] -// .into_iter() -// .map(String::from) -// .collect(); -// let prefs_for_ordinals: Vec = -// vec!["femenino", /* "f", "feminine", */ -// "plural"].into_iter().map(String::from).collect(); let prefs_for_decimal_char: Vec = -// vec!["coma"].into_iter().map(String::from).collect(); - -// let driver = to_language( -// Lang::Spanish, -// prefs_basics.iter().chain(&prefs_for_decimal_char).cloned().collect(), -// ); -// let word = driver.to_cardinal(BigFloat::from(-821_442_524.69)).unwrap(); -// #[rustfmt::skip] -// assert_eq!(word, "ochocientos veintiún millones cuatrocientos cuarenta y dos mil quinientos -// veinticuatro coma seis nueve negativo"); let word = driver.to_ordinal(BigFloat::from(-484)); -// assert!(word.is_err()); // You can't get the ordinal of a negative number - -// let driver = to_language(Lang::Spanish, prefs_for_ordinals.clone()); -// assert_eq!(driver.to_ordinal(14.into()).unwrap(), "decimocuartas"); -// assert_eq!(driver.to_ordinal(1.into()).unwrap(), "primeras"); -// assert_eq!(driver.to_ordinal(2.into()).unwrap(), "segundas"); - -// let driver = to_language(Lang::Spanish, vec![]); -// assert_eq!( -// driver.to_ordinal(141_100_211_021u64.into()).unwrap(), -// "centésimo cuadragésimo primero billonésimo centésimo millonésimo ducentésimo undécimo \ -// milésimo vigésimo primero" -// ); -// assert_eq!(driver.to_ordinal(14.into()).unwrap(), "decimocuarto"); -// assert_eq!(driver.to_ordinal(1.into()).unwrap(), "primero"); -// assert_eq!(driver.to_ordinal(2.into()).unwrap(), "segundo"); -// assert_eq!(driver.to_ordinal(3.into()).unwrap(), "tercero"); -// } +use num2words::{Currency, Lang, Num2Err, Num2Words}; +use num_bigfloat::BigFloat; +enum Outputs { + Cardinal, + Ordinal, + OrdinalNum, + Year, + Currency, +} +fn to_words(num: BigFloat, output: Outputs, preference: &[&str]) -> Result { + let mut driver = Num2Words::new(num).lang(Lang::Spanish); + for preference in preference.into_iter() { + driver = driver.prefer(preference.to_string()); + } + let driver = match output { + Outputs::Cardinal => driver.cardinal(), + Outputs::Ordinal => driver.ordinal(), + Outputs::OrdinalNum => driver.ordinal_num(), + Outputs::Year => driver.year(), + Outputs::Currency => driver.currency(Currency::USD), + }; + driver.to_words() +} +#[test] +fn test_lang_es() { + let prefs_basics = + ["negativo" /* , "veinte", "menos", "prepended", "appended", "bajo cero" */]; + let prefs_for_ordinals = vec!["femenino" /* "f", "feminine", */, "plural"]; + let prefs_for_decimal_char = vec!["coma"]; + + let driver = |output: Outputs, num: BigFloat| { + to_words( + num, + output, + prefs_basics + .iter() + .chain(&prefs_for_decimal_char) + .copied() + .collect::>() + .as_slice(), + ) + }; + let word = driver(Outputs::Cardinal, BigFloat::from(-821_442_524.69)).unwrap(); + assert_eq!( + word, + "ochocientos veintiún millones cuatrocientos cuarenta y dos mil quinientos veinticuatro \ + coma seis nueve negativo" + ); + let word = driver(Outputs::Ordinal, BigFloat::from(-484)); + assert!(word.is_err()); // You can't get the ordinal of a negative number + + let driver = + |output: Outputs, num: BigFloat| to_words(num, output, prefs_for_ordinals.as_slice()); + + // let driver = to_language(Lang::Spanish, prefs_for_ordinals.clone()); + // let word = ; + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(14)).unwrap(), "decimocuartas"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(1)).unwrap(), "primeras"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(2)).unwrap(), "segundas"); + + let driver = |output: Outputs, num: BigFloat| to_words(num, output, &[]); + let word = driver(Outputs::Ordinal, BigFloat::from(141_100_211_021u64)).unwrap(); + assert_eq!( + word, + "centésimo cuadragésimo primero billonésimo centésimo millonésimo ducentésimo undécimo \ + milésimo vigésimo primero" + ); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(14)).unwrap(), "decimocuarto"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(1)).unwrap(), "primero"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(2)).unwrap(), "segundo"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(3)).unwrap(), "tercero"); + + let word = to_words(BigFloat::from(14), Outputs::OrdinalNum, &["f"]); + assert_eq!(word.unwrap(), "14ª"); + let word = to_words(BigFloat::from(14), Outputs::OrdinalNum, &[]); + assert_eq!(word.unwrap(), "14º"); + + let word = to_words(BigFloat::from(2021), Outputs::Year, &[]); + assert_eq!(word.unwrap(), "dos mil veintiuno"); + let word = to_words(BigFloat::from(-2021), Outputs::Year, &[]); + assert_eq!(word.unwrap(), "dos mil veintiuno a. C."); + + let word = to_words(BigFloat::from(21_001.21), Outputs::Currency, &[]); + assert_eq!(word.unwrap(), "veintiún mil un US dollars con veintiún centavos"); + + let word = to_words(BigFloat::from(21.01), Outputs::Currency, &[]); + assert_eq!(word.unwrap(), "veintiún US dollars con un centavo"); +} From a3be35056f68804c1251528878365e0e18d1aad8 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:14:21 -0500 Subject: [PATCH 32/52] Main became too much of a mess, so un-using it --- Cargo.toml | 4 --- src/main.rs | 98 +---------------------------------------------------- 2 files changed, 1 insertion(+), 101 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e9fe240..8e97e78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,5 @@ path = "src/lib.rs" name = "num2words" path = "src/bin/bin.rs" -[[bin]] -name = "test_es" -path = "src/main.rs" - [dependencies] num-bigfloat = { version = "^1.7.1", default-features = false } diff --git a/src/main.rs b/src/main.rs index a507ed5..deea281 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,99 +1,3 @@ -use std::io::{stdin, Write}; - -use num2words::lang::{Language, Spanish}; -use num_bigfloat::BigFloat; pub fn main() { - let es = Spanish::default(); - let mut string = String::new(); - // Sort slice - let mut slice = ["bajo cero", "negativo", "menos"]; - slice.sort(); - println!("{:?}", slice); - let feminine = ["f", "femi", "feminino"].iter().find(|preference| { - let result = slice.binary_search(preference); - println!("{:?} := {preference:?}", result); - false - }); - println!("{:?}", feminine); - /* { - let found = slice.binary_search(preference); - println!("{:?}", found); - } */ - loop { - string.clear(); - stdin().read_line(&mut string).unwrap(); - let num = string.trim().parse::(); - if num.is_err() { - println!("Número inválido"); - continue; - } - let num = num.unwrap(); - let result = es.to_ordinal(BigFloat::from(num)); - pretty_print_int(num); - println!("\n{:?}", result); - } - // let mut input = String::new(); - // print!("\nIngrese un número para convertir a palabras\nIngrese `exit` para salir:\n\n"); - // fn read_line(input: &mut String) { - // input.clear(); - // std::io::stdin().read_line(input).unwrap(); - // } - // loop { - // print!("Ingrese su número: "); - // flush(); - // read_line(&mut input); - // let input = input.trim(); - // match input { - // "exit" => { - // clear_terminal(); - // println!("Saliendo..."); - // break; - // } - // "clear" => { - // clear_terminal(); - // continue; - // } - // _ => {} - // } - // if input.is_empty() { - // println!("Número inválido {input:?} no puede estar vacío"); - // continue; - // } - // let num = match input.parse::() { - // Ok(num) => num, - // Err(_) => { - // println!("Número inválido {input:?} - no es convertible a un número entero"); - // continue; - // } - // }; - // print!("Entrada:"); - // pretty_print_int(num); - // println!(" => {:?}", es.to_int_cardinal(num.into()).unwrap()); - // } -} -pub fn clear_terminal() { - print!("{esc}[2J{esc}[1;1H", esc = 27 as char); -} -pub fn back_space(amount: usize) { - for _i in 0..amount { - print!("{}", 8u8 as char); - } - flush(); -} -pub fn flush() { - std::io::stdout().flush().unwrap(); -} -pub fn pretty_print_int>(num: T) { - let mut num: i128 = num.into(); - let mut vec = vec![]; - while num > 0 { - vec.push((num % 1000) as i16); - num /= 1000; - } - vec.reverse(); - let prettied = - vec.into_iter().map(|num| format!("{num:03}")).collect::>().join(","); - - print!("{:?}", prettied.trim_start_matches('0')); - flush(); + println!("Hello, world!"); } From 63c599ad5ca813c52644935d37fe09316a71abb3 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Thu, 11 Apr 2024 08:54:39 -0500 Subject: [PATCH 33/52] Remove devcontainers and rustfmt --- .devcontainer/Dockerfile | 9 ------ .devcontainer/devcontainer.json | 56 --------------------------------- .devcontainer/script.sh | 38 ---------------------- rustfmt.toml | 47 --------------------------- 4 files changed, 150 deletions(-) delete mode 100644 .devcontainer/Dockerfile delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .devcontainer/script.sh delete mode 100644 rustfmt.toml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index 8180d95..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ubuntu:22.04 - -WORKDIR /home/ - -COPY . . - -RUN bash ./script.sh - -ENV PATH="/root/.cargo/bin:$PATH" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 0caf314..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "Codespaces Rust Starter", - "customizations": { - "vscode": { - "extensions": [ - "cschleiden.vscode-github-actions", - "ms-vsliveshare.vsliveshare", - "serayuzgur.crates", - "vadimcn.vscode-lldb", - - "GitHub.copilot", - "rust-lang.rust-analyzer", - "serayuzgur.crates", - "zhuangtongfa.material-theme", - "usernamehw.errorlens", - "tamasfe.even-better-toml", - "formulahendry.code-runner" - ], - "settings": { - "workbench.colorTheme": "One Dark Pro Mix", - "editor.formatOnSave": true, - "editor.inlayHints.enabled": "offUnlessPressed", - "terminal.integrated.shell.linux": "/usr/bin/zsh", - "rust-analyzer.rustfmt.extraArgs": [ - "+nightly" // I personally love nightly rustfmt - ], - "files.exclude": { - "**/CODE_OF_CONDUCT.md": true, - "**/LICENSE": true - } - }, - "keybindings": // Place your key bindings in this file to override the defaults - [ - { - "key": "alt+q", - "command": "workbench.action.openQuickChat.copilot" - }, - { - "key": "alt+a", - "command": "github.copilot.ghpr.applySuggestion" - }, - { - "key": "alt+`", - "command": "editor.action.showHover", - "when": "editorTextFocus" - }, - { - "key": "ctrl+k ctrl+i", - "command": "-editor.action.showHover", - "when": "editorTextFocus" - } - ] - } - }, - "dockerFile": "Dockerfile" -} \ No newline at end of file diff --git a/.devcontainer/script.sh b/.devcontainer/script.sh deleted file mode 100644 index 52bdb62..0000000 --- a/.devcontainer/script.sh +++ /dev/null @@ -1,38 +0,0 @@ -## update and install some things we should probably have -apt-get update -apt-get install -y \ - curl \ - git \ - gnupg2 \ - jq \ - sudo \ - zsh \ - vim \ - build-essential \ - openssl - -## update and install 2nd level of packages -apt-get install -y pkg-config - -## Install rustup and common components -curl https://sh.rustup.rs -sSf | sh -s -- -y - -export PATH="/root/.cargo/bin/":$PATH - -rustup toolchain install nightly -# rustup component add rustfmt -# rustup component add rustfmt --toolchain nightly -# rustup component add clippy -# rustup component add clippy --toolchain nightly - -# Download cargo-binstall to ~/.cargo/bin directory -curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash - -cargo binstall cargo-expand cargo-edit cargo-watch -y - -## setup and install oh-my-zsh -sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" -cp -R /root/.oh-my-zsh /home/$USERNAME -cp /root/.zshrc /home/$USERNAME -sed -i -e "s/\/root\/.oh-my-zsh/\/home\/$USERNAME\/.oh-my-zsh/g" /home/$USERNAME/.zshrc -chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 06f6800..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,47 +0,0 @@ -# Update to nightly for nightly gated rustfmt fields -# Command: "rustup toolchain install nightly" - -# Add to setting.json of your profile in VSCode -# "rust-analyzer.rustfmt.extraArgs": [ -# "+nightly" -# ], -######################################## - -# I can't rely on contributors using .editorconfig -newline_style = "Unix" -# require the shorthand instead of it being optional -use_field_init_shorthand = true -# outdated default — `?` was unstable at the time -# additionally the `try!` macro is deprecated now -use_try_shorthand = false -# Max to use the 100 char width for everything or Default. See https://rust-lang.github.io/rustfmt/?version=v1.4.38&search=#use_small_heuristics -use_small_heuristics = "Max" -# Unstable features below -unstable_features = true -version = "Two" -## code can be 100 characters, why not comments? -comment_width = 140 -# force contributors to follow the formatting requirement -error_on_line_overflow = true -# error_on_unformatted = true ## Error if unable to get comments or string literals within max_width, or they are left with trailing whitespaces. -# next 4: why not? -format_code_in_doc_comments = true -format_macro_bodies = true ## Format the bodies of macros. -format_macro_matchers = true ## Format the metavariable matching patterns in macros. -## Wraps string when it overflows max_width -format_strings = true -# better grepping -imports_granularity = "Module" -# quicker manual lookup -group_imports = "StdExternalCrate" -# why use an attribute if a normal doc comment would suffice? -normalize_doc_attributes = true -# why not? -wrap_comments = true - -merge_derives = false ## I might need multi-line derives -overflow_delimited_expr = false -## When structs, slices, arrays, and block/array-like macros are used as the last argument in an -## expression list, allow them to overflow (like blocks/closures) instead of being indented on a new line. -reorder_impl_items = true -## Reorder impl items. type and const are put first, then macros and methods. From 25de99618afd824866fa3aecc32d4868810e6001 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Thu, 11 Apr 2024 08:55:11 -0500 Subject: [PATCH 34/52] attend clippy warning in test --- tests/lang_es_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index f4c5952..a163cae 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -9,7 +9,7 @@ enum Outputs { } fn to_words(num: BigFloat, output: Outputs, preference: &[&str]) -> Result { let mut driver = Num2Words::new(num).lang(Lang::Spanish); - for preference in preference.into_iter() { + for preference in preference.iter() { driver = driver.prefer(preference.to_string()); } let driver = match output { From 26eef471946a97c5941c5b3a5ac09f1ba02e0a50 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Thu, 11 Apr 2024 17:51:41 +0000 Subject: [PATCH 35/52] undo rustfmt on bin.rs --- src/bin/bin.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bin/bin.rs b/src/bin/bin.rs index 4a5db8b..d3394c5 100644 --- a/src/bin/bin.rs +++ b/src/bin/bin.rs @@ -1,8 +1,7 @@ +use ::num2words::{Currency, Lang, Num2Words}; use std::env; use std::str::FromStr; -use ::num2words::{Currency, Lang, Num2Words}; - const HELP: &str = r#"NAME: num2words - convert numbers into words From 5af4b80344431926ecb0b694f97d63b6ea52ee05 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:32:02 +0000 Subject: [PATCH 36/52] Undoing rustfmt on lang.rs --- src/lang/lang.rs | 74 +++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/src/lang/lang.rs b/src/lang/lang.rs index 2b496ca..1bf3adb 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -1,9 +1,8 @@ -use std::str::FromStr; - -use num_bigfloat::BigFloat; - +use crate::lang; use crate::num2words::Num2Err; -use crate::{lang, Currency}; +use crate::Currency; +use num_bigfloat::BigFloat; +use std::str::FromStr; /// Defines what is a language pub trait Language { @@ -99,7 +98,10 @@ impl FromStr for Lang { pub fn to_language(lang: Lang, preferences: Vec) -> Box { match lang { Lang::English => { - let last = preferences.iter().rev().find(|v| ["oh", "nil"].contains(&v.as_str())); + let last = preferences + .iter() + .rev() + .find(|v| ["oh", "nil"].contains(&v.as_str())); if let Some(v) = last { return Box::new(lang::English::new(v == "oh", v == "nil")); @@ -114,9 +116,7 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| { - ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) - }) + .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::FR)) @@ -128,9 +128,7 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| { - ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) - }) + .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) .is_some(); Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::BE)) @@ -142,12 +140,14 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| { - ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) - }) + .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) .is_some(); - Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::CH)) + Box::new(lang::French::new( + feminine, + reformed, + lang::fr::RegionFrench::CH, + )) } Lang::Spanish => { use super::es::{DecimalChar, NegativeFlavour}; @@ -155,14 +155,21 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .iter() .find_map(|v| NegativeFlavour::from_str(v).ok()) .unwrap_or_default(); - let prefer_veinte = - preferences.iter().any(|v| ["veinte"].binary_search(&v.as_str()).is_ok()); - let decimal_char = - preferences.iter().find_map(|v| DecimalChar::from_str(v).ok()).unwrap_or_default(); - let feminine = preferences + let prefer_veinte = preferences + .iter() + .any(|v| ["veinte"].binary_search(&v.as_str()).is_ok()); + let decimal_char = preferences .iter() - .any(|v| ["f", "femenino", "feminine"].binary_search(&v.as_str()).is_ok()); - let plural = preferences.iter().any(|v| ["plural"].binary_search(&v.as_str()).is_ok()); + .find_map(|v| DecimalChar::from_str(v).ok()) + .unwrap_or_default(); + let feminine = preferences.iter().any(|v| { + ["f", "femenino", "feminine"] + .binary_search(&v.as_str()) + .is_ok() + }); + let plural = preferences + .iter() + .any(|v| ["plural"].binary_search(&v.as_str()).is_ok()); let lang = lang::Spanish::new(decimal_char, feminine) .with_plural(plural) .with_veinte(prefer_veinte) @@ -170,12 +177,21 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { Box::new(lang) } Lang::Ukrainian => { - let declension: lang::uk::Declension = - preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); - let gender: lang::uk::Gender = - preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); - let number: lang::uk::GrammaticalNumber = - preferences.iter().rev().find_map(|d| d.parse().ok()).unwrap_or_default(); + let declension: lang::uk::Declension = preferences + .iter() + .rev() + .find_map(|d| d.parse().ok()) + .unwrap_or_default(); + let gender: lang::uk::Gender = preferences + .iter() + .rev() + .find_map(|d| d.parse().ok()) + .unwrap_or_default(); + let number: lang::uk::GrammaticalNumber = preferences + .iter() + .rev() + .find_map(|d| d.parse().ok()) + .unwrap_or_default(); Box::new(lang::Ukrainian::new(gender, number, declension)) } } From 21d77de98be1666a22f0871ed4bdaf69447ea024 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:34:31 +0000 Subject: [PATCH 37/52] undo missed a rustfmt on lang.rs --- src/lang/lang.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lang/lang.rs b/src/lang/lang.rs index 1bf3adb..5255836 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -143,12 +143,8 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) .is_some(); - Box::new(lang::French::new( - feminine, - reformed, - lang::fr::RegionFrench::CH, - )) - } + Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::CH)) + } Lang::Spanish => { use super::es::{DecimalChar, NegativeFlavour}; let neg_flavour = preferences From 428adfd1137678401518ebbad3ac6ede1f977e25 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:42:24 +0000 Subject: [PATCH 38/52] undo rustfmt on lib.rs --- src/lib.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 61bdea1..0c68f93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,8 +22,14 @@ * * ```rust * use num2words::*; - * assert_eq!(Num2Words::new(42).lang(Lang::French).to_words(), Ok(String::from("quarante-deux"))); - * assert_eq!(Num2Words::new(42).ordinal().to_words(), Ok(String::from("forty-second"))); + * assert_eq!( + * Num2Words::new(42).lang(Lang::French).to_words(), + * Ok(String::from("quarante-deux")) + * ); + * assert_eq!( + * Num2Words::new(42).ordinal().to_words(), + * Ok(String::from("forty-second")) + * ); * assert_eq!( * Num2Words::new(42.01).currency(Currency::DOLLAR).to_words(), * Ok(String::from("forty-two dollars and one cent")) @@ -112,9 +118,8 @@ mod currency; mod lang; mod output; +pub use crate::num2words::{Num2Err, Num2Words}; pub use currency::Currency; pub use lang::Lang; use lang::Language; -use output::Output; - -pub use crate::num2words::{Num2Err, Num2Words}; +use output::Output; \ No newline at end of file From 0d7ee4c10e04a6a78243f919bb8ca606fc197fae Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:43:48 +0000 Subject: [PATCH 39/52] try fix trailing whitespace --- src/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0c68f93..07b4e8a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,11 +24,11 @@ * use num2words::*; * assert_eq!( * Num2Words::new(42).lang(Lang::French).to_words(), - * Ok(String::from("quarante-deux")) - * ); - * assert_eq!( - * Num2Words::new(42).ordinal().to_words(), - * Ok(String::from("forty-second")) + * Ok(String::from("quarante-deux")) + * ); + * assert_eq!( + * Num2Words::new(42).ordinal().to_words(), + * Ok(String::from("forty-second")) * ); * assert_eq!( * Num2Words::new(42.01).currency(Currency::DOLLAR).to_words(), From 6299fa3119d46ff96213ea4592a58ee54819830e Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:46:49 +0000 Subject: [PATCH 40/52] try fix EOF --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 07b4e8a..5da6b38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,4 +122,4 @@ pub use crate::num2words::{Num2Err, Num2Words}; pub use currency::Currency; pub use lang::Lang; use lang::Language; -use output::Output; \ No newline at end of file +use output::Output; From 88bb1ad86e23980a76b5023e6e2b2cefc3de7cbb Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:47:56 +0000 Subject: [PATCH 41/52] delete unused main.rs --- src/main.rs | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 src/main.rs diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index deea281..0000000 --- a/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn main() { - println!("Hello, world!"); -} From 7371a47ac0ef5b5a040c3ef999e2650da13fd9b5 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:51:41 +0000 Subject: [PATCH 42/52] remove cfg attribute from num2words.rs --- src/num2words.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/num2words.rs b/src/num2words.rs index 4cf523e..3b578bf 100644 --- a/src/num2words.rs +++ b/src/num2words.rs @@ -1,4 +1,3 @@ -#![cfg_attr(rustfmt, rustfmt_skip)] use crate::{lang, Currency, Lang, Output}; use num_bigfloat::BigFloat; From f6aeb2d3679352326d50716d8f6a6f0e11b00153 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:53:34 +0000 Subject: [PATCH 43/52] temporarily remove derives from enum Output in output.rs --- src/output.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/output.rs b/src/output.rs index a05f86a..f78e072 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,7 +1,6 @@ use std::str::FromStr; /// Type of the output `num2words` give -#[derive(Debug, Clone, Copy, PartialEq)] pub enum Output { /// Number in cardinal form, e.g., `forty-two` Cardinal, From d61def798a0d7fd221afdfe813c573a15dc725a8 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:56:50 +0000 Subject: [PATCH 44/52] Undo added derives to currency.rs --- src/currency.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/currency.rs b/src/currency.rs index d0427c7..7a5dffe 100644 --- a/src/currency.rs +++ b/src/currency.rs @@ -5,7 +5,7 @@ use std::str::FromStr; /// Every three-letter variant is a valid ISO 4217 currency code. The only /// exceptions are `DINAR`, `DOLLAR`, `PESO` and `RIYAL`, which are generic /// terminology for the respective currencies. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy)] #[non_exhaustive] pub enum Currency { /// Dirham From c72b3391ef4f8e411cf8279f23879db54cc12465 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:22:19 +0000 Subject: [PATCH 45/52] update README with new language --- src/bin/bin.rs | 1 + src/lang/lang.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/bin/bin.rs b/src/bin/bin.rs index d3394c5..93043e1 100644 --- a/src/bin/bin.rs +++ b/src/bin/bin.rs @@ -25,6 +25,7 @@ AVAILABLE LANGUAGES: fr: French (France and Canada) fr_BE: French (Belgium and the Democratic Republic of the Congo) fr_CH: French (Swiss Confederation and Aosta Valley) + es: Spanish uk: Ukrainian AVAILABLE OUTPUTS: diff --git a/src/lang/lang.rs b/src/lang/lang.rs index 5255836..ec64c55 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -78,6 +78,7 @@ impl FromStr for Lang { /// | Locale | Lang | 42 | /// | --------- | ----------------- | ------------- | /// | `en` | `Lang::English` | forty-two | + /// | `es` | `Lang::Spanish` | cuarenta y dos| /// | `fr` | `Lang::French` | quarante-deux | /// | `fr_BE` | `Lang::French_BE` | quarante-deux | /// | `fr_CH` | `Lang::French_CH` | quarante-deux | From 92f21c8d85daeb85aff2e848753d90c832be29ea Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:24:41 +0000 Subject: [PATCH 46/52] Update readme to reflect spanish as option --- README.md | 1 + src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cd4cfa4..95b4d1d 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Here is a list of all of the supported languages: | 🇫🇷🇨🇦 | `Lang::French` | `fr` | French | quarante-deux | | 🇧🇪🇨🇩 | `Lang::French_BE` | `fr_BE` | French (BE) | quarante-deux | | 🇨🇭 | `Lang::French_CH` | `fr_CH` | French (CH) | quarante-deux | +| 🇪🇸 | `Lang::Spanish` | `es` | Spanish | cuarenta y dos| | 🇺🇦 | `Lang::Ukrainian` | `uk` | Ukrainian | сорок два | This list can be expanded! Contributions are welcomed. diff --git a/src/lib.rs b/src/lib.rs index 5da6b38..6f6bc09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,8 +80,8 @@ * | 🇫🇷🇨🇦 | `Lang::French` | `fr` | French | quarante-deux | * | 🇧🇪🇨🇩 | `Lang::French_BE` | `fr_BE` | French (BE) | quarante-deux | * | 🇨🇭 | `Lang::French_CH` | `fr_CH` | French (CH) | quarante-deux | - * | 🇺🇦 | `Lang::Ukrainian` | `uk` | Ukrainian | сорок два | * | 🇪🇸 | `Lang::Spanish` | `es` | Spanish | cuarenta y dos| + * | 🇺🇦 | `Lang::Ukrainian` | `uk` | Ukrainian | сорок два | * * This list can be expanded! Contributions are welcomed. * From 2a8d28a4644f4a553d3bbbd4a5b93afa506a25ad Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:31:20 +0000 Subject: [PATCH 47/52] temporarily remove derives --- src/lang/lang.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lang/lang.rs b/src/lang/lang.rs index ec64c55..31ed5a9 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -15,7 +15,6 @@ pub trait Language { /// Languages available in `num2words` #[allow(non_camel_case_types)] -#[derive(Debug, Clone, Copy)] pub enum Lang { /// ``` /// use num2words::{Num2Words, Lang}; From 5b7b311e218c90156a5abe055d8ceee7785d2d07 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 14 Apr 2024 17:19:16 +0000 Subject: [PATCH 48/52] First Benchmarks result - Using vanilla For Loop --- Cargo.toml | 7 +++++++ benches/num2words.rs | 38 +++++++++++++++++++++++++++++++++ benches/test results.txt | 30 +++++++++++++++++++++++++++ criterion.toml | 45 ++++++++++++++++++++++++++++++++++++++++ src/lang/lang.rs | 33 ++++++++++++++++++++++------- 5 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 benches/num2words.rs create mode 100644 benches/test results.txt create mode 100644 criterion.toml diff --git a/Cargo.toml b/Cargo.toml index 8e97e78..5d9b307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,5 +20,12 @@ path = "src/lib.rs" name = "num2words" path = "src/bin/bin.rs" +[[bench]] +name = "num2words" +harness = false + [dependencies] num-bigfloat = { version = "^1.7.1", default-features = false } + +[dev-dependencies] +criterion = "0.5.1" diff --git a/benches/num2words.rs b/benches/num2words.rs new file mode 100644 index 0000000..1100a71 --- /dev/null +++ b/benches/num2words.rs @@ -0,0 +1,38 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use num2words::Num2Words; +use num_bigfloat::BigFloat; +// Criterion's quick start guide https://bheisler.github.io/criterion.rs/book/getting_started.html + +pub fn criterion_benchmark(c: &mut Criterion) { + //rewrite consts so they are instantiated here instead + let quadrillions = BigFloat::from(121_121_121_121_121u64); + let trillions = BigFloat::from(121_121_121_121u64); + let billions = BigFloat::from(121_121_121); + let millions = BigFloat::from(121_121_121); + let thousands = BigFloat::from(121_121); + let hundreds = BigFloat::from(121); + use num2words::Lang::*; + for (language, name) in [English, French, French_BE, French_CH, Spanish, Ukrainian] + .map(|lang| (lang, format!("{lang:?}"))) + { + c.bench_function(name.as_str(), |b| { + b.iter(|| { + for num in [ + quadrillions, + trillions, + billions, + millions, + thousands, + hundreds, + ] + .iter() + { + let driver = Num2Words::new(*num).lang(language); + black_box(driver.cardinal().to_words()).unwrap(); + } + }) + }); + } +} +criterion_group!(benches, criterion_benchmark,); +criterion_main!(benches); diff --git a/benches/test results.txt b/benches/test results.txt new file mode 100644 index 0000000..28662fb --- /dev/null +++ b/benches/test results.txt @@ -0,0 +1,30 @@ +//8GB RAM 2-cores Codespace +English time: [159.08 µs 161.34 µs 163.93 µs] +Found 14 outliers among 100 measurements (14.00%) + 4 (4.00%) high mild + 10 (10.00%) high severe + +French time: [157.37 µs 159.43 µs 161.73 µs] +Found 18 outliers among 100 measurements (18.00%) + 6 (6.00%) high mild + 12 (12.00%) high severe + +French_BE time: [157.24 µs 159.91 µs 163.33 µs] +Found 16 outliers among 100 measurements (16.00%) + 3 (3.00%) high mild + 13 (13.00%) high severe + +French_CH time: [157.60 µs 159.73 µs 162.13 µs] +Found 18 outliers among 100 measurements (18.00%) + 11 (11.00%) high mild + 7 (7.00%) high severe + +Spanish time: [17.792 µs 18.141 µs 18.551 µs] +Found 15 outliers among 100 measurements (15.00%) + 2 (2.00%) high mild + 13 (13.00%) high severe + +Ukrainian time: [158.49 µs 160.56 µs 162.99 µs] +Found 19 outliers among 100 measurements (19.00%) + 6 (6.00%) high mild + 13 (13.00%) high severe \ No newline at end of file diff --git a/criterion.toml b/criterion.toml new file mode 100644 index 0000000..9b97161 --- /dev/null +++ b/criterion.toml @@ -0,0 +1,45 @@ +# This is used to override the directory where cargo-criterion saves +# its data and generates reports. +criterion_home = "./target/criterion" + +# This is used to configure the format of cargo-criterion's command-line output. +# Options are: +# criterion: Prints confidence intervals for measurement and throughput, and +# indicates whether a change was detected from the previous run. The default. +# quiet: Like criterion, but does not indicate changes. Useful for simply +# presenting output numbers, eg. on a library's README. +# verbose: Like criterion, but prints additional statistics. +# bencher: Emulates the output format of the bencher crate and nightly-only +# libtest benchmarks. +output_format = "criterion" + +# This is used to configure the plotting backend used by cargo-criterion. +# Options are "gnuplot" and "plotters", or "auto", which will use gnuplot if it's +# available or plotters if it isn't. +ploting_backend = "auto" + +# The colors table allows users to configure the colors used by the charts +# cargo-criterion generates. +[colors] +# These are used in many charts to compare the current measurement against +# the previous one. +current_sample = {r = 31, g = 120, b = 180} +previous_sample = {r = 7, g = 26, b = 28} + +# These are used by the full PDF chart to highlight which samples were outliers. +not_an_outlier = {r = 31, g = 120, b = 180} +mild_outlier = {r = 5, g = 127, b = 0} +severe_outlier = {r = 7, g = 26, b = 28} + +# These are used for the line chart to compare multiple different functions. +comparison_colors = [ + {r = 8, g = 34, b = 34}, + {r = 6, g = 139, b = 87}, + {r = 0, g = 139, b = 139}, + {r = 5, g = 215, b = 0}, + {r = 0, g = 0, b = 139}, + {r = 0, g = 20, b = 60}, + {r = 9, g = 0, b = 139}, + {r = 0, g = 255, b = 127}, +] + diff --git a/src/lang/lang.rs b/src/lang/lang.rs index 31ed5a9..772dad8 100644 --- a/src/lang/lang.rs +++ b/src/lang/lang.rs @@ -15,6 +15,7 @@ pub trait Language { /// Languages available in `num2words` #[allow(non_camel_case_types)] +#[derive(Copy, Clone, Debug)] pub enum Lang { /// ``` /// use num2words::{Num2Words, Lang}; @@ -116,10 +117,16 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); - Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::FR)) + Box::new(lang::French::new( + feminine, + reformed, + lang::fr::RegionFrench::FR, + )) } Lang::French_BE => { let feminine = preferences @@ -128,10 +135,16 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); - Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::BE)) + Box::new(lang::French::new( + feminine, + reformed, + lang::fr::RegionFrench::BE, + )) } Lang::French_CH => { let feminine = preferences @@ -140,11 +153,17 @@ pub fn to_language(lang: Lang, preferences: Vec) -> Box { .is_some(); let reformed = preferences .iter() - .find(|v: &&String| ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str())) + .find(|v: &&String| { + ["reformed", "1990", "rectifié", "rectification"].contains(&v.as_str()) + }) .is_some(); - Box::new(lang::French::new(feminine, reformed, lang::fr::RegionFrench::CH)) - } + Box::new(lang::French::new( + feminine, + reformed, + lang::fr::RegionFrench::CH, + )) + } Lang::Spanish => { use super::es::{DecimalChar, NegativeFlavour}; let neg_flavour = preferences From b1630258d9465995d2e6c971f393980eb4559954 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Tue, 16 Apr 2024 21:47:44 -0500 Subject: [PATCH 49/52] Sync Bench branch with latest Spanish Impl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Tens edge case for Ordinals based on [Real Academia Española](https://www.rae.es/dpd/ordinales) standards, the TENS of 2 have no space between itself and its units. `"la primera y a la segunda decena se pueden escribir en una o en dos palabras, siendo hoy mayoritaria y siempre preferible la grafía univerbal"` * Fix Ordinals Representation From https://github.com/Ballasi/num2words/pull/29#discussion_r1565124539, fix ordinal representation based on the rules defined in 2.b && 2.d @ https://www.rae.es/dpd/ordinales * Fix Tests & remove comments * Currency Translation for spanish * Cardinal Fix - Mistakenly used English semantics (#1) Fix the wrong semantics that was used. Spanish semantics follows this pattern of for each big milliard (Million, Billion, Trillion) grows at a pace of `1 000 000`^n This means that 1 million is `1 000 000`^1 1 billion => `1 000 000`^2 [1 000_000 000_000] 1 trillion => `1 000 000`^3 [1 000_000 000_000 000_000] 1 quadrillion => `1 000 000`^4 [......] ...... etc This semantic differs from english's 1 billion => `1 000`^3 [1 000 000_000] 1 trillion => `1 000 000`^4 [1 000_000 000_000] 1 quadrillion => `1 000 000`^4 [......] --- src/lang/es.rs | 392 +++++++++++++++++++++++++++++++++++------- tests/lang_es_test.rs | 17 +- 2 files changed, 341 insertions(+), 68 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index dae7dec..5ec0403 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use num_bigfloat::BigFloat; use super::Language; -use crate::Num2Err; +use crate::{Currency, Num2Err}; // Reference that can hopefully be implemented seamlessly: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol const UNIDADES: [&str; 10] = ["", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve"]; @@ -52,13 +52,12 @@ const CENTENAS: [&str; 10] = [ // To ensure both arrays doesn't desync const MILLAR_SIZE: usize = 22; /// from source: https://es.wikipedia.org/wiki/Anexo:Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol -/// Based on https://en.wikipedia.org/wiki/Names_of_large_numbers, each thousands is from the Short Scales, -/// which each thousands can be defined as 10^(3n+3) magnitude, where n is replaced by the index of -/// the Array. For example 10^3 = Thousands (starts at n=1 here) -/// 10^6 = Millions -/// 10^9 = Billions -/// 10^33 = Decillion -// Saltos en Millares +/// The amount of zeros after the unit of a particular milliard, can be calculated through +/// ((Index of the Milliard) * 2 - 2) * 3 [Is the index to get the milliard. Index 21 gets +/// vigintillion] `(Index of the Milliard) * 6 - 6` [If we de-factorize] +/// For example, Trillion is stored at Index 4, so the amount of zeros after the unit is 4 * 6 - 6 = +/// `18` let i = (Millare's index) - 2 +/// let zeros = (i * 2 ) const MILLARES: [&str; MILLAR_SIZE] = [ "", "mil", @@ -263,7 +262,183 @@ impl Spanish { thousands } - // Only should be called if you're sure the number has no fraction + fn currencies(&self, currency: Currency, plural_form: bool) -> String { + let dollar: &str = match currency { + Currency::AED => "dirham{}", + Currency::ARS => "peso{} argentino{}", + Currency::AUD => { + if plural_form { + "dólares australianos" + } else { + "dólar australiano" + } + } + Currency::BRL => { + if plural_form { + "reales brasileño" + } else { + "real brasileño" + } + } + Currency::CAD => { + if plural_form { + "dólares canadienses" + } else { + "dólar canadiense" + } + } + Currency::CHF => "franco{} suizo{}", + Currency::CLP => "peso{} chileno{}", + Currency::CNY => { + if plural_form { + "yuanes" + } else { + "yuan" + } + } + Currency::COP => "peso{} colombiano{}", + Currency::CRC => { + if plural_form { + "colones" + } else { + "colón" + } + } + Currency::DINAR => { + if plural_form { + "dinares" + } else { + "dinar" + } + } + Currency::DOLLAR => { + if plural_form { + "dólares" + } else { + "dólar" + } + } + Currency::DZD => { + if plural_form { + "dinares argelinos" + } else { + "dinar argelino" + } + } + Currency::EUR => "euro{}", + Currency::GBP => "libra{} esterlina{}", + Currency::HKD => { + if plural_form { + "dólares de Hong Kong" + } else { + "dólar de Hong Kong" + } + } + Currency::IDR => "rupia{} indonesia{}", + Currency::ILS => { + // https://www.rae.es/dpd/s%C3%A9quel + if plural_form { "séqueles" } else { "séquel" } + } + Currency::INR => "rupia{}", + Currency::JPY => { + if plural_form { + "yenes" + } else { + "yen" + } + } + Currency::KRW => "won{}", + Currency::KWD => { + if plural_form { + "dinares kuwaitíes" + } else { + "dinar kuwaití" + } + } + Currency::KZT => "tenge{}", + Currency::MXN => "peso{} mexicano{}", + Currency::MYR => "ringgit{}", + Currency::NOK => "corona{} noruega{}", + Currency::NZD => { + if plural_form { + "dólares neozelandeses" + } else { + "dólar neozelandés" + } + } + Currency::PEN => { + if plural_form { + "soles" + } else { + "sol" + } + } + Currency::PESO => "peso{}", + Currency::PHP => "peso{} filipino{}", + Currency::PLN => "zloty{}", + Currency::QAR => { + if plural_form { + "riyales cataríes" + } else { + "riyal catarí" + } + } + Currency::RIYAL => { + if plural_form { + "riyales" + } else { + "riyal" + } + } + Currency::RUB => "rublo{} ruso{}", + Currency::SAR => { + if plural_form { + "riyales saudíes" + } else { + "riyal saudí" + } + } + Currency::SGD => { + if plural_form { + "dólares singapurenses" + } else { + "dólar singapurense" + } + } + Currency::THB => { + if plural_form { + "bahts tailandeses" + } else { + "baht tailandés" + } + } + Currency::TRY => "lira{}", + Currency::TWD => { + if plural_form { + "dólares taiwaneses" + } else { + "dólar taiwanes" + } + } + Currency::UAH => "grivna{}", + Currency::USD => { + if plural_form { + "dólares estadounidenses" + } else { + "dólar estadounidense" + } + } + Currency::UYU => "peso{} uruguayo{}", + Currency::VND => "dong{}", + Currency::ZAR => "rand{} sudafricano{}", + }; + dollar.replace("{}", if plural_form { "s" } else { "" }) + } + + fn cents(&self, currency: Currency, plural_form: bool) -> String { + currency.default_subunit_string("centavo{}", plural_form) + } + fn int_to_cardinal(&self, num: BigFloat) -> Result { // Don't convert a number with fraction, NaN or Infinity if !num.frac().is_zero() || num.is_nan() || num.is_inf() { @@ -275,7 +450,8 @@ impl Spanish { } let mut words = vec![]; - for (i, triplet) in self.en_miles(num.int()).into_iter().enumerate().rev() { + let triplets = self.en_miles(num); + for (i, triplet) in triplets.iter().copied().enumerate().rev() { let hundreds = ((triplet / 100) % 10) as usize; let tens = ((triplet / 10) % 10) as usize; let units = (triplet % 10) as usize; @@ -294,7 +470,13 @@ impl Spanish { // case `1_001_000` => `un millón mil` instead of `un millón un mil` // Explanation: Second triplet is always read as thousand, so we // don't need to say "un mil" - (_, 1) if triplet == 1 => "", + (_, i) if triplet == 1 && i > 0 => { + if i % 2 == 0 { + "un" + } else { + "" + } + } // case `001_001_100...` => `un billón un millón cien mil...` instead of // `uno billón uno millón cien mil...` // All `triplets == 1`` can can be named as "un". except for first or second @@ -331,16 +513,33 @@ impl Spanish { } } + /* + Explanation + 011 010 009 008 007 006 005 004 003 002 001 000 [This is the index of milliard in triplet format] + x 6 x 5 x 4 x 3 x 2 x [The actual Index we should be calling, x is replaced by 1] + 1 : Thousand + 2 : Million + 3 : Billion + 4 : Trillion + 5 : Quadrillion + 6 : Quintillion + */ + let milliard_index = if i % 2 == 0 { i / 2 + 1 } else { 1 }; + // Triplet of the last iteration + let last_triplet = triplets.get(i + 1).copied().unwrap_or(0); + if i == 0 { + continue; + } // Add the next Milliard if there's any. - if i != 0 && triplet != 0 { - if i > MILLARES.len() - 1 { + if (triplet != 0) || (last_triplet != 0 && milliard_index > 1) { + if milliard_index > MILLARES.len() - 1 { return Err(Num2Err::CannotConvert); } // Boolean that checks if next Milliard is plural - let plural = triplet != 1; + let plural = triplet > 1 || last_triplet > 0; match plural { - false => words.push(String::from(MILLAR[i])), - true => words.push(String::from(MILLARES[i])), + false => words.push(String::from(MILLAR[milliard_index])), + true => words.push(String::from(MILLARES[milliard_index])), } } } @@ -465,34 +664,49 @@ impl Language for Spanish { .rev() .filter(|(_, triplet)| *triplet != 0) { - let hundreds = ((triplet / 100) % 10) as usize; - let tens = ((triplet / 10) % 10) as usize; - let units = (triplet % 10) as usize; - - if hundreds > 0 { - // case `500` => `quingentesim@` - words.push(String::from(CENTENAS[hundreds]) + gender()); - } - - if tens != 0 || units != 0 { - let unit_word = UNIDADES[units]; - match tens { - // case `?_001` => `? primer` - 0 if triplet == 1 && i > 0 => words.push(String::from("primer")), - 0 => words.push(String::from(unit_word) + gender()), - // case `?_119` => `? centésim@ decimonoven@` - // case `?_110` => `? centésim@ decim@` - 1 => words.push(String::from(DIECIS[units]) + gender()), - _ => { - let ten = DECENAS[tens]; - let word = match units { - // case `?_120 => `? centésim@ vigésim@` - 0 => String::from(ten), - // case `?_122 => `? centésim@ vigésim@ segund@` - _ => format!("{ten}{} {unit_word}", gender()), - }; + if i == 0 { + let hundreds = ((triplet / 100) % 10) as usize; + let tens = ((triplet / 10) % 10) as usize; + let units = (triplet % 10) as usize; + + if hundreds > 0 { + // case `500` => `quingentesim@` + words.push(String::from(CENTENAS[hundreds]) + gender()); + } - words.push(word + gender()); + if tens != 0 || units != 0 { + let unit_word = UNIDADES[units]; + let decenas = || -> String { + // As lazy operation because there's no guarantees we will + // inmediately use the String + match units { + 7 => DECENAS[tens].replace('é', "e"), + _ => String::from(DECENAS[tens]), + } + }; + match tens { + // case `?_001` => `? primer@` + 0 => words.push(String::from(unit_word) + gender()), + // case `?_119` => `? centésim@ decimonoven@` + // case `?_110` => `? centésim@ decim@` + 1 => words.push(String::from(DIECIS[units]) + gender()), + 2 if units != 0 => words.push( + // case `122 => `? centésim@ vigésim@segund@` + // for DECENAS[1..=2], the unit word actually stays sticked to the + // DECENAS + decenas() + format!("{g}{unit_word}{g}", g = gender()).as_str(), + ), + _ => { + let ten = decenas(); + let word = match units { + // case `?_120 => `? centésim@ vigésim@` + 0 => ten, + // case `?_132 => `? centésim@ trigésim@ segund@` + _ => format!("{ten}{} {unit_word}", gender()), + }; + + words.push(word + gender()); + } } } } @@ -501,7 +715,17 @@ impl Language for Spanish { if i > MILLARES.len() - 1 { return Err(Num2Err::CannotConvert); } - words.push(String::from(MILLARES[i]) + gender()); + // from `2.b` in https://www.rae.es/dpd/ordinales + // Quote: + // ```Los ordinales complejos de la serie de los millares, los millones, los + // billones, etc., en la práctica inusitados, se forman prefijando al ordinal + // simple el cardinal que lo multiplica, y posponiendo los ordinales + // correspondientes a los órdenes inferiores``` + let unit_word = match triplet { + 1 => String::from(""), + _ => self.to_cardinal(triplet.into())?, + }; + words.push(format!("{}{}{}", unit_word, MILLARES[i], gender())); } } if self.plural { @@ -582,18 +806,18 @@ impl Language for Spanish { /// /// let words = /// Num2Words::new(-2021).lang(Lang::Spanish).currency(Currency::USD).to_words().unwrap(); - /// assert_eq!(words, "menos dos mil veintiún US dollars"); + /// assert_eq!(words, "menos dos mil veintiún dólares estadounidenses"); /// /// let words = /// Num2Words::new(81.21).lang(Lang::Spanish).currency(Currency::USD).to_words().unwrap(); - /// assert_eq!(words, "ochenta y un US dollars con veintiún centavos"); + /// assert_eq!(words, "ochenta y un dólares estadounidenses con veintiún centavos"); /// /// let words = /// Num2Words::new(1.01).lang(Lang::Spanish).currency(Currency::USD).to_words().unwrap(); - /// assert_eq!(words, "un US dollar con un centavo"); + /// assert_eq!(words, "un dólar estadounidense con un centavo"); /// /// let words = Num2Words::new(1).lang(Lang::Spanish).currency(Currency::USD).to_words().unwrap(); - /// assert_eq!(words, "un US dollar"); + /// assert_eq!(words, "un dólar estadounidense"); /// ``` fn to_currency(&self, num: BigFloat, currency: crate::Currency) -> Result { let strip_uno_into_un = |string: String| -> String { @@ -609,13 +833,13 @@ impl Language for Spanish { if num.is_nan() { Err(Num2Err::CannotConvert) } else if num.is_inf() { - let currency = currency.default_string(true); + let currency = self.currencies(currency, true); let inf = self.inf_to_cardinal(&num)? + "de {}"; let word = inf.replace("{}", ¤cy); return Ok(word); } else if num.frac().is_zero() { let is_plural = num.int() != 1.into(); - let currency = currency.default_string(is_plural); + let currency = self.currencies(currency, is_plural); let cardinal = strip_uno_into_un(self.int_to_cardinal(num)?); return Ok(format!("{cardinal} {currency}")); } else { @@ -626,7 +850,7 @@ impl Language for Spanish { self.to_currency(integral, currency)?, strip_uno_into_un(self.int_to_cardinal(cents)?), ); - let cents_suffix = currency.default_subunit_string("centavo{}", cents_is_plural); + let cents_suffix = self.cents(currency, cents_is_plural); if cents.is_zero() { return Ok(int_words); @@ -744,6 +968,38 @@ mod tests { assert_eq!(es.int_to_cardinal(to(800)).unwrap(), "ochocientos"); } + #[test] + fn lang_es_milliards() { + let es = Spanish::default(); + assert_eq!(es.int_to_cardinal(to(1_000_000)).unwrap(), "un millón"); + assert_eq!(es.int_to_cardinal(to(1_000_000_000)).unwrap(), "mil millones"); + assert_eq!(es.int_to_cardinal(to(1_000_000_000_000.0f64)).unwrap(), "un billón"); + assert_eq!(es.int_to_cardinal(to(1_000_000_000_000_000_000.0f64)).unwrap(), "un trillón"); + assert_eq!( + es.int_to_cardinal(to(9_008_001_006_000_000_000_000_000_000.0f64)).unwrap(), + "nueve mil ocho cuatrillones mil seis trillones" + ); + assert_eq!( + es.int_to_cardinal(to(9_008_000_001_000_000_000_000_000_000.0f64)).unwrap(), + "nueve mil ocho cuatrillones un trillón" + ); + assert_eq!( + es.int_to_cardinal(to(8_007_006_005_000_000_000_000_000.0f64)).unwrap(), + "ocho cuatrillones siete mil seis trillones cinco mil billones" + ); + assert_eq!( + es.int_to_cardinal(to(8_007_000_005_000_000_000_000_000.0f64)).unwrap(), + "ocho cuatrillones siete mil trillones cinco mil billones" + ); + assert_eq!( + es.int_to_cardinal(to(8_007_006_000_000_000_000_000_000.0f64)).unwrap(), + "ocho cuatrillones siete mil seis trillones" + ); + assert_eq!( + es.int_to_cardinal(to(8_007_000_000_001_000_000_000_000.0f64)).unwrap(), + "ocho cuatrillones siete mil trillones un billón" + ); + } #[test] fn lang_es_thousands() { let es = Spanish::default(); @@ -805,10 +1061,22 @@ mod tests { let es = Spanish::default(); let to_cardinal = Language::to_cardinal; assert_eq!(to_cardinal(&es, to(f64::NAN)).unwrap_err(), Num2Err::CannotConvert); - // Vigintillion supposedly has 63 zeroes, so anything beyond ~66 digits should fail with - // current impl - let some_big_num = BigFloat::from_u8(2).pow(&BigFloat::from_u8(230)); - assert_eq!(to_cardinal(&es, to(some_big_num)).unwrap_err(), Num2Err::CannotConvert); + // unit of Vigintillion, which is at index 21 has 120 zeros, so anything beyond 120+6 digits + // should fail + let some_big_num = BigFloat::from_u8(2).pow(&BigFloat::from_u16(418)); + + assert_eq!( + to_cardinal(&es, to(some_big_num)).unwrap(), /* There's no guarantee that this + * number is correct */ + "seiscientos setenta y seis mil novecientos veintiún vigintillones trescientos doce \ + mil cuarenta y uno novendecillones doscientos catorce mil quinientos sesenta y cinco \ + octodecillones trescientos veintiseis mil setecientos sesenta y uno septendecillones \ + doscientos setenta y cinco mil cuatrocientos veinticinco sexdecillones quinientos \ + cincuenta y siete mil quinientos cuarenta y cuatro quindeciollones setecientos \ + ochenta y cuatro mil trescientos cuatrodecillones" + ); + let too_big_num = BigFloat::from_u8(2).pow(&BigFloat::from_u16(419)); + assert_eq!(to_cardinal(&es, to(too_big_num)).unwrap_err(), Num2Err::CannotConvert); let to_ordinal = Language::to_ordinal; assert_eq!(to_ordinal(&es, to(0.001)).unwrap_err(), Num2Err::FloatingOrdinal); @@ -875,22 +1143,18 @@ mod tests { fn lang_es_ordinal() { let es = Spanish::default().with_feminine(true); let ordinal = |num: i128| es.to_ordinal(to(num)).unwrap(); - assert_eq!(ordinal(1_101_001), "primer millonésima centésima primera milésima primera"); - assert_eq!(ordinal(2_001_022), "segunda millonésima primer milésima vigésima segunda"); - assert_eq!( - ordinal(12_114_011), - "duodécima millonésima centésima decimocuarta milésima undécima" - ); + assert_eq!(ordinal(1_101_001), "millonésima ciento unomilésima primera"); + assert_eq!(ordinal(2_001_022), "dosmillonésima milésima vigésimasegunda"); + assert_eq!(ordinal(12_114_011), "docemillonésima ciento catorcemilésima undécima"); assert_eq!( ordinal(124_121_091), - "centésima vigésima cuarta millonésima centésima vigésima primera milésima nonagésima \ - primera" + "ciento veinticuatromillonésima ciento veintiunomilésima nonagésima primera" ); let es = Spanish::default().with_plural(true); let ordinal = |num: i128| es.to_ordinal(to(num)).unwrap(); assert_eq!( ordinal(124_001_091), - "centésimo vigésimo cuarto millonésimo primer milésimo nonagésimo primeros" + "ciento veinticuatromillonésimo milésimo nonagésimo primeros" ); } diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index a163cae..337349f 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -62,13 +62,22 @@ fn test_lang_es() { let word = driver(Outputs::Ordinal, BigFloat::from(141_100_211_021u64)).unwrap(); assert_eq!( word, - "centésimo cuadragésimo primero billonésimo centésimo millonésimo ducentésimo undécimo \ - milésimo vigésimo primero" + "ciento cuarenta y unobillonésimo cienmillonésimo doscientos oncemilésimo vigésimoprimero" ); assert_eq!(driver(Outputs::Ordinal, BigFloat::from(14)).unwrap(), "decimocuarto"); assert_eq!(driver(Outputs::Ordinal, BigFloat::from(1)).unwrap(), "primero"); assert_eq!(driver(Outputs::Ordinal, BigFloat::from(2)).unwrap(), "segundo"); assert_eq!(driver(Outputs::Ordinal, BigFloat::from(3)).unwrap(), "tercero"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(27)).unwrap(), "vigesimoséptimo"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(26)).unwrap(), "vigésimosexto"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(20)).unwrap(), "vigésimo"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(1000)).unwrap(), "milésimo"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(2000)).unwrap(), "dosmilésimo"); + assert_eq!(driver(Outputs::Ordinal, BigFloat::from(3100)).unwrap(), "tresmilésimo centésimo"); + assert_eq!( + driver(Outputs::Ordinal, BigFloat::from(54_223_231)).unwrap(), + "cincuenta y cuatromillonésimo doscientos veintitresmilésimo ducentésimo trigésimo primero" + ); let word = to_words(BigFloat::from(14), Outputs::OrdinalNum, &["f"]); assert_eq!(word.unwrap(), "14ª"); @@ -81,8 +90,8 @@ fn test_lang_es() { assert_eq!(word.unwrap(), "dos mil veintiuno a. C."); let word = to_words(BigFloat::from(21_001.21), Outputs::Currency, &[]); - assert_eq!(word.unwrap(), "veintiún mil un US dollars con veintiún centavos"); + assert_eq!(word.unwrap(), "veintiún mil un dólares estadounidenses con veintiún centavos"); let word = to_words(BigFloat::from(21.01), Outputs::Currency, &[]); - assert_eq!(word.unwrap(), "veintiún US dollars con un centavo"); + assert_eq!(word.unwrap(), "veintiún dólares estadounidenses con un centavo"); } From 233ccf7a29f4e5b961e3e10ef1f77c82b5b4775a Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Mon, 22 Apr 2024 07:42:32 -0500 Subject: [PATCH 50/52] Update code for benchmark --- benches/num2words.rs | 101 +++++++++++++++++++++++++++++++++---------- criterion.toml | 14 ++++++ 2 files changed, 91 insertions(+), 24 deletions(-) diff --git a/benches/num2words.rs b/benches/num2words.rs index 1100a71..4bbcdbf 100644 --- a/benches/num2words.rs +++ b/benches/num2words.rs @@ -1,38 +1,91 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use num2words::Num2Words; +use std::time::Duration; + +use criterion::measurement::WallTime; +use criterion::{ + black_box, criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, +}; +use num2words::{Currency, Num2Words}; use num_bigfloat::BigFloat; // Criterion's quick start guide https://bheisler.github.io/criterion.rs/book/getting_started.html - +fn group_with_config(bench_group: &mut BenchmarkGroup<'_, WallTime>) { + bench_group.measurement_time(Duration::from_secs(30)); + bench_group.sample_size(600); + bench_group.confidence_level(0.98); + bench_group.warm_up_time(Duration::from_secs(5)); +} pub fn criterion_benchmark(c: &mut Criterion) { - //rewrite consts so they are instantiated here instead + let nonillion = BigFloat::from(121_121_121_121_121_121_121_121_121_121_121u128); let quadrillions = BigFloat::from(121_121_121_121_121u64); let trillions = BigFloat::from(121_121_121_121u64); let billions = BigFloat::from(121_121_121); let millions = BigFloat::from(121_121_121); let thousands = BigFloat::from(121_121); let hundreds = BigFloat::from(121); + // from lowest to biggest + let numbers = [hundreds, thousands, millions, billions, trillions, quadrillions, nonillion]; + let numbers_name = + ["hundreds", "thousands", "millions", "billions", "trillions", "quadrillions", "nonillion"]; use num2words::Lang::*; - for (language, name) in [English, French, French_BE, French_CH, Spanish, Ukrainian] - .map(|lang| (lang, format!("{lang:?}"))) - { - c.bench_function(name.as_str(), |b| { - b.iter(|| { - for num in [ - quadrillions, - trillions, - billions, - millions, - thousands, - hundreds, - ] - .iter() - { - let driver = Num2Words::new(*num).lang(language); - black_box(driver.cardinal().to_words()).unwrap(); - } - }) - }); + let numbers_and_name = numbers.iter().zip(numbers_name.iter()).collect::>(); + let languages = [ + (Spanish, "ES"), + (English, "EN"), + (French, "FR"), + (French_BE, "FR_BE"), + (French_CH, "FR_CH"), + (Ukrainian, "UK"), + ]; + let mut group = c.benchmark_group("Num2Words to_cardinal()"); + group_with_config(&mut group); + + for row_data in languages.iter().copied() { + let (lang, language) = row_data; + for (num, number) in numbers_and_name.iter() { + let id = BenchmarkId::new(language, number); + group.bench_with_input(id, *num, |b, num| { + b.iter(|| { + let driver = Num2Words::new(*num).lang(lang); + black_box(driver.cardinal().to_words().unwrap()); + }) + }); + } + } + group.finish(); + + let mut group = c.benchmark_group("Num2Words to_ordinal()"); + group_with_config(&mut group); + + for row_data in languages.iter().copied() { + let (lang, language) = row_data; + for (num, number) in numbers_and_name.iter() { + let id = BenchmarkId::new(language, number); + group.bench_with_input(id, *num, |b, num| { + b.iter(|| { + let driver = Num2Words::new(*num).lang(lang); + black_box(driver.ordinal().to_words().unwrap()); + }) + }); + } + } + group.finish(); + + // to currency + let mut group = c.benchmark_group("Num2Words to_currency(Currency::USD)"); + group_with_config(&mut group); + + for row_data in languages.iter().copied() { + let (lang, language) = row_data; + for (num, number) in numbers_and_name.iter() { + let id = BenchmarkId::new(language, number); + group.bench_with_input(id, *num, |b, num| { + b.iter(|| { + let driver = Num2Words::new(*num).lang(lang); + black_box(driver.currency(Currency::USD).to_words().unwrap()); + }) + }); + } } + group.finish(); } criterion_group!(benches, criterion_benchmark,); criterion_main!(benches); diff --git a/criterion.toml b/criterion.toml index 9b97161..34d7bb7 100644 --- a/criterion.toml +++ b/criterion.toml @@ -1,3 +1,17 @@ +# Fast getting started guide: +# https://bheisler.github.io/criterion.rs/book/getting_started.html +# +# Following this guide: https://crates.io/crates/criterion-table/0.2.2 +# Installing the following binaries: +# cargo install cargo-criterion +# cargo install criterion-table +# run the following pipings for specific benchmarks +# cargo criterion --bench num2words --message-format=json > num2word_bench.json +# cat iterative_fib.json num2word_bench.json | criterion-table > BENCHMARKS.md +# Alternatively to do all at once +# cargo criterion --message-format=json | criterion-table > BENCHMARKS.md +# +# # This is used to override the directory where cargo-criterion saves # its data and generates reports. criterion_home = "./target/criterion" From 40429a0aea23e770b3691addce6af0618695ae63 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 26 May 2024 19:15:06 -0500 Subject: [PATCH 51/52] updated criterion.toml --- criterion.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/criterion.toml b/criterion.toml index 34d7bb7..49500cc 100644 --- a/criterion.toml +++ b/criterion.toml @@ -7,7 +7,7 @@ # cargo install criterion-table # run the following pipings for specific benchmarks # cargo criterion --bench num2words --message-format=json > num2word_bench.json -# cat iterative_fib.json num2word_bench.json | criterion-table > BENCHMARKS.md +# cat num2word_bench.json | criterion-table > BENCHMARKS.md # Alternatively to do all at once # cargo criterion --message-format=json | criterion-table > BENCHMARKS.md # From 6f6b66f13dd2d249bea401adaa55488046645d7b Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Mon, 27 May 2024 00:31:15 +0000 Subject: [PATCH 52/52] Fix numerical representation --- benches/num2words.rs | 76 ++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/benches/num2words.rs b/benches/num2words.rs index 4bbcdbf..b55501b 100644 --- a/benches/num2words.rs +++ b/benches/num2words.rs @@ -8,16 +8,16 @@ use num2words::{Currency, Num2Words}; use num_bigfloat::BigFloat; // Criterion's quick start guide https://bheisler.github.io/criterion.rs/book/getting_started.html fn group_with_config(bench_group: &mut BenchmarkGroup<'_, WallTime>) { - bench_group.measurement_time(Duration::from_secs(30)); - bench_group.sample_size(600); - bench_group.confidence_level(0.98); + bench_group.measurement_time(Duration::from_secs(20)); + bench_group.sample_size(1000); + bench_group.confidence_level(0.99); bench_group.warm_up_time(Duration::from_secs(5)); } pub fn criterion_benchmark(c: &mut Criterion) { let nonillion = BigFloat::from(121_121_121_121_121_121_121_121_121_121_121u128); - let quadrillions = BigFloat::from(121_121_121_121_121u64); - let trillions = BigFloat::from(121_121_121_121u64); - let billions = BigFloat::from(121_121_121); + let quadrillions = BigFloat::from(121_121_121_121_121_121u64); + let trillions = BigFloat::from(121_121_121_121_121u64); + let billions = BigFloat::from(121_121_121_121u64); let millions = BigFloat::from(121_121_121); let thousands = BigFloat::from(121_121); let hundreds = BigFloat::from(121); @@ -28,7 +28,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { use num2words::Lang::*; let numbers_and_name = numbers.iter().zip(numbers_name.iter()).collect::>(); let languages = [ - (Spanish, "ES"), + // (Spanish, "ES"), (English, "EN"), (French, "FR"), (French_BE, "FR_BE"), @@ -52,40 +52,40 @@ pub fn criterion_benchmark(c: &mut Criterion) { } group.finish(); - let mut group = c.benchmark_group("Num2Words to_ordinal()"); - group_with_config(&mut group); + // let mut group = c.benchmark_group("Num2Words to_ordinal()"); + // group_with_config(&mut group); - for row_data in languages.iter().copied() { - let (lang, language) = row_data; - for (num, number) in numbers_and_name.iter() { - let id = BenchmarkId::new(language, number); - group.bench_with_input(id, *num, |b, num| { - b.iter(|| { - let driver = Num2Words::new(*num).lang(lang); - black_box(driver.ordinal().to_words().unwrap()); - }) - }); - } - } - group.finish(); + // for row_data in languages.iter().copied() { + // let (lang, language) = row_data; + // for (num, number) in numbers_and_name.iter() { + // let id = BenchmarkId::new(language, number); + // group.bench_with_input(id, *num, |b, num| { + // b.iter(|| { + // let driver = Num2Words::new(*num).lang(lang); + // black_box(driver.ordinal().to_words().unwrap()); + // }) + // }); + // } + // } + // group.finish(); - // to currency - let mut group = c.benchmark_group("Num2Words to_currency(Currency::USD)"); - group_with_config(&mut group); + // // to currency + // let mut group = c.benchmark_group("Num2Words to_currency(Currency::USD)"); + // group_with_config(&mut group); - for row_data in languages.iter().copied() { - let (lang, language) = row_data; - for (num, number) in numbers_and_name.iter() { - let id = BenchmarkId::new(language, number); - group.bench_with_input(id, *num, |b, num| { - b.iter(|| { - let driver = Num2Words::new(*num).lang(lang); - black_box(driver.currency(Currency::USD).to_words().unwrap()); - }) - }); - } - } - group.finish(); + // for row_data in languages.iter().copied() { + // let (lang, language) = row_data; + // for (num, number) in numbers_and_name.iter() { + // let id = BenchmarkId::new(language, number); + // group.bench_with_input(id, *num, |b, num| { + // b.iter(|| { + // let driver = Num2Words::new(*num).lang(lang); + // black_box(driver.currency(Currency::USD).to_words().unwrap()); + // }) + // }); + // } + // } + // group.finish(); } criterion_group!(benches, criterion_benchmark,); criterion_main!(benches);