lindera_analysis/character_filter/
mapping.rs1use std::collections::HashMap;
2
3use daachorse::DoubleArrayAhoCorasick;
4use daachorse::DoubleArrayAhoCorasickBuilder;
5use daachorse::MatchKind;
6use serde_json::Value;
7
8use crate::character_filter::{CharacterFilter, OffsetMapping, Transformation};
9use lindera::LinderaResult;
10use lindera::error::LinderaErrorKind;
11
12pub const MAPPING_CHARACTER_FILTER_NAME: &str = "mapping";
13
14pub type MappingCharacterFilterConfig = Value;
15
16#[derive(Clone)]
17pub struct MappingCharacterFilter {
18 mapping: HashMap<String, String>,
19 trie: DoubleArrayAhoCorasick<u32>,
20}
21
22impl MappingCharacterFilter {
23 pub fn new(mapping: HashMap<String, String>) -> LinderaResult<Self> {
37 if mapping.keys().any(|key| key.is_empty()) {
38 return Err(LinderaErrorKind::Args
39 .with_error(anyhow::anyhow!("mapping key must not be empty.")));
40 }
41
42 let mut keyset: Vec<(&[u8], u32)> = Vec::new();
43 let mut keys = mapping.keys().collect::<Vec<_>>();
44 keys.sort();
45 for (value, key) in keys.into_iter().enumerate() {
46 keyset.push((key.as_bytes(), value as u32));
47 }
48
49 let trie = DoubleArrayAhoCorasickBuilder::new()
50 .match_kind(MatchKind::LeftmostLongest)
51 .build_with_values(keyset)
52 .map_err(|err| LinderaErrorKind::Build.with_error(anyhow::anyhow!(err)))?;
53
54 Ok(Self { mapping, trie })
55 }
56
57 pub fn from_config(config: &MappingCharacterFilterConfig) -> LinderaResult<Self> {
58 let mapping = config
59 .get("mapping")
60 .and_then(Value::as_object)
61 .ok_or_else(|| {
62 LinderaErrorKind::Parse.with_error(anyhow::anyhow!("mapping must be an object."))
63 })?
64 .iter()
65 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
66 .collect::<HashMap<String, String>>();
67
68 Self::new(mapping)
69 }
70}
71
72impl CharacterFilter for MappingCharacterFilter {
73 fn name(&self) -> &'static str {
74 MAPPING_CHARACTER_FILTER_NAME
75 }
76
77 fn apply(&self, text: &mut String) -> LinderaResult<OffsetMapping> {
95 let mut filtered_text = String::with_capacity(text.len());
96 let mut mapping = OffsetMapping::new();
97
98 {
99 let source = text.as_str();
100 let mut cursor = 0_usize;
101
102 for m in self.trie.leftmost_find_iter(source) {
103 debug_assert!(m.start() >= cursor && m.end() > m.start());
107
108 filtered_text.push_str(&source[cursor..m.start()]);
110
111 let input_start = m.start();
112 let input_len = m.end() - m.start();
113 let replacement_text = &self.mapping[&source[m.start()..m.end()]];
114 let replacement_len = replacement_text.len();
115
116 if input_len != replacement_len {
118 let transformation = Transformation::new(
119 input_start,
120 input_start + input_len,
121 filtered_text.len(),
122 filtered_text.len() + replacement_len,
123 );
124 mapping.add_transformation(transformation);
125 }
126
127 filtered_text.push_str(replacement_text);
128 cursor = m.end();
129 }
130
131 filtered_text.push_str(&source[cursor..]);
133 }
134
135 *text = filtered_text;
136 Ok(mapping)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use std::collections::HashMap;
143
144 use crate::character_filter::mapping::{MappingCharacterFilter, MappingCharacterFilterConfig};
145 use crate::character_filter::{CharacterFilter, Transformation};
146
147 #[test]
148 fn test_mapping_character_filter_config() {
149 let config_str = r#"
150 {
151 "mapping": {
152 "ア": "ア",
153 "イ": "イ",
154 "ウ": "ウ",
155 "エ": "エ",
156 "オ": "オ"
157 }
158 }
159 "#;
160 let result: Result<MappingCharacterFilterConfig, _> = serde_json::from_str(config_str);
161 assert!(result.is_ok());
162 }
163
164 #[test]
165 fn test_mapping_character_filter_from_config() {
166 let config_str = r#"
167 {
168 "mapping": {
169 "ア": "ア",
170 "イ": "イ",
171 "ウ": "ウ",
172 "エ": "エ",
173 "オ": "オ"
174 }
175 }
176 "#;
177 let config = serde_json::from_str::<MappingCharacterFilterConfig>(config_str).unwrap();
178
179 let result = MappingCharacterFilter::from_config(&config);
180 assert!(result.is_ok());
181 }
182
183 #[test]
184 fn test_mapping_character_filter_apply() {
185 {
186 let config_str = r#"
187 {
188 "mapping": {
189 "ア": "ア",
190 "イ": "イ",
191 "ウ": "ウ",
192 "エ": "エ",
193 "オ": "オ"
194 }
195 }
196 "#;
197 let config = serde_json::from_str::<MappingCharacterFilterConfig>(config_str).unwrap();
198
199 let filter = MappingCharacterFilter::from_config(&config).unwrap();
200
201 let original_text = "アイウエオ";
202 let mut text = original_text.to_string();
203 let mapping = filter.apply(&mut text).unwrap();
204 assert_eq!("アイウエオ", text.as_str());
205 assert!(mapping.is_empty());
206
207 let start = 3;
209 let end = 6;
210 assert_eq!("イ", &text[start..end]);
211 let correct_start = mapping.correct_offset(start, text.len());
212 let correct_end = mapping.correct_offset(end, text.len());
213 assert_eq!(3, correct_start);
214 assert_eq!(6, correct_end);
215 assert_eq!("イ", &original_text[correct_start..correct_end]);
216 }
217
218 {
219 let config_str = r#"
220 {
221 "mapping": {
222 "リ": "リ",
223 "ン": "ン",
224 "デ": "デ",
225 "ラ": "ラ"
226 }
227 }
228 "#;
229 let config = serde_json::from_str::<MappingCharacterFilterConfig>(config_str).unwrap();
230
231 let filter = MappingCharacterFilter::from_config(&config).unwrap();
232 let original_text = "リンデラ";
233 let mut text = original_text.to_string();
234 let mapping = filter.apply(&mut text).unwrap();
235 assert_eq!("リンデラ", text.as_str());
236
237 assert_eq!(1, mapping.transformations.len());
239 let transform = &mapping.transformations[0];
240 assert_eq!(6, transform.original_start);
241 assert_eq!(12, transform.original_end);
242 assert_eq!(6, transform.filtered_start);
243 assert_eq!(9, transform.filtered_end);
244
245 let start = 6;
247 let end = 9;
248 assert_eq!("デ", &text[start..end]);
249 let correct_start = mapping.correct_offset(start, text.len());
250 let correct_end = mapping.correct_offset(end, text.len());
251 assert_eq!(6, correct_start);
252 assert_eq!(12, correct_end);
253 assert_eq!("デ", &original_text[correct_start..correct_end]);
254 }
255
256 {
257 let config_str = r#"
258 {
259 "mapping": {
260 "リンデラ": "リンデラ"
261 }
262 }
263 "#;
264 let config = serde_json::from_str::<MappingCharacterFilterConfig>(config_str).unwrap();
265
266 let filter = MappingCharacterFilter::from_config(&config).unwrap();
267 let original_text = "リンデラ";
268 let mut text = original_text.to_string();
269 let mapping = filter.apply(&mut text).unwrap();
270 assert_eq!("リンデラ", text.as_str());
271
272 assert_eq!(1, mapping.transformations.len());
274 let transform = &mapping.transformations[0];
275 assert_eq!(0, transform.original_start);
276 assert_eq!(15, transform.original_end);
277 assert_eq!(0, transform.filtered_start);
278 assert_eq!(12, transform.filtered_end);
279
280 let start = 0;
282 let end = 12;
283 assert_eq!("リンデラ", &text[start..end]);
284 let correct_start = mapping.correct_offset(start, text.len());
285 let correct_end = mapping.correct_offset(end, text.len());
286 assert_eq!(0, correct_start);
287 assert_eq!(15, correct_end);
288 assert_eq!("リンデラ", &original_text[correct_start..correct_end]);
289 }
290
291 {
292 let config_str = r#"
293 {
294 "mapping": {
295 "リンデラ": "Lindera"
296 }
297 }
298 "#;
299 let config = serde_json::from_str::<MappingCharacterFilterConfig>(config_str).unwrap();
300
301 let filter = MappingCharacterFilter::from_config(&config).unwrap();
302 let original_text = "Rust製形態素解析器リンデラで日本語を形態素解析する。";
303 let mut text = original_text.to_string();
304 let mapping = filter.apply(&mut text).unwrap();
305 assert_eq!(
306 "Rust製形態素解析器Linderaで日本語を形態素解析する。",
307 text.as_str()
308 );
309
310 assert_eq!(1, mapping.transformations.len());
312 let transform = &mapping.transformations[0];
313 assert_eq!(25, transform.original_start);
314 assert_eq!(37, transform.original_end);
315 assert_eq!(25, transform.filtered_start);
316 assert_eq!(32, transform.filtered_end);
317
318 let start = 25;
320 let end = 32;
321 assert_eq!("Lindera", &text[start..end]);
322 let correct_start = mapping.correct_offset(start, text.len());
323 let correct_end = mapping.correct_offset(end, text.len());
324 assert_eq!(25, correct_start);
325 assert_eq!(37, correct_end);
326 assert_eq!("リンデラ", &original_text[correct_start..correct_end]);
327
328 let start = 35;
329 let end = 44;
330 assert_eq!("日本語", &text[start..end]);
331 let correct_start = mapping.correct_offset(start, text.len());
332 let correct_end = mapping.correct_offset(end, text.len());
333 assert_eq!(40, correct_start);
334 assert_eq!(49, correct_end);
335 assert_eq!("日本語", &original_text[correct_start..correct_end]);
336 }
337
338 {
339 let config_str = r#"
340 {
341 "mapping": {
342 "1": "1",
343 "0": "0",
344 "㍑": "リットル"
345 }
346 }
347 "#;
348 let config = serde_json::from_str(config_str).unwrap();
349
350 let filter = MappingCharacterFilter::from_config(&config).unwrap();
351 let original_text = "10㍑";
352 let mut text = original_text.to_string();
353 let mapping = filter.apply(&mut text).unwrap();
354 assert_eq!("10リットル", text.as_str());
355
356 assert_eq!(3, mapping.transformations.len());
358
359 let transform = &mapping.transformations[2];
361 assert_eq!(6, transform.original_start);
362 assert_eq!(9, transform.original_end);
363 assert_eq!(2, transform.filtered_start);
364 assert_eq!(14, transform.filtered_end);
365
366 let start = 2;
368 let end = 14;
369 assert_eq!("リットル", &text[start..end]);
370 let correct_start = mapping.correct_offset(start, text.len());
371 let correct_end = mapping.correct_offset(end, text.len());
372 assert_eq!(6, correct_start);
373 assert_eq!(9, correct_end);
374 assert_eq!("㍑", &original_text[correct_start..correct_end]);
375 }
376 }
377
378 #[test]
379 fn test_mapping_character_filter_apply_longest_match() {
380 let mut mapping = HashMap::new();
381 mapping.insert("ab".to_string(), "X".to_string());
382 mapping.insert("abc".to_string(), "YY".to_string());
383 mapping.insert("b".to_string(), "Z".to_string());
384 let filter = MappingCharacterFilter::new(mapping).unwrap();
385
386 let mut text = "abcabx".to_string();
387 let mapping = filter.apply(&mut text).unwrap();
388 assert_eq!("YYXx", text.as_str());
389
390 assert_eq!(2, mapping.transformations.len());
392 assert_eq!(Transformation::new(0, 3, 0, 2), mapping.transformations[0]);
393 assert_eq!(Transformation::new(3, 5, 2, 3), mapping.transformations[1]);
394 }
395
396 #[test]
397 fn test_mapping_character_filter_apply_backtrack() {
398 let mut mapping = HashMap::new();
401 mapping.insert("abcd".to_string(), "1".to_string());
402 mapping.insert("b".to_string(), "22".to_string());
403 let filter = MappingCharacterFilter::new(mapping).unwrap();
404
405 let mut text = "abx".to_string();
406 let mapping = filter.apply(&mut text).unwrap();
407 assert_eq!("a22x", text.as_str());
408 assert_eq!(1, mapping.transformations.len());
409 assert_eq!(Transformation::new(1, 2, 1, 3), mapping.transformations[0]);
410 }
411
412 #[test]
413 fn test_mapping_character_filter_apply_leftmost_wins() {
414 let mut mapping = HashMap::new();
417 mapping.insert("abcdefgh".to_string(), "1".to_string());
418 mapping.insert("hz".to_string(), "2".to_string());
419 let filter = MappingCharacterFilter::new(mapping).unwrap();
420
421 let mut text = "abcdefghz".to_string();
422 let mapping = filter.apply(&mut text).unwrap();
423 assert_eq!("1z", text.as_str());
424 assert_eq!(1, mapping.transformations.len());
425 }
426
427 #[test]
428 fn test_mapping_character_filter_apply_shared_prefix() {
429 let mut mapping = HashMap::new();
433 mapping.insert("デ".to_string(), "デ".to_string());
434 mapping.insert("ラ".to_string(), "ラ".to_string());
435 let filter = MappingCharacterFilter::new(mapping).unwrap();
436
437 let mut text = "テラ".to_string();
438 let mapping = filter.apply(&mut text).unwrap();
439 assert_eq!("テラ", text.as_str());
440 assert!(mapping.is_empty());
442 }
443
444 #[test]
445 fn test_mapping_character_filter_empty_key_rejected() {
446 let mut mapping = HashMap::new();
447 mapping.insert(String::new(), "x".to_string());
448 assert!(MappingCharacterFilter::new(mapping).is_err());
449
450 let mut mapping = HashMap::new();
451 mapping.insert("a".to_string(), "b".to_string());
452 assert!(MappingCharacterFilter::new(mapping).is_ok());
453 }
454
455 #[test]
456 fn test_mapping_character_filter_apply_large_input() {
457 let mut mapping = HashMap::new();
458 mapping.insert("リンデラ".to_string(), "Lindera".to_string());
459 let filter = MappingCharacterFilter::new(mapping).unwrap();
460
461 let mut text = "あ".repeat(100_000);
469 let original_len = text.len();
470
471 let start = std::time::Instant::now();
472 let mapping = filter.apply(&mut text).unwrap();
473 let elapsed = start.elapsed();
474
475 assert_eq!(original_len, text.len());
476 assert!(mapping.is_empty());
477 assert!(
478 elapsed.as_secs() < 3,
479 "apply() took too long ({elapsed:?}); the quadratic-scan regression may have returned"
480 );
481 }
482}