1use std::collections::{BTreeMap, HashMap};
2use std::io::{BufReader, Read};
3use std::path::Path;
4use std::sync::Arc;
5
6use anyhow::Result;
7
8use super::feature_extractor::FeatureExtractor;
9use super::feature_rewriter::DictionaryRewriter;
10use lindera_dictionary::dictionary::Dictionary;
11use lindera_dictionary::dictionary::character_definition::CharacterDefinition;
12use lindera_dictionary::dictionary::connection_cost_matrix::ConnectionCostMatrix;
13use lindera_dictionary::dictionary::metadata::Metadata;
14use lindera_dictionary::dictionary::prefix_dictionary::PrefixDictionary;
15use lindera_dictionary::dictionary::unknown_dictionary::UnknownDictionary;
16
17pub struct TrainerConfig {
19 pub(crate) dict: Dictionary,
20 pub(crate) surfaces: Vec<String>,
21 pub(crate) features: Vec<String>,
23 pub(crate) surface_features: HashMap<String, String>,
25 pub(crate) user_lexicon: HashMap<String, String>,
27 pub(crate) feature_extractor: FeatureExtractor,
28 pub(crate) dictionary_rewriter: DictionaryRewriter,
29 pub(crate) cost_factor: i32,
31 pub(crate) metadata: Metadata,
33 pub(crate) unk_categories: BTreeMap<String, String>,
40 pub(crate) unk_costs: HashMap<String, i32>,
43 pub(crate) char_def_content: String,
46 pub(crate) feature_def_content: String,
49 pub(crate) rewrite_def_content: String,
52}
53
54impl TrainerConfig {
55 pub fn system_lexicon(&self) -> &PrefixDictionary {
57 self.dict.prefix_dictionary.as_ref()
58 }
59
60 pub fn dict(&self) -> &Dictionary {
62 &self.dict
63 }
64
65 pub fn unk_handler(
67 &self,
68 ) -> &lindera_dictionary::dictionary::unknown_dictionary::UnknownDictionary {
69 &self.dict.unknown_dictionary
70 }
71}
72
73impl TrainerConfig {
74 pub fn from_readers<R1, R2, R3, R4, R5>(
84 lexicon_rdr: R1,
85 char_prop_rdr: R2,
86 unk_handler_rdr: R3,
87 feature_templates_rdr: R4,
88 rewrite_rules_rdr: R5,
89 ) -> Result<Self>
90 where
91 R1: Read,
92 R2: Read,
93 R3: Read,
94 R4: Read,
95 R5: Read,
96 {
97 let mut surfaces = Vec::new();
99 let mut features = Vec::new();
100 let mut surface_features = HashMap::new();
101 let mut lexicon_content = String::new();
102 {
103 let mut lexicon_reader = BufReader::new(lexicon_rdr);
104 std::io::Read::read_to_string(&mut lexicon_reader, &mut lexicon_content)?;
105 }
106
107 for line in lexicon_content.lines() {
108 if line.trim().is_empty() || line.starts_with('#') {
109 continue;
110 }
111 let parts: Vec<&str> = line.split(',').collect();
112
113 if parts.len() >= 5 {
120 let surface = parts[0].to_string();
121 let feature_str = parts[4..].join(",");
124 surfaces.push(surface.clone());
125 features.push(feature_str.clone());
126 surface_features.insert(surface, feature_str);
127 }
128 }
129
130 let mut feature_content = String::new();
132 {
133 let mut template_reader = BufReader::new(feature_templates_rdr);
134 std::io::Read::read_to_string(&mut template_reader, &mut feature_content)?;
135 }
136
137 let mut unigram_templates = Vec::new();
139 let mut bigram_templates = Vec::new();
140
141 for line in feature_content.lines() {
142 if line.trim().is_empty() || line.starts_with('#') {
143 continue;
144 }
145 if let Some(rest) = line.strip_prefix("UNIGRAM") {
149 let rest = rest.trim_start().trim_start_matches(':').trim_start();
151 let template = if let Some(idx) = rest.find('%') {
152 &rest[idx..]
153 } else {
154 rest
155 };
156 unigram_templates.push(template.to_string());
157 } else if let Some(rest) = line.strip_prefix("BIGRAM") {
158 let rest = rest.trim_start().trim_start_matches(':').trim_start();
160 let template = if let Some(idx) = rest.find('%') {
161 &rest[idx..]
162 } else {
163 rest
164 };
165 if let Some((left, right)) = template.split_once('/') {
166 bigram_templates.push((left.to_string(), right.to_string()));
167 }
168 } else {
169 unigram_templates.push(line.to_string());
171 }
172 }
173
174 let feature_extractor =
176 FeatureExtractor::from_templates(&unigram_templates, &bigram_templates);
177
178 let mut rewrite_def_content = String::new();
180 {
181 let mut rewrite_reader = BufReader::new(rewrite_rules_rdr);
182 std::io::Read::read_to_string(&mut rewrite_reader, &mut rewrite_def_content)?;
183 }
184
185 let dictionary_rewriter =
187 DictionaryRewriter::from_reader(std::io::Cursor::new(rewrite_def_content.as_bytes()))?;
188
189 let mut unk_content = String::new();
191 {
192 let mut unk_reader = BufReader::new(unk_handler_rdr);
193 std::io::Read::read_to_string(&mut unk_reader, &mut unk_content)?;
194 }
195
196 let mut unk_categories = BTreeMap::new();
197 let mut unk_costs = HashMap::new();
198 for line in unk_content.lines() {
199 if line.trim().is_empty() || line.starts_with('#') {
200 continue;
201 }
202 let parts: Vec<&str> = line.split(',').collect();
203 if parts.len() >= 5 {
205 let category = parts[0].to_string();
206 let features = parts[4..].join(",");
207 unk_categories.insert(category.clone(), features);
208
209 if let Ok(cost) = parts[3].parse::<i32>() {
211 unk_costs.insert(category, cost);
212 }
213 }
214 }
215
216 let mut char_def_content = String::new();
218 {
219 let mut char_prop_reader = BufReader::new(char_prop_rdr);
220 std::io::Read::read_to_string(&mut char_prop_reader, &mut char_def_content)?;
221 }
222
223 use std::io::Cursor;
225 let dict = Self::build_dictionary_from_readers(
226 &lexicon_content,
227 Cursor::new(char_def_content.as_bytes()),
228 Cursor::new(unk_content.as_bytes()),
229 )?;
230
231 Ok(Self {
232 dict,
233 surfaces,
234 features,
235 surface_features,
236 user_lexicon: HashMap::new(), feature_extractor,
238 dictionary_rewriter,
239 cost_factor: 700, metadata: Metadata::default(), unk_categories,
242 unk_costs,
243 char_def_content,
244 feature_def_content: feature_content,
245 rewrite_def_content,
246 })
247 }
248
249 pub fn surfaces(&self) -> &[String] {
251 &self.surfaces
252 }
253
254 pub fn surface_features(&self) -> &HashMap<String, String> {
256 &self.surface_features
257 }
258
259 pub fn user_lexicon(&self) -> &HashMap<String, String> {
269 &self.user_lexicon
270 }
271
272 #[deprecated(
279 since = "5.3.0",
280 note = "this map only feeds TrainerConfig::get_features; dictionary \
281 export reads user entries loaded via Model::read_user_lexicon"
282 )]
283 pub fn add_user_lexicon_entry(&mut self, surface: String, features: String) {
284 self.user_lexicon.insert(surface, features);
285 }
286
287 pub fn get_features(&self, surface: &str) -> Option<String> {
289 self.user_lexicon
291 .get(surface)
292 .or_else(|| self.surface_features.get(surface))
293 .cloned()
294 }
295
296 #[deprecated(
306 since = "5.3.0",
307 note = "this map only feeds TrainerConfig::get_features; dictionary \
308 export reads user entries loaded via Model::read_user_lexicon"
309 )]
310 pub fn load_user_lexicon_from_content(&mut self, content: &str) -> Result<()> {
311 for line in content.lines() {
312 if line.trim().is_empty() || line.starts_with('#') {
313 continue;
314 }
315
316 let parts: Vec<&str> = line.split(',').collect();
317 if parts.len() >= 5 {
318 let surface = parts[0].to_string();
319 let features = parts[4..].join(",");
321 self.user_lexicon.insert(surface, features);
322 }
323 }
324 Ok(())
325 }
326
327 pub fn from_paths(
329 lexicon_path: &Path,
330 char_prop_path: &Path,
331 unk_handler_path: &Path,
332 feature_templates_path: &Path,
333 rewrite_rules_path: &Path,
334 ) -> Result<Self> {
335 use std::fs::File;
336
337 Self::from_readers(
338 File::open(lexicon_path)?,
339 File::open(char_prop_path)?,
340 File::open(unk_handler_path)?,
341 File::open(feature_templates_path)?,
342 File::open(rewrite_rules_path)?,
343 )
344 }
345
346 pub fn metadata(&self) -> &Metadata {
348 &self.metadata
349 }
350
351 fn build_dictionary_from_readers<R2, R3>(
353 lexicon_content: &str,
354 char_prop_rdr: R2,
355 unk_handler_rdr: R3,
356 ) -> Result<Dictionary>
357 where
358 R2: Read,
359 R3: Read,
360 {
361 let mut char_prop_content = String::new();
363 let mut char_prop_reader = BufReader::new(char_prop_rdr);
364 std::io::Read::read_to_string(&mut char_prop_reader, &mut char_prop_content)?;
365
366 let mut unk_content = String::new();
368 let mut unk_reader = BufReader::new(unk_handler_rdr);
369 std::io::Read::read_to_string(&mut unk_reader, &mut unk_content)?;
370
371 let char_def = Self::build_char_def_from_content(&char_prop_content)?;
373
374 let unknown_dict = Self::build_unknown_dict_from_content(&unk_content, &char_def)?;
376
377 let prefix_dict = Self::build_prefix_dict_from_content(lexicon_content)?;
379
380 let conn_matrix = Self::create_minimal_connection_matrix()?;
382
383 Ok(Dictionary {
384 prefix_dictionary: Arc::new(prefix_dict),
385 connection_cost_matrix: Arc::new(conn_matrix),
386 character_definition: Arc::new(char_def),
387 unknown_dictionary: Arc::new(unknown_dict),
388 metadata: Arc::new(Metadata::default()),
389 })
390 }
391
392 fn build_char_def_from_content(content: &str) -> Result<CharacterDefinition> {
393 use lindera_dictionary::dictionary::character_definition::{
394 CategoryData, CategoryId, LookupTable,
395 };
396 use std::collections::HashMap;
397
398 let mut category_definitions = Vec::new();
399 let mut category_names = Vec::new();
400 let mut category_map = HashMap::new(); let mut char_ranges = Vec::new();
402
403 category_names.push("DEFAULT".to_string());
405 category_map.insert("DEFAULT".to_string(), 0);
406 category_definitions.push(CategoryData {
407 invoke: false,
408 group: true,
409 length: 0,
410 });
411
412 for line in content.lines() {
414 let line = line.trim();
415
416 if line.is_empty() || line.starts_with('#') {
418 continue;
419 }
420
421 if line.starts_with("0x") {
423 let parts: Vec<&str> = line.split_whitespace().collect();
424 if parts.len() >= 2 {
425 let range_str = parts[0];
426 let category = parts[1];
427
428 if let Some(range_parts) = range_str.split_once("..") {
430 let start = u32::from_str_radix(&range_parts.0[2..], 16)?;
431 let end = u32::from_str_radix(&range_parts.1[2..], 16)?;
432
433 let cat_idx =
435 *category_map.entry(category.to_string()).or_insert_with(|| {
436 let idx = category_names.len();
437 category_names.push(category.to_string());
438 category_definitions.push(CategoryData {
440 invoke: true,
441 group: true,
442 length: 0,
443 });
444 idx
445 });
446
447 char_ranges.push((start, end, cat_idx));
448 }
449 }
450 } else {
451 let parts: Vec<&str> = line.split_whitespace().collect();
453 if parts.len() >= 4 {
454 let name = parts[0];
455 let invoke = parts[1] != "0";
456 let group = parts[2] != "0";
457 let length = parts[3].parse::<u8>().unwrap_or(0);
458
459 let cat_idx = *category_map.entry(name.to_string()).or_insert_with(|| {
461 let idx = category_names.len();
462 category_names.push(name.to_string());
463 category_definitions.push(CategoryData {
464 invoke,
465 group,
466 length: length.into(),
467 });
468 idx
469 });
470
471 if cat_idx < category_definitions.len() {
473 category_definitions[cat_idx] = CategoryData {
474 invoke,
475 group,
476 length: length.into(),
477 };
478 }
479 }
480 }
481 }
482
483 char_ranges.sort_by_key(|&(start, _, _)| start);
485
486 let mut boundaries = vec![0u32];
488 for &(start, end, _) in &char_ranges {
489 if start > boundaries[boundaries.len() - 1] {
490 boundaries.push(start);
491 }
492 boundaries.push(end + 1);
493 }
494 if boundaries[boundaries.len() - 1] < 0x10FFFF {
495 boundaries.push(0x10FFFF);
496 }
497
498 let ranges_clone = char_ranges.clone();
500 let mapping = LookupTable::from_fn(boundaries, &|c, buff| {
501 let code = c;
502
503 for &(start, end, cat_idx) in &ranges_clone {
505 if code >= start && code <= end {
506 buff.push(CategoryId(cat_idx));
507 return;
508 }
509 }
510
511 buff.push(CategoryId(0));
513 });
514
515 Ok(CharacterDefinition::new(
516 category_definitions,
517 category_names,
518 mapping,
519 ))
520 }
521
522 fn build_unknown_dict_from_content(
523 _content: &str,
524 _char_def: &CharacterDefinition,
525 ) -> Result<UnknownDictionary> {
526 Ok(UnknownDictionary {
528 category_references: vec![vec![0]; 6], costs: vec![], words_idx_data: vec![],
531 words_data: vec![],
532 })
533 }
534
535 fn build_prefix_dict_from_content(_content: &str) -> Result<PrefixDictionary> {
536 PrefixDictionary::from_word_entry_map(&std::collections::BTreeMap::new())
539 .map_err(|err| anyhow::anyhow!("failed to build empty prefix dictionary: {err}"))
540 }
541
542 fn create_minimal_connection_matrix() -> Result<ConnectionCostMatrix> {
543 let matrix_size = 6u16;
545 let mut matrix_data = vec![0u8; 4]; matrix_data[0..2].copy_from_slice(&matrix_size.to_le_bytes());
549 matrix_data[2..4].copy_from_slice(&matrix_size.to_le_bytes());
550
551 let cost_data_size = (matrix_size as usize) * (matrix_size as usize) * 2; matrix_data.extend(vec![0u8; cost_data_size]);
554
555 Ok(ConnectionCostMatrix::load(matrix_data)?)
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562 use std::io::Cursor;
563
564 #[test]
565 fn test_ipadic_format_13_columns() {
566 let seed_csv = "東京,0,0,5000,名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー\n\
568 行く,1,1,4000,動詞,自立,*,*,五段・カ行促音便,基本形,行く,イク,イク\n";
569 let char_def = "DEFAULT 0 1 0\nHIRAGANA 1 1 0\n0x3042..0x3096 HIRAGANA\n";
570 let unk_def = "DEFAULT,0,0,1500,名詞,一般,*,*,*,*,*,*,*\n";
571 let feature_def = "UNIGRAM:%F[0]\nUNIGRAM:%F[1]\n";
572 let rewrite_def = "*\tUNK\n";
573
574 let config = TrainerConfig::from_readers(
575 Cursor::new(seed_csv),
576 Cursor::new(char_def),
577 Cursor::new(unk_def),
578 Cursor::new(feature_def),
579 Cursor::new(rewrite_def),
580 )
581 .unwrap();
582
583 assert_eq!(config.surfaces().len(), 2);
584 assert!(config.surfaces().contains(&"東京".to_string()));
585 assert!(config.surfaces().contains(&"行く".to_string()));
586
587 let tokyo_features = config.surface_features().get("東京").unwrap();
589 assert_eq!(
590 tokyo_features,
591 "名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー"
592 );
593 }
594
595 #[test]
596 fn test_ko_dic_format_8_columns() {
597 let seed_csv = "한국,0,0,5000,NNG,Korea,F,han-guk\n\
599 안녕,1,1,4000,NNG,hello,F,an-nyeong\n";
600 let char_def = "DEFAULT 0 1 0\nHANGUL 1 1 0\n0xAC00..0xD7A3 HANGUL\n";
601 let unk_def = "DEFAULT,0,0,1500,NNG,unknown,F,*\n";
602 let feature_def = "UNIGRAM:%F[0]\n";
603 let rewrite_def = "*\tUNK\n";
604
605 let config = TrainerConfig::from_readers(
606 Cursor::new(seed_csv),
607 Cursor::new(char_def),
608 Cursor::new(unk_def),
609 Cursor::new(feature_def),
610 Cursor::new(rewrite_def),
611 )
612 .unwrap();
613
614 assert_eq!(config.surfaces().len(), 2);
615 assert!(config.surfaces().contains(&"한국".to_string()));
616 assert!(config.surfaces().contains(&"안녕".to_string()));
617
618 let korea_features = config.surface_features().get("한국").unwrap();
620 assert_eq!(korea_features, "NNG,Korea,F,han-guk");
621 }
622
623 #[test]
624 fn test_cc_cedict_format_8_columns() {
625 let seed_csv = "中国,0,0,5000,n,China,*,zhong1guo2\n\
627 你好,1,1,4000,x,hello,*,ni3hao3\n";
628 let char_def = "DEFAULT 0 1 0\nHANZI 1 1 0\n0x4E00..0x9FFF HANZI\n";
629 let unk_def = "DEFAULT,0,0,1500,n,unknown,*,*\n";
630 let feature_def = "UNIGRAM:%F[0]\n";
631 let rewrite_def = "*\tUNK\n";
632
633 let config = TrainerConfig::from_readers(
634 Cursor::new(seed_csv),
635 Cursor::new(char_def),
636 Cursor::new(unk_def),
637 Cursor::new(feature_def),
638 Cursor::new(rewrite_def),
639 )
640 .unwrap();
641
642 assert_eq!(config.surfaces().len(), 2);
643 assert!(config.surfaces().contains(&"中国".to_string()));
644 assert!(config.surfaces().contains(&"你好".to_string()));
645
646 let china_features = config.surface_features().get("中国").unwrap();
648 assert_eq!(china_features, "n,China,*,zhong1guo2");
649 }
650
651 #[test]
652 fn test_unidic_format_21_columns() {
653 let seed_csv = "東京,0,0,5000,名詞,固有名詞,地名,一般,*,*,トウキョウ,東京,東京,東京,東京,東京,トウキョウ,トーキョー,東京,東京,1\n";
655 let char_def = "DEFAULT 0 1 0\nKANJI 0 0 2\n0x4E00..0x9FFF KANJI\n";
656 let unk_def = "DEFAULT,0,0,1500,名詞,普通名詞,一般,*,*,*,*,*,*,*,*,*,*,*,*,*,*\n";
657 let feature_def = "UNIGRAM:%F[0]\nUNIGRAM:%F[1]\n";
658 let rewrite_def = "*\tUNK\n";
659
660 let config = TrainerConfig::from_readers(
661 Cursor::new(seed_csv),
662 Cursor::new(char_def),
663 Cursor::new(unk_def),
664 Cursor::new(feature_def),
665 Cursor::new(rewrite_def),
666 )
667 .unwrap();
668
669 assert_eq!(config.surfaces().len(), 1);
670 assert!(config.surfaces().contains(&"東京".to_string()));
671
672 let tokyo_features = config.surface_features().get("東京").unwrap();
674 assert_eq!(
675 tokyo_features,
676 "名詞,固有名詞,地名,一般,*,*,トウキョウ,東京,東京,東京,東京,東京,トウキョウ,トーキョー,東京,東京,1"
677 );
678 }
679
680 #[test]
681 fn test_mixed_column_counts() {
682 let seed_csv = "東京,0,0,5000,名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー\n\
684 한국,1,1,4000,NNG,Korea,F,han-guk\n\
685 中国,2,2,3000,n,China,*,zhong1guo2\n";
686 let char_def = "DEFAULT 0 1 0\n";
687 let unk_def = "DEFAULT,0,0,1500,*,*,*,*\n";
688 let feature_def = "UNIGRAM:%F[0]\n";
689 let rewrite_def = "*\tUNK\n";
690
691 let config = TrainerConfig::from_readers(
692 Cursor::new(seed_csv),
693 Cursor::new(char_def),
694 Cursor::new(unk_def),
695 Cursor::new(feature_def),
696 Cursor::new(rewrite_def),
697 )
698 .unwrap();
699
700 assert_eq!(config.surfaces().len(), 3);
701
702 assert_eq!(
704 config.surface_features().get("東京").unwrap(),
705 "名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー"
706 );
707 assert_eq!(
708 config.surface_features().get("한국").unwrap(),
709 "NNG,Korea,F,han-guk"
710 );
711 assert_eq!(
712 config.surface_features().get("中国").unwrap(),
713 "n,China,*,zhong1guo2"
714 );
715 }
716
717 #[test]
718 fn test_trainer_config_creation() {
719 let lexicon_data = "外国,0,0,5000,名詞,一般,*,*,*,*,外国,ガイコク,ガイコク\n人,1,1,5000,名詞,接尾,一般,*,*,*,人,ジン,ジン\n";
721 let char_data = "# char.def placeholder\n";
722 let unk_data = "# unk.def placeholder\n";
723 let feature_data = "UNIGRAM:%F[0]\nLEFT:%L[0]\nRIGHT:%R[0]\n";
724 let rewrite_data = "# rewrite.def placeholder\n";
725
726 let result = TrainerConfig::from_readers(
727 Cursor::new(lexicon_data.as_bytes()),
728 Cursor::new(char_data.as_bytes()),
729 Cursor::new(unk_data.as_bytes()),
730 Cursor::new(feature_data.as_bytes()),
731 Cursor::new(rewrite_data.as_bytes()),
732 );
733
734 assert!(result.is_ok());
736 let config = result.unwrap();
737 assert_eq!(config.surfaces().len(), 2);
739 assert!(config.surfaces().contains(&"外国".to_string()));
740 assert!(config.surfaces().contains(&"人".to_string()));
741 }
742
743 #[test]
744 fn test_unk_categories_ipadic() {
745 let lexicon_data = "東京,0,0,5000,名詞,固有名詞,地域,一般,*,*,東京,トウキョウ,トーキョー\n";
747 let char_data = "DEFAULT 0 1 0\nHIRAGANA 1 1 0\n";
748 let unk_data = "DEFAULT,0,0,1500,名詞,一般,*,*,*,*,*,*,*\nHIRAGANA,1,1,2000,名詞,代名詞,一般,*,*,*,*,*,*\n";
749 let feature_data = "UNIGRAM:%F[0]\n";
750 let rewrite_data = "*\tUNK\n";
751
752 let config = TrainerConfig::from_readers(
753 Cursor::new(lexicon_data),
754 Cursor::new(char_data),
755 Cursor::new(unk_data),
756 Cursor::new(feature_data),
757 Cursor::new(rewrite_data),
758 )
759 .unwrap();
760
761 assert_eq!(config.unk_categories.len(), 2);
763 assert_eq!(
764 config.unk_categories.get("DEFAULT").unwrap(),
765 "名詞,一般,*,*,*,*,*,*,*"
766 );
767 assert_eq!(
768 config.unk_categories.get("HIRAGANA").unwrap(),
769 "名詞,代名詞,一般,*,*,*,*,*,*"
770 );
771 }
772
773 #[test]
774 fn test_unk_categories_ko_dic() {
775 let lexicon_data = "한국,0,0,5000,NNG,Korea,F,han-guk\n";
777 let char_data = "DEFAULT 0 1 0\n";
778 let unk_data = "DEFAULT,0,0,1500,NNG,unknown,F,*\n";
779 let feature_data = "UNIGRAM:%F[0]\n";
780 let rewrite_data = "*\tUNK\n";
781
782 let config = TrainerConfig::from_readers(
783 Cursor::new(lexicon_data),
784 Cursor::new(char_data),
785 Cursor::new(unk_data),
786 Cursor::new(feature_data),
787 Cursor::new(rewrite_data),
788 )
789 .unwrap();
790
791 assert_eq!(config.unk_categories.len(), 1);
792 assert_eq!(
793 config.unk_categories.get("DEFAULT").unwrap(),
794 "NNG,unknown,F,*"
795 );
796 }
797
798 #[test]
799 fn test_unk_categories_cc_cedict() {
800 let lexicon_data = "中国,0,0,5000,n,China,*,zhong1guo2\n";
802 let char_data = "DEFAULT 0 1 0\n";
803 let unk_data = "DEFAULT,0,0,1500,n,unknown,*,*\n";
804 let feature_data = "UNIGRAM:%F[0]\n";
805 let rewrite_data = "*\tUNK\n";
806
807 let config = TrainerConfig::from_readers(
808 Cursor::new(lexicon_data),
809 Cursor::new(char_data),
810 Cursor::new(unk_data),
811 Cursor::new(feature_data),
812 Cursor::new(rewrite_data),
813 )
814 .unwrap();
815
816 assert_eq!(config.unk_categories.len(), 1);
817 assert_eq!(
818 config.unk_categories.get("DEFAULT").unwrap(),
819 "n,unknown,*,*"
820 );
821 }
822}