Skip to main content

moine_ja/
overrides.rs

1use std::collections::HashMap;
2use std::error::Error;
3use std::fmt;
4
5use serde::Deserialize;
6
7use moine_core::Lattice;
8
9use crate::romaji::{romaji_lattice, romaji_lattice_from_readings, JaLatticeError};
10
11const SUPPORTED_VERSION: u32 = 1;
12
13/// In-memory surface-to-reading override dictionary.
14#[derive(Clone, Debug, Default, Eq, PartialEq)]
15pub struct OverrideDictionary {
16    readings_by_surface: HashMap<String, Vec<String>>,
17}
18
19/// Errors returned while loading an override dictionary.
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum OverrideLoadError {
22    /// The YAML document could not be parsed.
23    Yaml(String),
24    /// The override file version is not supported.
25    UnsupportedVersion {
26        /// Version read from the file.
27        version: u32,
28    },
29    /// An override entry had an empty surface form.
30    EmptySurface {
31        /// Zero-based entry index.
32        entry_index: usize,
33    },
34    /// An override entry had no readings.
35    EmptyReadings {
36        /// Surface form for the invalid entry.
37        surface: String,
38    },
39    /// An override entry contained an empty reading.
40    EmptyReading {
41        /// Surface form for the invalid entry.
42        surface: String,
43        /// Zero-based reading index.
44        reading_index: usize,
45    },
46    /// A surface form appeared more than once.
47    DuplicateSurface {
48        /// Duplicated surface form.
49        surface: String,
50    },
51}
52
53impl fmt::Display for OverrideLoadError {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::Yaml(err) => write!(f, "invalid override YAML: {err}"),
57            Self::UnsupportedVersion { version } => {
58                write!(f, "unsupported override version {version}")
59            }
60            Self::EmptySurface { entry_index } => {
61                write!(f, "override entry {entry_index} has an empty surface")
62            }
63            Self::EmptyReadings { surface } => {
64                write!(f, "override entry {surface:?} has no readings")
65            }
66            Self::EmptyReading {
67                surface,
68                reading_index,
69            } => write!(
70                f,
71                "override entry {surface:?} has an empty reading at index {reading_index}"
72            ),
73            Self::DuplicateSurface { surface } => {
74                write!(f, "override entry {surface:?} is duplicated")
75            }
76        }
77    }
78}
79
80impl Error for OverrideLoadError {}
81
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
83struct OverrideFile {
84    version: u32,
85    entries: Vec<OverrideEntry>,
86}
87
88#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
89struct OverrideEntry {
90    surface: String,
91    readings: Vec<String>,
92}
93
94impl OverrideDictionary {
95    /// Creates an empty override dictionary.
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Builds an override dictionary from `(surface, readings)` entries.
101    pub fn from_entries<I, S, R, Rs>(entries: I) -> Self
102    where
103        I: IntoIterator<Item = (S, Rs)>,
104        S: Into<String>,
105        R: Into<String>,
106        Rs: IntoIterator<Item = R>,
107    {
108        let mut dict = Self::new();
109        for (surface, readings) in entries {
110            dict.insert_readings(surface, readings);
111        }
112        dict
113    }
114
115    /// Loads an override dictionary from a YAML string.
116    pub fn from_yaml_str(input: &str) -> Result<Self, OverrideLoadError> {
117        let file = serde_yaml::from_str::<OverrideFile>(input)
118            .map_err(|err| OverrideLoadError::Yaml(err.to_string()))?;
119        Self::from_override_file(file)
120    }
121
122    /// Inserts or replaces readings for a surface form.
123    pub fn insert_readings<S, R, Rs>(&mut self, surface: S, readings: Rs)
124    where
125        S: Into<String>,
126        R: Into<String>,
127        Rs: IntoIterator<Item = R>,
128    {
129        self.readings_by_surface.insert(
130            surface.into(),
131            readings.into_iter().map(Into::into).collect(),
132        );
133    }
134
135    /// Returns override readings for `surface`, if present.
136    pub fn readings(&self, surface: &str) -> Option<&[String]> {
137        self.readings_by_surface
138            .get(surface)
139            .map(std::vec::Vec::as_slice)
140    }
141
142    /// Builds a romaji lattice using an override when one exists.
143    pub fn romaji_lattice(&self, input: &str) -> Result<Lattice, JaLatticeError> {
144        if let Some(readings) = self.readings(input) {
145            romaji_lattice_from_readings(readings)
146        } else {
147            romaji_lattice(input)
148        }
149    }
150
151    fn from_override_file(file: OverrideFile) -> Result<Self, OverrideLoadError> {
152        if file.version != SUPPORTED_VERSION {
153            return Err(OverrideLoadError::UnsupportedVersion {
154                version: file.version,
155            });
156        }
157
158        let mut dict = Self::new();
159        for (entry_index, entry) in file.entries.into_iter().enumerate() {
160            let surface = entry.surface.trim();
161            if surface.is_empty() {
162                return Err(OverrideLoadError::EmptySurface { entry_index });
163            }
164            if entry.readings.is_empty() {
165                return Err(OverrideLoadError::EmptyReadings {
166                    surface: surface.to_string(),
167                });
168            }
169            if dict.readings_by_surface.contains_key(surface) {
170                return Err(OverrideLoadError::DuplicateSurface {
171                    surface: surface.to_string(),
172                });
173            }
174
175            let mut readings = Vec::with_capacity(entry.readings.len());
176            for (reading_index, reading) in entry.readings.into_iter().enumerate() {
177                let reading = reading.trim();
178                if reading.is_empty() {
179                    return Err(OverrideLoadError::EmptyReading {
180                        surface: surface.to_string(),
181                        reading_index,
182                    });
183                }
184                readings.push(reading.to_string());
185            }
186
187            dict.insert_readings(surface, readings);
188        }
189
190        Ok(dict)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use moine_core::{distance, distance_with_trace, Lattice};
197
198    use super::*;
199
200    fn symbols_to_string(symbols: &[moine_core::Symbol]) -> String {
201        symbols
202            .iter()
203            .map(|&symbol| char::from_u32(symbol).expect("test symbol should be a char"))
204            .collect()
205    }
206
207    #[test]
208    fn override_reading_builds_romaji_lattice_for_kanji_surface() {
209        let dict = OverrideDictionary::from_entries([("鬼滅の刃", ["キメツノヤイバ"])]);
210
211        let left = romaji_lattice("きめつのやいば").expect("kana input should build");
212        let right = dict
213            .romaji_lattice("鬼滅の刃")
214            .expect("override should build");
215
216        assert_eq!(distance(&left, &right), 0);
217    }
218
219    #[test]
220    fn multiple_override_readings_share_one_lookup() {
221        let dict = OverrideDictionary::from_entries([("茶道具", ["チャドウグ", "チャドーグ"])]);
222
223        let lattice = dict
224            .romaji_lattice("茶道具")
225            .expect("override should build");
226
227        assert_eq!(distance(&lattice, &Lattice::from_paths(["chadougu"])), 0);
228        assert_eq!(distance(&lattice, &Lattice::from_paths(["chado-gu"])), 0);
229    }
230
231    #[test]
232    fn override_trace_exposes_best_path() {
233        let dict = OverrideDictionary::from_entries([("印刷", ["インサツ"])]);
234
235        let left = romaji_lattice("いんさt").expect("kana ascii input should build");
236        let right = dict.romaji_lattice("印刷").expect("override should build");
237        let trace = distance_with_trace(&left, &right);
238
239        assert_eq!(trace.distance, 1);
240        assert_eq!(symbols_to_string(&trace.left_symbols()), "insat");
241        assert_eq!(symbols_to_string(&trace.right_symbols()), "insatu");
242    }
243
244    #[test]
245    fn loads_versioned_yaml_fixture() {
246        let dict =
247            OverrideDictionary::from_yaml_str(include_str!("../tests/resources/overrides.yaml"))
248                .expect("fixture should load");
249
250        assert_eq!(
251            dict.readings("茶道具"),
252            Some(&["チャドウグ".to_string(), "チャドーグ".to_string()][..])
253        );
254    }
255
256    #[test]
257    fn rejects_unsupported_version() {
258        let result = OverrideDictionary::from_yaml_str(
259            "
260version: 2
261entries: []
262",
263        );
264
265        assert!(matches!(
266            result,
267            Err(OverrideLoadError::UnsupportedVersion { version: 2 })
268        ));
269    }
270
271    #[test]
272    fn rejects_duplicate_surface() {
273        let result = OverrideDictionary::from_yaml_str(
274            "
275version: 1
276entries:
277  - surface: 印刷
278    readings: [インサツ]
279  - surface: 印刷
280    readings: [インサツ]
281",
282        );
283
284        assert!(matches!(
285            result,
286            Err(OverrideLoadError::DuplicateSurface { surface }) if surface == "印刷"
287        ));
288    }
289
290    #[test]
291    fn rejects_empty_reading() {
292        let result = OverrideDictionary::from_yaml_str(
293            "
294version: 1
295entries:
296  - surface: 印刷
297    readings: [インサツ, '']
298",
299        );
300
301        assert!(matches!(
302            result,
303            Err(OverrideLoadError::EmptyReading {
304                surface,
305                reading_index: 1,
306            }) if surface == "印刷"
307        ));
308    }
309}