Skip to main content

harper_core/
irregular_nouns.rs

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