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#[derive(Clone, Debug, Default, Eq, PartialEq)]
15pub struct OverrideDictionary {
16 readings_by_surface: HashMap<String, Vec<String>>,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq)]
21pub enum OverrideLoadError {
22 Yaml(String),
24 UnsupportedVersion {
26 version: u32,
28 },
29 EmptySurface {
31 entry_index: usize,
33 },
34 EmptyReadings {
36 surface: String,
38 },
39 EmptyReading {
41 surface: String,
43 reading_index: usize,
45 },
46 DuplicateSurface {
48 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 pub fn new() -> Self {
97 Self::default()
98 }
99
100 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 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 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 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 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}