french_pluralize/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
2
3mod exceptions;
4use grapheme_picker::{drop_last, drop_lasts, take_last, take_lasts};
5
6/**
7 * Takes any French word in the singular and return it in the plural
8 *  @see https://fr.wiktionary.org/wiki/Annexe:Pluriels_irr%C3%A9guliers_en_fran%C3%A7ais
9 */
10#[must_use]
11pub fn pluralize_word(word: &str) -> String {
12    // TODO: composed words
13    // ex: œil-de-bœuf -> œils-de-bœufs
14
15    // TODO: disambiguation
16    // "lieu" the place takes an "x", when "lieu" the fish take an "s"
17    // "travail" the work takes an "x", when "travail" the horse tool take an "s"
18    // "aïeul" the general ancestor pluralize to "aïeux", when "aïeul" direct grand-parent pluralize to "aïeuls"
19    // "œil" the eye pluralize to "yeux", when "œil" the needle's eye pluralize to "œils"
20    // "ciel" the sky pluralize to "cieux", when "ciel" the bed part pluralize to "ciels"
21    // "banal" the ordinary pluralize to "banals", when "banal" the publication pluralize to "banaux"
22
23    // TODO: two possibilities
24    // final -> finals / finaux
25    // ail -> ails / aulx
26
27    // 1) global exceptions
28    if word == "" {
29        return String::new();
30    }
31    if word == "ail" {
32        return String::from("aulx");
33    }
34    if word == "oeil" {
35        return String::from("yeux");
36    }
37    if word == "viel" {
38        return String::from("vieux");
39    }
40    // topos -> topoï
41    if word == "topos" {
42        return String::from("topo\u{ef}");
43    }
44    // dû -> dus
45    if word == "d\u{fb}" {
46        return String::from("dus");
47    }
48
49    // 2) local exceptions
50    let result = String::from(word);
51    let last = take_last(word);
52
53    // ending with "y" (few english words like "hobby")
54    if exceptions::Y.contains(&word) {
55        return drop_last(word) + "ies";
56    }
57
58    // -- ending with "s", "x", "z" (like "nez")
59    if last == "s" || last == "x" || last == "z" {
60        return result;
61    }
62
63    // -- ending with "au", "eu", "ou", "al", "œu" (like "animal")
64    let last_2_graphemes = take_lasts(word, 2);
65    let last2 = last_2_graphemes.as_str();
66
67    if last2 == "au" || last2 == "eu" || last2 == "ou" || last2 == "al" || last2 == "\u{153}u" {
68        return match last2 {
69            // exceptions
70            "ou" => {
71                if exceptions::OU.contains(&word) {
72                    result + "x"
73                } else {
74                    result + "s"
75                }
76            }
77            "eu" | "au" | "\u{153}u" => {
78                if exceptions::AU.contains(&word) || exceptions::EU.contains(&word) {
79                    result + "s"
80                } else {
81                    result + "x"
82                }
83            }
84            "al" => {
85                if exceptions::AL.contains(&word) {
86                    result + "s"
87                } else {
88                    drop_last(word) + "ux"
89                }
90            }
91            // not reachable
92            _ => result,
93        };
94    }
95
96    // -- ending with "ail" (like "vitrail")
97    let last_3_graphemes = take_lasts(word, 3);
98    let last3 = last_3_graphemes.as_str();
99    if last3 == "ail" && exceptions::AIL.contains(&word) {
100        return drop_lasts(word, 2) + "ux";
101    }
102    // 3) no exception: the most classic form
103    result + "s"
104}
105
106#[cfg(test)]
107#[allow(clippy::non_ascii_literal)]
108mod tests {
109    use super::pluralize_word;
110    #[test]
111    fn pluralize_works() {
112        assert_eq!(pluralize_word(""), "");
113        assert_eq!(pluralize_word("a"), "as");
114        assert_eq!(pluralize_word("oeil"), "yeux");
115        assert_eq!(pluralize_word("tests"), "tests");
116        assert_eq!(pluralize_word("houx"), "houx");
117        assert_eq!(pluralize_word("nez"), "nez");
118        assert_eq!(pluralize_word("bleu"), "bleus");
119        assert_eq!(pluralize_word("vieu"), "vieux");
120        assert_eq!(pluralize_word("vœu"), "vœux");
121        assert_eq!(pluralize_word("bateau"), "bateaux");
122        assert_eq!(pluralize_word("landau"), "landaus");
123        assert_eq!(pluralize_word("bijou"), "bijoux");
124        assert_eq!(pluralize_word("matou"), "matous");
125        assert_eq!(pluralize_word("animal"), "animaux");
126        assert_eq!(pluralize_word("festival"), "festivals");
127        assert_eq!(pluralize_word("corail"), "coraux");
128        assert_eq!(pluralize_word("émail"), "émaux");
129        assert_eq!(pluralize_word("chandail"), "chandails");
130        assert_eq!(pluralize_word("voiture"), "voitures");
131        assert_eq!(pluralize_word("vélo"), "vélos");
132    }
133}