Skip to main content

harper_core/
irregular_nouns.rs

1use crate::CharStringExt;
2use serde::Deserialize;
3use std::sync::{Arc, LazyLock};
4
5type Noun = (String, String);
6
7#[derive(Debug, Deserialize)]
8pub struct IrregularNouns {
9    nouns: Vec<Noun>,
10}
11
12/// The uncached function that is used to produce the original copy of the
13/// irregular noun table.
14fn uncached_inner_new() -> Arc<IrregularNouns> {
15    IrregularNouns::from_json_file(include_str!("../irregular_nouns.json"))
16        .map(Arc::new)
17        .unwrap_or_else(|e| panic!("Failed to load irregular noun table: {}", e))
18}
19
20static NOUNS: LazyLock<Arc<IrregularNouns>> = LazyLock::new(uncached_inner_new);
21
22impl IrregularNouns {
23    pub fn new() -> Self {
24        Self { nouns: vec![] }
25    }
26
27    pub fn from_json_file(json: &str) -> Result<Self, serde_json::Error> {
28        // Deserialize into Vec<serde_json::Value> to handle mixed types
29        let values: Vec<serde_json::Value> =
30            serde_json::from_str(json).expect("Failed to parse irregular nouns JSON");
31
32        let mut nouns = Vec::new();
33
34        for value in values {
35            match value {
36                serde_json::Value::Array(arr) if arr.len() == 2 => {
37                    // Handle array of 2 strings
38                    if let (Some(singular), Some(plural)) = (arr[0].as_str(), arr[1].as_str()) {
39                        nouns.push((singular.to_string(), plural.to_string()));
40                    }
41                }
42                // Strings are used for comments to guide contributors editing the file
43                serde_json::Value::String(_) => {}
44                _ => {}
45            }
46        }
47
48        Ok(Self { nouns })
49    }
50
51    pub fn curated() -> Arc<Self> {
52        (*NOUNS).clone()
53    }
54
55    pub fn get_plural_for_singular_chars(&self, singular: &[char]) -> Option<&str> {
56        self.nouns
57            .iter()
58            .find(|(sg, _)| singular.eq_str(sg))
59            .map(|(_, pl)| pl.as_str())
60    }
61
62    pub fn get_plural_for_singular(&self, singular: &str) -> Option<&str> {
63        self.nouns
64            .iter()
65            .find(|(sg, _)| sg.eq_ignore_ascii_case(singular))
66            .map(|(_, pl)| pl.as_str())
67    }
68
69    pub fn get_singular_for_plural(&self, plural: &str) -> Option<&str> {
70        self.nouns
71            .iter()
72            .find(|(_, pl)| pl.eq_ignore_ascii_case(plural))
73            .map(|(sg, _)| sg.as_str())
74    }
75
76    pub fn get_singular_for_plural_chars(&self, plural: &[char]) -> Option<&str> {
77        self.nouns
78            .iter()
79            .find(|(_, pl)| plural.eq_str(pl))
80            .map(|(sg, _)| sg.as_str())
81    }
82}
83
84impl Default for IrregularNouns {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn can_find_irregular_plural_for_singular_lowercase() {
96        assert_eq!(
97            IrregularNouns::curated().get_plural_for_singular("man"),
98            Some("men")
99        );
100    }
101
102    #[test]
103    fn can_find_irregular_plural_for_singular_uppercase() {
104        assert_eq!(
105            IrregularNouns::curated().get_plural_for_singular("WOMAN"),
106            Some("women")
107        );
108    }
109
110    #[test]
111    fn can_find_singular_for_irregular_plural() {
112        assert_eq!(
113            IrregularNouns::curated().get_singular_for_plural("children"),
114            Some("child")
115        );
116    }
117
118    #[test]
119    fn cant_find_regular_plural() {
120        assert_eq!(
121            IrregularNouns::curated().get_plural_for_singular("car"),
122            None
123        );
124    }
125
126    #[test]
127    fn cant_find_non_noun() {
128        assert_eq!(
129            IrregularNouns::curated().get_plural_for_singular("the"),
130            None
131        );
132    }
133}