Skip to main content

nounsql_core/
dict.rs

1use std::collections::HashMap;
2
3use crate::ast::NounsBlock;
4use crate::span::Span;
5
6#[derive(Debug, Clone)]
7pub struct Entry {
8    pub singular: String,
9    pub plural: String,
10    pub short: String,
11    pub comment: Option<String>,
12    pub span: Span,
13}
14
15/// `nouns` ブロックの名詞辞書。識別子で引く。
16#[derive(Debug, Default)]
17pub struct Dict {
18    by_id: HashMap<String, Entry>,
19}
20
21/// 複合名詞の構成要素。
22#[cfg_attr(feature = "serde", derive(serde::Serialize))]
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Part {
25    /// 名詞。文脈に応じて屈折する。
26    Noun(String),
27    /// 文字列。屈折しない。
28    Literal(String),
29}
30
31/// 1つ以上の要素からなる名詞。単数形と複数形を持つ。
32///
33/// 最後の要素だけが文脈の数に従い、それ以外は単数形になる。
34#[cfg_attr(feature = "serde", derive(serde::Serialize))]
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Compound {
37    pub parts: Vec<Part>,
38}
39
40impl Compound {
41    pub fn noun(name: impl Into<String>) -> Self {
42        Compound {
43            parts: vec![Part::Noun(name.into())],
44        }
45    }
46
47    pub fn literal(text: impl Into<String>) -> Self {
48        Compound {
49            parts: vec![Part::Literal(text.into())],
50        }
51    }
52
53    /// 単一の名詞ならその名前。
54    pub fn as_single_noun(&self) -> Option<&str> {
55        match self.parts.as_slice() {
56            [Part::Noun(n)] => Some(n),
57            _ => None,
58        }
59    }
60
61    /// 含まれる名詞。辞書登録の検査に使う。
62    pub fn nouns(&self) -> impl Iterator<Item = &str> {
63        self.parts.iter().filter_map(|p| match p {
64            Part::Noun(n) => Some(n.as_str()),
65            Part::Literal(_) => None,
66        })
67    }
68
69    pub fn singular(&self, dict: &Dict, separator: &str) -> String {
70        self.render(dict, separator, false)
71    }
72
73    pub fn plural(&self, dict: &Dict, separator: &str) -> String {
74        self.render(dict, separator, true)
75    }
76
77    /// 略語。数は変えず、名詞の要素をすべて略語にする。
78    pub fn short(&self, dict: &Dict, separator: &str) -> String {
79        self.parts
80            .iter()
81            .map(|part| match part {
82                Part::Literal(text) => text.clone(),
83                Part::Noun(id) => dict.short(id).into_value(),
84            })
85            .collect::<Vec<_>>()
86            .join(separator)
87    }
88
89    /// 屈折させず、書かれたまま連結する。
90    pub fn as_written(&self, separator: &str) -> String {
91        self.parts
92            .iter()
93            .map(|p| match p {
94                Part::Noun(n) | Part::Literal(n) => n.as_str(),
95            })
96            .collect::<Vec<_>>()
97            .join(separator)
98    }
99
100    fn render(&self, dict: &Dict, separator: &str, last_plural: bool) -> String {
101        let last = self.parts.len().saturating_sub(1);
102        self.parts
103            .iter()
104            .enumerate()
105            .map(|(i, part)| match part {
106                Part::Literal(text) => text.clone(),
107                Part::Noun(name) if i == last && last_plural => dict.plural(name).into_value(),
108                Part::Noun(name) => dict.singular(name).into_value(),
109            })
110            .collect::<Vec<_>>()
111            .join(separator)
112    }
113}
114
115/// 辞書で解決できたか、規則変化に落ちたか。
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum Resolved {
118    FromDict(String),
119    FromRule(String),
120}
121
122impl Resolved {
123    pub fn value(&self) -> &str {
124        match self {
125            Resolved::FromDict(s) | Resolved::FromRule(s) => s,
126        }
127    }
128
129    pub fn into_value(self) -> String {
130        match self {
131            Resolved::FromDict(s) | Resolved::FromRule(s) => s,
132        }
133    }
134}
135
136impl Dict {
137    /// 語形は書かれていなければ順に埋める。
138    /// 識別子 → 単数形 → 複数形 / 略語、の一方通行。
139    pub fn from_block(block: Option<&NounsBlock>) -> Dict {
140        let mut dict = Dict::default();
141        let Some(block) = block else { return dict };
142        for e in &block.entries {
143            let word = |w: &Option<crate::ast::Name>| w.as_ref().map(|w| w.value.clone());
144            let singular = word(&e.singular).unwrap_or_else(|| e.id.value.clone());
145            let plural = word(&e.plural).unwrap_or_else(|| pluralize(&singular));
146            let short = word(&e.short).unwrap_or_else(|| singular.clone());
147            dict.by_id.insert(
148                e.id.value.clone(),
149                Entry {
150                    singular,
151                    plural,
152                    short,
153                    comment: e.comment.as_ref().map(|c| c.value.clone()),
154                    span: e.id.span,
155                },
156            );
157        }
158        dict
159    }
160
161    pub fn get(&self, id: &str) -> Option<&Entry> {
162        self.by_id.get(id)
163    }
164
165    pub fn entries(&self) -> impl Iterator<Item = &Entry> {
166        self.by_id.values()
167    }
168
169    pub fn plural(&self, id: &str) -> Resolved {
170        match self.by_id.get(id) {
171            Some(e) => Resolved::FromDict(e.plural.clone()),
172            None => Resolved::FromRule(pluralize(id)),
173        }
174    }
175
176    pub fn singular(&self, id: &str) -> Resolved {
177        match self.by_id.get(id) {
178            Some(e) => Resolved::FromDict(e.singular.clone()),
179            None => Resolved::FromRule(id.to_string()),
180        }
181    }
182
183    pub fn short(&self, id: &str) -> Resolved {
184        match self.by_id.get(id) {
185            Some(e) => Resolved::FromDict(e.short.clone()),
186            None => Resolved::FromRule(id.to_string()),
187        }
188    }
189}
190
191fn is_vowel(c: char) -> bool {
192    matches!(c, 'a' | 'e' | 'i' | 'o' | 'u')
193}
194
195/// 規則変化。辞書に無い語のフォールバック。
196pub fn pluralize(word: &str) -> String {
197    let lower = word.to_ascii_lowercase();
198    if lower.ends_with("s")
199        || lower.ends_with("x")
200        || lower.ends_with("z")
201        || lower.ends_with("ch")
202        || lower.ends_with("sh")
203    {
204        return format!("{word}es");
205    }
206    if lower.ends_with('y') {
207        let before = lower.chars().rev().nth(1);
208        if before.is_some_and(|c| !is_vowel(c)) {
209            return format!("{}ies", &word[..word.len() - 1]);
210        }
211    }
212    format!("{word}s")
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::span::Span;
219
220    #[test]
221    fn regular_plurals() {
222        assert_eq!(pluralize("post"), "posts");
223        assert_eq!(pluralize("category"), "categories");
224        assert_eq!(pluralize("history"), "histories");
225        assert_eq!(pluralize("box"), "boxes");
226        assert_eq!(pluralize("day"), "days");
227    }
228
229    /// `(識別子, 単数形, 複数形, 略語)`
230    fn dict_with(entries: &[(&str, &str, &str, &str)]) -> Dict {
231        let mut dict = Dict::default();
232        for (id, singular, plural, short) in entries {
233            dict.by_id.insert(
234                id.to_string(),
235                Entry {
236                    singular: singular.to_string(),
237                    plural: plural.to_string(),
238                    short: short.to_string(),
239                    comment: None,
240                    span: Span::new(0, 0),
241                },
242            );
243        }
244        dict
245    }
246
247    #[test]
248    fn compound_inflects_only_the_last_noun() {
249        let dict = dict_with(&[
250            ("message", "message", "messages", "msg"),
251            ("history", "history", "histories", "hist"),
252        ]);
253        let c = Compound {
254            parts: vec![Part::Literal("sent".into()), Part::Noun("message".into())],
255        };
256        assert_eq!(c.singular(&dict, "_"), "sent_message");
257        assert_eq!(c.plural(&dict, "_"), "sent_messages");
258
259        let c = Compound {
260            parts: vec![Part::Noun("post".into()), Part::Noun("history".into())],
261        };
262        assert_eq!(c.singular(&dict, "_"), "post_history");
263        assert_eq!(c.plural(&dict, "_"), "post_histories");
264    }
265
266    #[test]
267    fn literal_never_inflects() {
268        let dict = dict_with(&[]);
269        let c = Compound::literal("users");
270        assert_eq!(c.plural(&dict, "_"), "users");
271        assert_eq!(c.short(&dict, "_"), "users");
272    }
273
274    #[test]
275    fn short_replaces_every_noun_and_keeps_the_number() {
276        let dict = dict_with(&[
277            ("message", "message", "messages", "msg"),
278            ("history", "history", "histories", "hist"),
279        ]);
280        let c = Compound {
281            parts: vec![Part::Noun("message".into()), Part::Noun("history".into())],
282        };
283        assert_eq!(c.short(&dict, "_"), "msg_hist");
284
285        let c = Compound {
286            parts: vec![Part::Literal("sent".into()), Part::Noun("message".into())],
287        };
288        assert_eq!(c.short(&dict, "_"), "sent_msg");
289    }
290
291    #[test]
292    fn unregistered_nouns_keep_the_identifier() {
293        let dict = dict_with(&[]);
294        assert_eq!(dict.singular("widget").value(), "widget");
295        assert_eq!(dict.short("widget").value(), "widget");
296        assert_eq!(dict.plural("widget").value(), "widgets");
297    }
298}