1#![deny(missing_docs)]
4#![allow(clippy::non_ascii_literal)]
5#![doc = include_str!("../README.md")]
6
7use num_integer::Integer;
8use num_traits::{CheckedMul, FromPrimitive, ToPrimitive};
9use std::fmt::Display;
10
11#[derive(Debug)]
13pub struct Options {
14 pub feminine: bool,
17 pub reformed: bool,
21}
22
23pub static PRE_REFORM_MASCULINE: Options = Options {
25 feminine: false,
26 reformed: false,
27};
28
29pub static PRE_REFORM_FEMININE: Options = Options {
31 feminine: true,
32 reformed: false,
33};
34
35pub static POST_REFORM_MASCULINE: Options = Options {
37 feminine: false,
38 reformed: true,
39};
40
41pub static POST_REFORM_FEMININE: Options = Options {
43 feminine: true,
44 reformed: true,
45};
46
47#[allow(clippy::derivable_impls)] impl Default for Options {
49 fn default() -> Options {
50 Options {
51 ..POST_REFORM_MASCULINE
52 }
53 }
54}
55
56impl Options {
57 fn masculinize(&self) -> Self {
58 Options {
59 feminine: false,
60 ..*self
61 }
62 }
63}
64
65fn literal_for(value: usize, options: &Options) -> Option<String> {
66 static SMALLS: [&str; 21] = [
67 "zéro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", "dix",
68 "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit",
69 "dix-neuf", "vingt",
70 ];
71 let literal = if value == 1 && options.feminine {
72 Some("une")
73 } else if value <= 20 {
74 Some(SMALLS[value])
75 } else if value == 30 {
76 Some("trente")
77 } else if value == 40 {
78 Some("quarante")
79 } else if value == 50 {
80 Some("cinquante")
81 } else if value == 60 {
82 Some("soixante")
83 } else if value == 71 {
84 Some(if options.reformed {
85 "soixante-et-onze"
86 } else {
87 "soixante et onze"
88 })
89 } else if value == 80 {
90 Some("quatre-vingts")
91 } else if value == 81 {
92 Some(if options.feminine {
93 "quatre-vingt-une"
94 } else {
95 "quatre-vingt-un"
96 })
97 } else if value == 100 {
98 Some("cent")
99 } else if value == 1000 {
100 Some("mille")
101 } else {
102 None
103 };
104 literal.map(String::from)
105}
106
107fn add_unit_for(str: &mut String, prefix_count: usize, log1000: usize) -> bool {
108 static PREFIXES: [&str; 16] = [
109 "m",
110 "b",
111 "tr",
112 "quadr",
113 "quint",
114 "sext",
115 "sept",
116 "oct",
117 "non",
118 "déc",
119 "unodéc",
120 "duodéc",
121 "trédéc",
122 "quattuordéc",
123 "quindéc",
124 "sexdéc",
125 ];
126 PREFIXES.get(log1000 / 2).map_or(false, |prefix| {
127 str.push_str(prefix);
128 if log1000 % 2 == 0 {
129 str.push_str("illion");
130 } else {
131 str.push_str("illiard");
132 }
133 if prefix_count > 1 {
134 str.push('s');
135 }
136 true
137 })
138}
139
140fn unpluralize(str: &mut String) {
141 if str.ends_with("ts") {
142 str.truncate(str.len() - 1);
143 }
144}
145
146fn complete(mut str: String, n: usize, prefix_under_100: bool, options: &Options) -> String {
147 if n > 0 {
148 unpluralize(&mut str);
149 if n == 1 {
150 if prefix_under_100 && options.reformed {
151 str.push_str("-et-un");
152 } else if prefix_under_100 {
153 str.push_str(" et un");
154 } else if options.reformed {
155 str.push_str("-un");
156 } else {
157 str.push_str(" un");
158 }
159 if options.feminine {
160 str.push('e');
161 }
162 } else {
163 str.push(if options.reformed || (prefix_under_100 && n < 100) {
164 '-'
165 } else {
166 ' '
167 });
168 str.push_str(&basic(&n, options, false));
169 }
170 }
171 str
172}
173
174fn basic<N: Integer + FromPrimitive + ToPrimitive + Display>(
175 n: &N,
176 options: &Options,
177 negative: bool,
178) -> String {
179 n.to_usize()
180 .and_then(|n| {
181 literal_for(n, options).or_else(|| match n {
182 n if n < 60 => Some(smaller_than_60(n, options)),
183 n if n < 80 => Some(base_onto(60, n, options)),
184 n if n < 100 => Some(base_onto(80, n, options)),
185 n if n < 1000 => Some(smaller_than_1000(n, options)),
186 n if n < 2000 => Some(smaller_than_2000(n, options)),
187 n if n < 1_000_000 => Some(smaller_than_1000000(n, options)),
188 _ => None,
189 })
190 })
191 .map_or_else(
192 || over_1000000(n, options, negative),
193 |s| add_minus(s, negative),
194 )
195}
196
197fn smaller_than_60(n: usize, options: &Options) -> String {
198 let unit = n % 10;
199 complete(
200 basic(&(n - unit), &Options::default(), false),
201 unit,
202 true,
203 options,
204 )
205}
206
207fn base_onto(b: usize, n: usize, options: &Options) -> String {
208 complete(literal_for(b, options).unwrap(), n - b, true, options)
209}
210
211fn smaller_than_1000(n: usize, options: &Options) -> String {
212 let (hundredths, rest) = n.div_rem(&100);
213 let result = if hundredths > 1 {
214 let mut prefix = literal_for(hundredths, options).unwrap();
215 push_space_or_dash(&mut prefix, options);
216 prefix.push_str("cents");
217 prefix
218 } else {
219 String::from("cent")
220 };
221 complete(result, rest, false, options)
222}
223
224fn smaller_than_2000(n: usize, options: &Options) -> String {
225 complete(String::from("mille"), n - 1000, false, options)
226}
227
228fn push_space_or_dash(str: &mut String, options: &Options) {
229 str.push(if options.reformed { '-' } else { ' ' });
230}
231
232fn smaller_than_1000000(n: usize, options: &Options) -> String {
233 let (thousands, rest) = n.div_rem(&1000);
234 let prefix = if thousands > 1 {
235 let mut thousands = basic(&thousands, &options.masculinize(), false);
236 unpluralize(&mut thousands);
237 push_space_or_dash(&mut thousands, options);
238 thousands.push_str("mille");
239 thousands
240 } else {
241 String::from("mille")
242 };
243 complete(prefix, rest, false, options)
244}
245
246fn over_1000000<N: Integer + FromPrimitive + ToPrimitive + Display>(
247 n: &N,
248 options: &Options,
249 negative: bool,
250) -> String {
251 let thousand = N::from_u32(1000).unwrap();
252 let (mut num, small) = n.div_rem(&N::from_u32(1_000_000).unwrap());
253 let mut base = if small == N::zero() {
254 None
255 } else {
256 Some(basic(&small, options, false))
257 };
258 let mut log1000 = 0;
259 while num != N::zero() {
260 let (rest, prefix) = num.div_rem(&thousand);
261 let prefix = prefix.to_usize().unwrap();
262 if prefix > 0 {
263 let mut str = basic(&prefix, &options.masculinize(), false);
264 push_space_or_dash(&mut str, options);
265 if !add_unit_for(&mut str, prefix, log1000) {
266 return add_minus_digits(n, negative);
267 }
268 if let Some(base) = base {
269 push_space_or_dash(&mut str, options);
270 str.push_str(&base);
271 }
272 base = Some(str);
273 }
274 log1000 += 1;
275 num = rest;
276 }
277 base.map_or_else(|| add_minus_digits(n, negative), |s| add_minus(s, negative))
278}
279
280fn add_minus(s: String, negative: bool) -> String {
281 if negative {
282 format!("moins {s}")
283 } else {
284 s
285 }
286}
287
288fn add_minus_digits<N>(n: N, negative: bool) -> String
289where
290 N: Display,
291{
292 if negative {
293 format!("-{n}")
294 } else {
295 n.to_string()
296 }
297}
298
299pub fn french_number<N: Integer + FromPrimitive + ToPrimitive + Display + CheckedMul>(
324 n: &N,
325) -> String {
326 french_number_options(n, &Options::default())
327}
328
329pub fn french_number_options<N: Integer + FromPrimitive + ToPrimitive + Display + CheckedMul>(
354 n: &N,
355 options: &Options,
356) -> String {
357 if *n < N::zero() {
358 N::from_i8(-1)
362 .and_then(|m1| m1.checked_mul(n))
363 .map_or_else(|| n.to_string(), |n| basic(&n, options, true))
364 } else {
365 basic(n, options, false)
366 }
367}
368
369#[cfg(test)]
370mod tests {
371
372 use crate::{add_unit_for, basic, literal_for, unpluralize};
373
374 #[test]
375 fn test_literal_for() {
376 assert_eq!(
377 literal_for(30, &Default::default()),
378 Some(String::from("trente"))
379 );
380 assert_eq!(literal_for(31, &Default::default()), None);
381 }
382
383 #[test]
384 fn test_add_unit_for() {
385 let mut str = String::new();
386 assert!(add_unit_for(&mut str, 1, 0));
387 assert_eq!(str, "million");
388 str.clear();
389 assert!(add_unit_for(&mut str, 2, 0));
390 assert_eq!(str, "millions");
391 str.clear();
392 assert!(add_unit_for(&mut str, 1, 3));
393 assert_eq!(str, "billiard");
394 assert!(!add_unit_for(&mut str, 1, 97));
395 }
396
397 #[test]
398 fn test_unpluralize() {
399 let mut s = String::from("quatre-cents");
400 unpluralize(&mut s);
401 assert_eq!(s, "quatre-cent");
402 let mut s = String::from("cent");
403 unpluralize(&mut s);
404 assert_eq!(s, "cent");
405 }
406
407 #[test]
408 fn test_basic() {
409 assert_eq!(basic(&0, &Default::default(), false), "zéro");
410 assert_eq!(basic(&21, &Default::default(), false), "vingt-et-un");
411 assert_eq!(basic(&54, &Default::default(), false), "cinquante-quatre");
412 assert_eq!(basic(&64, &Default::default(), false), "soixante-quatre");
413 assert_eq!(basic(&71, &Default::default(), false), "soixante-et-onze");
414 assert_eq!(basic(&72, &Default::default(), false), "soixante-douze");
415 assert_eq!(basic(&80, &Default::default(), false), "quatre-vingts");
416 assert_eq!(basic(&81, &Default::default(), false), "quatre-vingt-un");
417 assert_eq!(basic(&91, &Default::default(), false), "quatre-vingt-onze");
418 assert_eq!(basic(&101, &Default::default(), false), "cent-un");
419 assert_eq!(basic(&800, &Default::default(), false), "huit-cents");
420 assert_eq!(basic(&803, &Default::default(), false), "huit-cent-trois");
421 assert_eq!(
422 basic(&872, &Default::default(), false),
423 "huit-cent-soixante-douze"
424 );
425 assert_eq!(
426 basic(&880, &Default::default(), false),
427 "huit-cent-quatre-vingts"
428 );
429 assert_eq!(
430 basic(&882, &Default::default(), false),
431 "huit-cent-quatre-vingt-deux"
432 );
433 assert_eq!(basic(&1001, &Default::default(), false), "mille-un");
434 assert_eq!(
435 basic(&1882, &Default::default(), false),
436 "mille-huit-cent-quatre-vingt-deux"
437 );
438 assert_eq!(basic(&2001, &Default::default(), false), "deux-mille-un");
439 assert_eq!(
440 basic(&300_001, &Default::default(), false),
441 "trois-cent-mille-un"
442 );
443 assert_eq!(
444 basic(&180_203, &Default::default(), false),
445 "cent-quatre-vingt-mille-deux-cent-trois"
446 );
447 assert_eq!(
448 basic(&180_203, &Default::default(), false),
449 "cent-quatre-vingt-mille-deux-cent-trois"
450 );
451 assert_eq!(
452 basic(&17_180_203, &Default::default(), false),
453 "dix-sept-millions-cent-quatre-vingt-mille-deux-cent-trois"
454 );
455 }
456}