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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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 fd72e8edf50a63499a554798833c99690902bcf8 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Mon, 15 Apr 2024 04:20:37 +0000 Subject: [PATCH 48/57] Fix Tens edge case for Ordinals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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"` --- src/lang/es.rs | 17 +++++++++++++++-- tests/lang_es_test.rs | 5 ++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index dae7dec..6aa68b9 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -476,6 +476,14 @@ impl Language for Spanish { 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 if triplet == 1 && i > 0 => words.push(String::from("primer")), @@ -483,12 +491,17 @@ impl Language for Spanish { // 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[tens]; + let ten = decenas(); let word = match units { // case `?_120 => `? centésim@ vigésim@` 0 => String::from(ten), - // case `?_122 => `? centésim@ vigésim@ segund@` + // case `?_132 => `? centésim@ trigésim@ segund@` _ => format!("{ten}{} {unit_word}", gender()), }; diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index a163cae..9274bc1 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -63,12 +63,15 @@ fn test_lang_es() { assert_eq!( word, "centésimo cuadragésimo primero billonésimo centésimo millonésimo ducentésimo undécimo \ - milésimo vigésimo primero" + milé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"); let word = to_words(BigFloat::from(14), Outputs::OrdinalNum, &["f"]); assert_eq!(word.unwrap(), "14ª"); From 35d13947388336ce63fa8c2582a491dd479216f9 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Mon, 15 Apr 2024 05:31:53 +0000 Subject: [PATCH 49/57] 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 --- src/lang/es.rs | 95 ++++++++++++++++++++++++------------------- tests/lang_es_test.rs | 10 ++++- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 6aa68b9..1adf9c0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -465,47 +465,50 @@ 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]; - 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 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()), - 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 => String::from(ten), - // case `?_132 => `? centésim@ trigé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 if triplet < 10 && i > 0 => words.push(String::from("")), + 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 => String::from(ten), + // case `?_132 => `? centésim@ trigésim@ segund@` + _ => format!("{ten}{} {unit_word}", gender()), + }; + + words.push(word + gender()); + } } } } @@ -514,7 +517,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 { diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index 9274bc1..2309041 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -62,8 +62,7 @@ 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ésimoprimero" + "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"); @@ -72,6 +71,13 @@ fn test_lang_es() { 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ª"); From ac222d707bebbd159ed0fc6adedd50f432f6c4ff Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Mon, 15 Apr 2024 05:41:57 +0000 Subject: [PATCH 50/57] Fix Tests & remove comments --- src/lang/es.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 1adf9c0..2fd30c7 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -486,8 +486,7 @@ impl Language for Spanish { } }; match tens { - // case `?_001` => `? primer` - // 0 if triplet < 10 && i > 0 => words.push(String::from("")), + // case `?_001` => `? primer@` 0 => words.push(String::from(unit_word) + gender()), // case `?_119` => `? centésim@ decimonoven@` // case `?_110` => `? centésim@ decim@` @@ -901,22 +900,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" ); } From 5e9f3aacccfc38beab1fd0cef6ea6037b4d463ce Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:20:41 -0500 Subject: [PATCH 51/57] Currency Translation for spanish --- src/lang/es.rs | 197 +++++++++++++++++++++++++++++++++++++++--- tests/lang_es_test.rs | 4 +- 2 files changed, 189 insertions(+), 12 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 2fd30c7..91d5615 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"]; @@ -263,6 +263,183 @@ impl Spanish { thousands } + 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) + } + // 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 @@ -481,7 +658,7 @@ impl Language for Spanish { // As lazy operation because there's no guarantees we will // inmediately use the String match units { - 7 => DECENAS[tens].replace("é", "e"), + 7 => DECENAS[tens].replace('é', "e"), _ => String::from(DECENAS[tens]), } }; @@ -501,7 +678,7 @@ impl Language for Spanish { let ten = decenas(); let word = match units { // case `?_120 => `? centésim@ vigésim@` - 0 => String::from(ten), + 0 => ten, // case `?_132 => `? centésim@ trigésim@ segund@` _ => format!("{ten}{} {unit_word}", gender()), }; @@ -607,18 +784,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 { @@ -634,13 +811,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 { @@ -651,7 +828,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); diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index 2309041..337349f 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -90,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 b5c00883598af449cb9d25a3a0fbea25e7f348cb Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Tue, 16 Apr 2024 19:09:03 -0500 Subject: [PATCH 52/57] 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 | 104 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 19 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 91d5615..5ec0403 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -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", @@ -440,7 +439,6 @@ impl Spanish { currency.default_subunit_string("centavo{}", plural_form) } - // 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() { @@ -452,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; @@ -471,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 @@ -508,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])), } } } @@ -946,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(); @@ -1007,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); From 904ee0958461e2abe53b54e8af04c64d2fa5ea99 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Wed, 17 Apr 2024 21:17:46 -0500 Subject: [PATCH 53/57] Fix tests due to incorrect Milliard Semantic --- src/lang/es.rs | 29 +++++++++++------------------ tests/lang_es_test.rs | 6 +++--- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 5ec0403..ffcecc5 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -1033,12 +1033,9 @@ mod tests { // This might make other tests trivial let es = Spanish::default(); // Triplet == 1 inserts following milliard in singular - 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.int_to_cardinal(to(2_002_002_000)).unwrap(), - "dos billones dos millones dos mil" - ); + assert_eq!(es.int_to_cardinal(to(1_000_000_001_000u64)).unwrap(), "un billón mil"); + // Following milliard in plural + assert_eq!(es.int_to_cardinal(to(2_002_002_000)).unwrap(), "dos mil dos millones dos mil"); // Thousand's milliard is singular assert_eq!(es.int_to_cardinal(to(1_100)).unwrap(), "mil cien"); // Thousand's milliard is plural @@ -1051,8 +1048,8 @@ mod tests { // "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.0)).unwrap(), - "ciento setenta y uno billones treinta y uno millones cuarenta y uno mil treinta y uno" + es.int_to_cardinal(to(1_000_000_041_031.0f64)).unwrap(), + "un billón cuarenta y uno mil treinta y uno" ); } @@ -1120,7 +1117,7 @@ mod tests { let es = Spanish::default(); assert_eq!( 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", + "ciento setenta y uno mil treinta y uno millones noventa y uno mil treinta y uno", ); assert!(!es.int_to_cardinal(to(171_031_091_031.0)).unwrap().contains(" un ")); } @@ -1130,7 +1127,7 @@ mod tests { let es = Spanish::default().with_veinte(true); assert_eq!( 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" + "veinte y un mil 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"); assert_eq!( @@ -1243,11 +1240,7 @@ mod tests { "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"); - assert_eq!( - es.int_to_cardinal(to(1_001_100_001)).unwrap(), - "un billón un millón cien mil uno" - ); + assert_eq!(es.int_to_cardinal(to(1_001_100_001)).unwrap(), "mil un millones cien mil uno"); } #[test] @@ -1266,7 +1259,7 @@ mod tests { assert_eq!(es.int_to_cardinal((-1_000_000).into()).unwrap(), "un millón negativo"); assert_eq!( es.int_to_cardinal((-1_020_010_000).into()).unwrap(), - "un billón veinte millones diez mil negativo" + "mil veinte millones diez mil negativo" ); es.set_neg_flavour(Prepended); @@ -1274,7 +1267,7 @@ mod tests { assert_eq!(es.int_to_cardinal((-1_000_000).into()).unwrap(), "menos un millón"); assert_eq!( es.int_to_cardinal((-1_020_010_000).into()).unwrap(), - "menos un billón veinte millones diez mil" + "menos mil veinte millones diez mil" ); es.set_neg_flavour(BelowZero); @@ -1282,7 +1275,7 @@ mod tests { assert_eq!(es.int_to_cardinal((-1_000_000).into()).unwrap(), "un millón bajo cero"); assert_eq!( es.int_to_cardinal((-1_020_010_000).into()).unwrap(), - "un billón veinte millones diez mil bajo cero" + "mil veinte millones diez mil bajo cero" ); } diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index 337349f..4369af5 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -40,11 +40,11 @@ fn test_lang_es() { .as_slice(), ) }; - let word = driver(Outputs::Cardinal, BigFloat::from(-821_442_524.69)).unwrap(); + let word = driver(Outputs::Cardinal, BigFloat::from(-3_000_821_442_524.69f64)).unwrap(); assert_eq!( word, - "ochocientos veintiún millones cuatrocientos cuarenta y dos mil quinientos veinticuatro \ - coma seis nueve negativo" + "tres billones 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 From c731f3e201b7aa5a0fdb13fc3d54c533b1139e74 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:54:33 -0500 Subject: [PATCH 54/57] remove unreachable code --- src/lang/es.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index ffcecc5..da1bab1 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -468,7 +468,7 @@ impl Spanish { let unit_word = match (units, i) { // 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 triplet is always read as thousand, so we + // Explanation: Odd triplets should always be read as thousand, so we // don't need to say "un mil" (_, i) if triplet == 1 && i > 0 => { if i % 2 == 0 { @@ -477,11 +477,6 @@ impl Spanish { "" } } - // 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], }; From 775604d034649780aacb2baf79b90935cbfd35e7 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Fri, 26 Apr 2024 23:55:03 -0500 Subject: [PATCH 55/57] Ordinal_fix (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Staging changes for ordinal Fix Some data to take into consideration Todo: Fix accent on monomorphized words i.e. VigesimoSegundo Si el ordinal se escribe en dos palabras, el primer elemento mantiene la tilde que le corresponde como palabra independiente: vigésimo segundo, vigésima cuarta, trigésimo octavo, cuadragésima quinta; pero, si se escribe en una sola palabra, el compuesto resultante, al ser ser una voz llana terminada en vocal, debe escribirse sin tilde, pues no le corresponde llevarla según las reglas de acentuación (v. cap. II, § 3.4.5.1.1): vigesimosegundo (no ⊗‍vigésimosegundo). Los ordinales complejos escritos en una sola palabra solo presentan variación de género y número en el segundo componente: vigesimoprimero, vigesimoprimera, vigesimoprimeros, vigesimoprimeras; pero, si se escriben en dos palabras, ambos componentes son variables: vigésimo primero, vigésima primera, vigésimos primeros, vigésimas primeras. No se consideran correctas las grafías en dos palabras si se mantiene invariable el primer componente: ⊗‍vigésimo segundos, ⊗‍vigésimo cuarta, ⊗‍vigésimo octavas. * temporarily add rustfmt back * Finish Ordinal Fix. Reference: https://www.rae.es/dpd/ordinales * remove accent on composites of `Vigesimo` * remove rustfmt for merge --- src/lang/es.rs | 103 +++++++++++++++++++++++++++++++----------- tests/lang_es_test.rs | 11 +++-- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index da1bab1..26c7b5d 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -651,14 +651,16 @@ 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() - .filter(|(_, triplet)| *triplet != 0) - { + let triplets = self.en_miles(num.int()); + let gender = || -> &'static str { + match (self.plural, self.feminine) { + (true, true) => "as", + (true, false) => "os", + (false, true) => "a", + (false, false) => "o", + } + }; + for (i, triplet) in triplets.iter().copied().enumerate().rev() { if i == 0 { let hundreds = ((triplet / 100) % 10) as usize; let tens = ((triplet / 10) % 10) as usize; @@ -675,7 +677,7 @@ impl Language for Spanish { // As lazy operation because there's no guarantees we will // inmediately use the String match units { - 7 => DECENAS[tens].replace('é', "e"), + 1..=9 if tens == 2 => DECENAS[tens].replace('é', "e"), _ => String::from(DECENAS[tens]), } }; @@ -686,7 +688,7 @@ impl Language for Spanish { // 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@` + // case `122 => `? centésim@ vigesim@segund@` // for DECENAS[1..=2], the unit word actually stays sticked to the // DECENAS decenas() + format!("{g}{unit_word}{g}", g = gender()).as_str(), @@ -704,30 +706,68 @@ impl Language for Spanish { } } } + continue; } + + 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); + // 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); } + if milliard_index == 1 && i > 1 { + // If we're indexing the thousand Milliard index we skip it + // because We will manually append it at the next milliard + continue; + } // 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())?, + let triplet_word = match triplet { + // I couldn't find any hard evidence whether bigger than single digits triplets + // should also be mono-worded with the milliard, so I'll assume they don't until + // otherwise because this way, something like "ciento unomilesima"(101_000) + // won't accidentally be misinterpreted as "1_000". + 10.. => self.to_cardinal(triplet.into())? + " ", + 2.. => self.to_cardinal(triplet.into())?, + _ => String::from(""), }; - words.push(format!("{}{}{}", unit_word, MILLARES[i], gender())); - } - } - if self.plural { - if let Some(word) = words.last_mut() { - word.push('s'); + // ciento cuarenta y uno milcien millonésimo doscientos once milésimo + // vigesimoprimero + + // ciento cuarenta y uno milcienmillonésimo doscientos oncemilésimo vigesimoprimero + let get_last_triplet = || -> Result { + match last_triplet { + 10.. => self.to_cardinal(last_triplet.into()).map(|word| word + " "), + 2.. => self.to_cardinal(last_triplet.into()), + _ => Ok(String::from("")), + } + }; + let thousand_of_milliard = match (milliard_index != 1, i > 1, last_triplet > 0) { + (true, true, true) => get_last_triplet()? + "mil", + (false, true, true) => unreachable!("Should be dead code"), + _ => String::from(""), + }; + words.push(format!( + "{}{}{}{}", + thousand_of_milliard, + triplet_word, + MILLARES[milliard_index], + gender() + )); } } + // if self.plural { + // if let Some(word) = words.last_mut() { + // word.push('s'); + // } + // } Ok(words.into_iter().filter(|word| !word.is_empty()).collect::>().join(" ")) } @@ -1135,18 +1175,29 @@ 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), "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(1_101_001), "millonésima ciento uno milésima primera"); + assert_eq!(ordinal(2_001_022), "dosmillonésima milésima vigesimasegunda"); + assert_eq!(ordinal(12_114_011), "doce millonésima ciento catorce milésima undécima"); assert_eq!( ordinal(124_121_091), - "ciento veinticuatromillonésima ciento veintiunomilésima nonagésima primera" + "ciento veinticuatro millonésima ciento veintiuno milésima nonagésima primera" ); + assert_eq!(ordinal(1_000_000_000), "milmillonésima"); let es = Spanish::default().with_plural(true); let ordinal = |num: i128| es.to_ordinal(to(num)).unwrap(); + assert_eq!(ordinal(101_000), "ciento uno milésimos"); + assert_eq!( ordinal(124_001_091), - "ciento veinticuatromillonésimo milésimo nonagésimo primeros" + "ciento veinticuatro millonésimos milésimos nonagésimos primeros" + ); + assert_eq!( + ordinal(124_001_091_000_000_000_001), + "ciento veinticuatro trillonésimos milnoventa y uno billonésimos primeros" + ); + assert_eq!( + ordinal(124_002_091_000_000_000_002), + "ciento veinticuatro trillonésimos dosmilnoventa y uno billonésimos segundos" ); } diff --git a/tests/lang_es_test.rs b/tests/lang_es_test.rs index 4369af5..7580e12 100644 --- a/tests/lang_es_test.rs +++ b/tests/lang_es_test.rs @@ -62,21 +62,26 @@ fn test_lang_es() { let word = driver(Outputs::Ordinal, BigFloat::from(141_100_211_021u64)).unwrap(); assert_eq!( word, - "ciento cuarenta y unobillonésimo cienmillonésimo doscientos oncemilésimo vigésimoprimero" + "ciento cuarenta y uno milcien millonésimo doscientos once milésimo vigesimoprimero" ); 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(26)).unwrap(), "vigesimosexto"); 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" + "cincuenta y cuatro millonésimo doscientos veintitres milésimo ducentésimo trigésimo \ + primero" + ); + assert_eq!( + driver(Outputs::Ordinal, BigFloat::from(1_223_231)).unwrap(), + "millonésimo doscientos veintitres milésimo ducentésimo trigésimo primero" ); let word = to_words(BigFloat::from(14), Outputs::OrdinalNum, &["f"]); From e7516ac38328551900028247f69ebbde8c32ac80 Mon Sep 17 00:00:00 2001 From: Laifsyn <99366187+Laifsyn@users.noreply.github.com> Date: Sun, 26 May 2024 23:25:07 -0500 Subject: [PATCH 56/57] Update split_thousand to match other langs implementation --- src/lang/es.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 26c7b5d..7bbd821 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -247,18 +247,17 @@ impl Spanish { Self { decimal_char, ..self } } - #[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 + fn split_thousands(&self, mut num: BigFloat) -> Vec { let mut thousands = Vec::new(); - let mil = 1000.into(); - num = num.abs(); - while !num.int().is_zero() { + let bf_1000 = BigFloat::from(1000); + + while !num.is_zero() { // Insertar en Low Endian - thousands.push((num % mil).to_u64().expect("triplet not under 1000")); - num /= mil; // DivAssign + thousands.push((num % bf_1000).to_u64().unwrap()); + num = num.div(&bf_1000).int(); } + println!("{:?}", thousands); thousands } @@ -450,7 +449,7 @@ impl Spanish { } let mut words = vec![]; - let triplets = self.en_miles(num); + let triplets = self.split_thousands(num); for (i, triplet) in triplets.iter().copied().enumerate().rev() { let hundreds = ((triplet / 100) % 10) as usize; let tens = ((triplet / 10) % 10) as usize; @@ -651,7 +650,7 @@ impl Language for Spanish { _ => (), /* Nothing Happens */ } let mut words = vec![]; - let triplets = self.en_miles(num.int()); + let triplets = self.split_thousands(num.int()); let gender = || -> &'static str { match (self.plural, self.feminine) { (true, true) => "as", From 8fddaec8f359b1c60d08e916b2ea36a7f62d48f7 Mon Sep 17 00:00:00 2001 From: Laifsyn Date: Thu, 20 Jun 2024 08:30:40 -0500 Subject: [PATCH 57/57] Remove unused println! call --- src/lang/es.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 7bbd821..061912b 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -257,7 +257,7 @@ impl Spanish { thousands.push((num % bf_1000).to_u64().unwrap()); num = num.div(&bf_1000).int(); } - println!("{:?}", thousands); + thousands }