1use std::borrow::Cow;
2use std::env;
3use std::fs::File;
4use std::io::Read;
5use std::path::Path;
6
7use serde_json::{Value, json};
8
9use crate::character_filter::{BoxCharacterFilter, CharacterFilterLoader, OffsetMapping};
10use crate::token_filter::{BoxTokenFilter, TokenFilterLoader};
11use lindera::LinderaResult;
12use lindera::dictionary::Lattice;
13use lindera::error::LinderaErrorKind;
14use lindera::mode::Mode;
15use lindera::segmenter::Segmenter;
16use lindera::space_penalty::SpacePenaltyConfig;
17use lindera::token::Token;
18
19pub type TokenizerConfig = Value;
20
21fn yaml_to_config(file_path: &Path) -> LinderaResult<TokenizerConfig> {
22 let mut input_read = File::open(file_path).map_err(|err| {
23 LinderaErrorKind::Io.with_error(err).add_context(format!(
24 "Failed to open tokenizer config file: {}",
25 file_path.display()
26 ))
27 })?;
28
29 let mut buffer = Vec::new();
30 input_read.read_to_end(&mut buffer).map_err(|err| {
31 LinderaErrorKind::Io.with_error(err).add_context(format!(
32 "Failed to read tokenizer config file: {}",
33 file_path.display()
34 ))
35 })?;
36
37 match serde_yaml_ng::from_slice::<serde_yaml_ng::Value>(&buffer) {
38 Ok(value) => {
39 match value {
41 serde_yaml_ng::Value::Mapping(_) => {
42 Ok(serde_json::to_value(value).map_err(|err| {
43 LinderaErrorKind::Deserialize
44 .with_error(err)
45 .add_context(format!(
46 "Failed to convert YAML to JSON for config file: {}",
47 file_path.display()
48 ))
49 })?)
50 }
51 _ => Err(LinderaErrorKind::Deserialize
52 .with_error(anyhow::anyhow!("Invalid YAML"))
53 .add_context(format!(
54 "Config file must contain a YAML mapping: {}",
55 file_path.display()
56 ))),
57 }
58 }
59 Err(err) => Err(LinderaErrorKind::Deserialize
60 .with_error(err)
61 .add_context(format!(
62 "Failed to parse YAML config file: {}",
63 file_path.display()
64 ))),
65 }
66}
67
68fn empty_config() -> Value {
70 json!({
71 "segmenter": {},
72 "character_filters": [],
73 "token_filters": []
74 })
75}
76
77fn ensure_keys(mut config: Value) -> Value {
79 if config.get("segmenter").is_none() {
80 config["segmenter"] = json!({});
81 }
82
83 if config.get("character_filters").is_none() {
84 config["character_filters"] = json!([]);
85 }
86
87 if config.get("token_filters").is_none() {
88 config["token_filters"] = json!([]);
89 }
90
91 config
92}
93
94#[derive(Debug)]
95pub struct TokenizerBuilder {
96 config: TokenizerConfig,
97}
98
99impl TokenizerBuilder {
100 pub fn new() -> LinderaResult<Self> {
101 if let Ok(config_path) = env::var("LINDERA_CONFIG_PATH") {
102 Self::from_file(Path::new(&config_path))
103 } else {
104 Ok(Self {
105 config: empty_config(),
106 })
107 }
108 }
109
110 pub fn from_file(file_path: &Path) -> LinderaResult<Self> {
111 let config = yaml_to_config(file_path)?;
112
113 Ok(TokenizerBuilder {
114 config: ensure_keys(config),
115 })
116 }
117
118 pub fn from_config(config: TokenizerConfig) -> LinderaResult<Self> {
119 Ok(TokenizerBuilder {
120 config: ensure_keys(config),
121 })
122 }
123
124 pub fn set_segmenter_mode(&mut self, mode: &Mode) -> &mut Self {
125 self.config["segmenter"]["mode"] = json!(mode.as_str());
126 self
127 }
128
129 pub fn set_segmenter_dictionary(&mut self, uri: &str) -> &mut Self {
130 self.config["segmenter"]["dictionary"] = json!(uri);
131 self
132 }
133
134 pub fn set_segmenter_user_dictionary(&mut self, uri: &str) -> &mut Self {
135 self.config["segmenter"]["user_dictionary"] = json!(uri);
136 self
137 }
138
139 pub fn set_segmenter_keep_whitespace(&mut self, keep_whitespace: bool) -> &mut Self {
140 self.config["segmenter"]["keep_whitespace"] = json!(keep_whitespace);
141 self
142 }
143
144 pub fn set_segmenter_max_grouping_len(&mut self, max_grouping_len: usize) -> &mut Self {
155 self.config["segmenter"]["max_grouping_len"] = json!(max_grouping_len);
156 self
157 }
158
159 pub fn set_segmenter_unknown_word_ladder(&mut self, unknown_word_ladder: bool) -> &mut Self {
170 self.config["segmenter"]["unknown_word_ladder"] = json!(unknown_word_ladder);
171 self
172 }
173
174 pub fn set_segmenter_space_penalty(
187 &mut self,
188 space_penalty: Option<&SpacePenaltyConfig>,
189 ) -> &mut Self {
190 self.config["segmenter"]["space_penalty"] = match space_penalty {
191 Some(config) => json!(config),
192 None => json!(false),
193 };
194 self
195 }
196
197 pub fn set_segmenter_space_penalty_from_dictionary(&mut self, enabled: bool) -> &mut Self {
211 self.config["segmenter"]["space_penalty"] = json!(enabled);
212 self
213 }
214
215 pub fn reset_segmenter_space_penalty(&mut self) -> &mut Self {
224 if let Some(segmenter) = self.config["segmenter"].as_object_mut() {
225 segmenter.remove("space_penalty");
226 }
227 self
228 }
229
230 pub fn set_segmenter_use_mmap(&mut self, use_mmap: bool) -> &mut Self {
241 self.config["segmenter"]["use_mmap"] = json!(use_mmap);
242 self
243 }
244
245 pub fn append_character_filter(&mut self, kind: &str, args: &Value) -> &mut Self {
246 if let Some(array) = self.config["character_filters"].as_array_mut() {
247 array.push(json!({ "kind": kind, "args": args }));
248 }
249 self
250 }
251
252 pub fn append_token_filter(&mut self, kind: &str, args: &Value) -> &mut Self {
253 if let Some(array) = self.config["token_filters"].as_array_mut() {
254 array.push(json!({ "kind": kind, "args": args }));
255 }
256 self
257 }
258
259 pub fn build(&self) -> LinderaResult<Tokenizer> {
260 Tokenizer::from_config(&self.config).map_err(|err| {
261 LinderaErrorKind::Parse.with_error(anyhow::anyhow!("failed to build tokenizer: {err}"))
262 })
263 }
264}
265
266pub struct Tokenizer {
267 pub segmenter: Segmenter,
272
273 pub character_filters: Vec<BoxCharacterFilter>,
279
280 pub token_filters: Vec<BoxTokenFilter>,
285}
286
287impl Tokenizer {
288 pub fn new(segmenter: Segmenter) -> Self {
304 Self {
305 segmenter,
306 character_filters: Vec::new(),
307 token_filters: Vec::new(),
308 }
309 }
310
311 pub fn from_config(config: &TokenizerConfig) -> LinderaResult<Self> {
312 let segmenter_config = config.get("segmenter").ok_or_else(|| {
313 LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!("missing segmenter config."))
314 })?;
315 let segmenter = Segmenter::from_config(segmenter_config)?;
316
317 let mut tokenizer = Tokenizer::new(segmenter);
319
320 if let Some(character_filter_settings) = config["character_filters"].as_array() {
322 for character_filter_setting in character_filter_settings {
323 let character_filter_name = character_filter_setting["kind"].as_str();
324 if let Some(character_filter_name) = character_filter_name {
325 tokenizer.append_character_filter(CharacterFilterLoader::load_from_value(
327 character_filter_name,
328 &character_filter_setting["args"],
329 )?);
330 }
331 }
332 }
333
334 if let Some(token_filter_settings) = config["token_filters"].as_array() {
336 for token_filter_setting in token_filter_settings {
337 let token_filter_name = token_filter_setting["kind"].as_str();
338 if let Some(token_filter_name) = token_filter_name {
339 tokenizer.append_token_filter(TokenFilterLoader::load_from_value(
341 token_filter_name,
342 &token_filter_setting["args"],
343 )?);
344 }
345 }
346 }
347
348 Ok(tokenizer)
349 }
350
351 pub fn append_character_filter(&mut self, character_filter: BoxCharacterFilter) -> &mut Self {
366 self.character_filters.push(character_filter);
367
368 self
369 }
370
371 pub fn append_token_filter(&mut self, token_filter: BoxTokenFilter) -> &mut Self {
386 self.token_filters.push(token_filter);
387
388 self
389 }
390
391 pub fn tokenize<'a>(&'a self, text: &'a str) -> LinderaResult<Vec<Token<'a>>> {
424 let mut lattice = Lattice::default();
425 self.tokenize_with_lattice(text, &mut lattice)
426 }
427
428 pub fn tokenize_with_lattice<'a>(
462 &'a self,
463 text: &'a str,
464 lattice: &mut Lattice,
465 ) -> LinderaResult<Vec<Token<'a>>> {
466 let mut normalized_text: Cow<'a, str> = Cow::Borrowed(text);
467
468 let mut offset_mappings: Vec<OffsetMapping> =
469 Vec::with_capacity(self.character_filters.len());
470
471 if !self.character_filters.is_empty() {
474 let text_mut = normalized_text.to_mut();
476
477 for character_filter in &self.character_filters {
478 let mapping = character_filter.apply(text_mut)?;
479
480 if !mapping.is_empty() {
481 offset_mappings.push(mapping);
484 }
485 }
486 }
487
488 let final_text_len = normalized_text.len();
490
491 let mut tokens = self
494 .segmenter
495 .segment_with_lattice(normalized_text, lattice)?;
496
497 for token_filter in &self.token_filters {
499 token_filter.apply(&mut tokens)?;
500 }
501
502 correct_offsets(&mut tokens, &offset_mappings, final_text_len);
504
505 Ok(tokens)
506 }
507
508 pub fn tokenize_nbest<'a>(
513 &'a self,
514 text: &'a str,
515 n: usize,
516 unique: bool,
517 cost_threshold: Option<i64>,
518 ) -> LinderaResult<Vec<(Vec<Token<'a>>, i64)>> {
519 let mut lattice = Lattice::default();
520 self.tokenize_nbest_with_lattice(text, &mut lattice, n, unique, cost_threshold)
521 }
522
523 pub fn tokenize_nbest_with_lattice<'a>(
529 &'a self,
530 text: &'a str,
531 lattice: &mut Lattice,
532 n: usize,
533 unique: bool,
534 cost_threshold: Option<i64>,
535 ) -> LinderaResult<Vec<(Vec<Token<'a>>, i64)>> {
536 let mut normalized_text: Cow<'a, str> = Cow::Borrowed(text);
537
538 let mut offset_mappings: Vec<OffsetMapping> =
539 Vec::with_capacity(self.character_filters.len());
540
541 if !self.character_filters.is_empty() {
542 let text_mut = normalized_text.to_mut();
543 for character_filter in &self.character_filters {
544 let mapping = character_filter.apply(text_mut)?;
545 if !mapping.is_empty() {
546 offset_mappings.push(mapping);
547 }
548 }
549 }
550
551 let final_text_len = normalized_text.len();
552
553 let mut all_results = self.segmenter.segment_nbest_with_lattice(
554 normalized_text,
555 lattice,
556 n,
557 unique,
558 cost_threshold,
559 )?;
560
561 for (tokens, _cost) in &mut all_results {
563 for token_filter in &self.token_filters {
564 token_filter.apply(tokens)?;
565 }
566
567 correct_offsets(tokens, &offset_mappings, final_text_len);
568 }
569
570 Ok(all_results)
571 }
572}
573
574pub(crate) fn correct_offsets(
589 tokens: &mut [Token<'_>],
590 offset_mappings: &[OffsetMapping],
591 final_text_len: usize,
592) {
593 if offset_mappings.is_empty() {
594 return;
595 }
596 for token in tokens.iter_mut() {
597 for mapping in offset_mappings.iter().rev() {
599 token.byte_start = mapping.correct_offset(token.byte_start, final_text_len);
600 token.byte_end = mapping.correct_offset(token.byte_end, final_text_len);
601 }
602 }
603}
604
605impl Clone for Tokenizer {
606 fn clone(&self) -> Self {
623 let character_filters: Vec<BoxCharacterFilter> = self
624 .character_filters
625 .iter()
626 .map(|filter| filter.box_clone())
627 .collect();
628
629 let token_filters: Vec<BoxTokenFilter> = self
630 .token_filters
631 .iter()
632 .map(|filter| filter.box_clone())
633 .collect();
634
635 Tokenizer {
636 character_filters,
637 segmenter: self.segmenter.clone(),
638 token_filters,
639 }
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use lindera::space_penalty::{SpacePenaltyConfig, SpacePenaltyRule};
646
647 use super::TokenizerBuilder;
648
649 #[test]
650 fn test_set_segmenter_use_mmap_writes_flat_key_under_segmenter() {
651 let mut builder = TokenizerBuilder::new().unwrap();
652 builder.set_segmenter_use_mmap(true);
653
654 assert_eq!(builder.config["segmenter"]["use_mmap"], true);
655 }
656
657 #[test]
660 fn test_set_segmenter_space_penalty_writes_false_true_or_rules() {
661 let mut builder = TokenizerBuilder::new().unwrap();
662 assert!(builder.config["segmenter"].get("space_penalty").is_none());
663
664 builder.set_segmenter_space_penalty(None);
665 assert_eq!(builder.config["segmenter"]["space_penalty"], false);
666
667 builder.set_segmenter_space_penalty_from_dictionary(true);
668 assert_eq!(builder.config["segmenter"]["space_penalty"], true);
669
670 let rules = SpacePenaltyConfig::new(vec![SpacePenaltyRule::new(["JKS"], 6000)]);
671 builder.set_segmenter_space_penalty(Some(&rules));
672 assert_eq!(
673 builder.config["segmenter"]["space_penalty"]["rules"][0]["cost"],
674 6000
675 );
676 }
677
678 #[test]
680 fn test_reset_segmenter_space_penalty_removes_the_key() {
681 let mut builder = TokenizerBuilder::new().unwrap();
682 builder.reset_segmenter_space_penalty();
684 assert!(builder.config["segmenter"].get("space_penalty").is_none());
685
686 builder.set_segmenter_space_penalty(None);
687 builder.reset_segmenter_space_penalty();
688 assert!(builder.config["segmenter"].get("space_penalty").is_none());
689 builder.set_segmenter_keep_whitespace(true);
691 builder.set_segmenter_space_penalty_from_dictionary(true);
692 builder.reset_segmenter_space_penalty();
693 assert_eq!(builder.config["segmenter"]["keep_whitespace"], true);
694 }
695
696 #[cfg(feature = "embed-ipadic")]
697 #[test]
698 fn test_tokenizer_config_from_slice() {
699 use std::path::PathBuf;
700
701 use crate::tokenizer::yaml_to_config;
702
703 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
704 .join("../resources")
705 .join("config")
706 .join("lindera.yml");
707
708 let result = yaml_to_config(&config_file);
709
710 assert!(result.is_ok());
711 }
712
713 #[test]
714 #[cfg(feature = "embed-ipadic")]
715 fn test_tokenizer_config_clone() {
716 use std::path::PathBuf;
717
718 use crate::tokenizer::yaml_to_config;
719
720 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
721 .join("../resources")
722 .join("config")
723 .join("lindera.yml");
724
725 let tokenizer_config = yaml_to_config(&config_file).unwrap();
726
727 let cloned_tokenizer_config = tokenizer_config.clone();
728
729 assert_eq!(tokenizer_config, cloned_tokenizer_config);
730 }
731
732 #[test]
736 fn test_binding_sample_configs_match_shipped_config() {
737 use std::path::PathBuf;
738
739 use crate::tokenizer::yaml_to_config;
740
741 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
742 let shipped = yaml_to_config(&root.join("resources/config/lindera.yml")).unwrap();
743
744 for sample in [
745 "lindera-python/resources/lindera.yml",
746 "lindera-nodejs/resources/lindera.yml",
747 ] {
748 let config = yaml_to_config(&root.join(sample)).unwrap();
749 assert_eq!(
750 config, shipped,
751 "{sample} differs from the shipped configuration"
752 );
753 }
754 }
755
756 #[test]
757 #[cfg(feature = "embed-ipadic")]
758 fn test_tokenize_ipadic() {
759 use std::borrow::Cow;
760 use std::path::PathBuf;
761
762 use crate::tokenizer::TokenizerBuilder;
763
764 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
765 .join("../resources")
766 .join("config")
767 .join("lindera.yml");
768
769 let builder = TokenizerBuilder::from_file(&config_file).unwrap();
770
771 let tokenizer = builder.build().unwrap();
772
773 {
774 let text = "リンデラは形態素解析エンジンです。";
775 let mut tokens = tokenizer.tokenize(text).unwrap();
776 let mut tokens_iter = tokens.iter_mut();
777 {
778 let token = tokens_iter.next().unwrap();
779 assert_eq!(token.surface, Cow::Borrowed("Lindera"));
780 assert_eq!(token.byte_start, 0);
781 assert_eq!(token.byte_end, 15);
782 assert_eq!(token.position, 0);
783 assert_eq!(token.position_length, 1);
784 assert!(token.word_id.is_unknown());
785 assert_eq!(
786 token.details,
787 Some(vec![
788 Cow::Borrowed("名詞"),
789 Cow::Borrowed("固有名詞"),
790 Cow::Borrowed("組織"),
791 Cow::Borrowed("*"),
792 Cow::Borrowed("*"),
793 Cow::Borrowed("*"),
794 Cow::Borrowed("*"),
795 Cow::Borrowed("*"),
796 Cow::Borrowed("*"),
797 ])
798 );
799 }
800 {
801 let token = tokens_iter.next().unwrap();
802 assert_eq!(token.surface, Cow::Borrowed("形態素"));
803 assert_eq!(token.byte_start, 18);
804 assert_eq!(token.byte_end, 27);
805 assert_eq!(token.position, 2);
806 assert_eq!(token.position_length, 1);
807 assert_eq!(
808 token.details,
809 Some(vec![
810 Cow::Borrowed("名詞"),
811 Cow::Borrowed("一般"),
812 Cow::Borrowed("*"),
813 Cow::Borrowed("*"),
814 Cow::Borrowed("*"),
815 Cow::Borrowed("*"),
816 Cow::Borrowed("形態素"),
817 Cow::Borrowed("ケイタイソ"),
818 Cow::Borrowed("ケイタイソ"),
819 ])
820 );
821 }
822 {
823 let token = tokens_iter.next().unwrap();
824 assert_eq!(token.surface, Cow::Borrowed("解析"));
825 assert_eq!(token.byte_start, 27);
826 assert_eq!(token.byte_end, 33);
827 assert_eq!(token.position, 3);
828 assert_eq!(token.position_length, 1);
829 assert_eq!(
830 token.details,
831 Some(vec![
832 Cow::Borrowed("名詞"),
833 Cow::Borrowed("サ変接続"),
834 Cow::Borrowed("*"),
835 Cow::Borrowed("*"),
836 Cow::Borrowed("*"),
837 Cow::Borrowed("*"),
838 Cow::Borrowed("解析"),
839 Cow::Borrowed("カイセキ"),
840 Cow::Borrowed("カイセキ"),
841 ])
842 );
843 }
844 {
845 let token = tokens_iter.next().unwrap();
846 assert_eq!(token.surface, Cow::Borrowed("エンジン"));
847 assert_eq!(token.byte_start, 33);
848 assert_eq!(token.byte_end, 48);
849 assert_eq!(token.position, 4);
850 assert_eq!(token.position_length, 1);
851 assert_eq!(
852 token.details,
853 Some(vec![
854 Cow::Borrowed("名詞"),
855 Cow::Borrowed("一般"),
856 Cow::Borrowed("*"),
857 Cow::Borrowed("*"),
858 Cow::Borrowed("*"),
859 Cow::Borrowed("*"),
860 Cow::Borrowed("エンジン"),
861 Cow::Borrowed("エンジン"),
862 Cow::Borrowed("エンジン"),
863 ])
864 );
865 }
866
867 let mut tokens_iter = tokens.iter();
868 {
869 let token = tokens_iter.next().unwrap();
870 let start = token.byte_start;
871 let end = token.byte_end;
872 assert_eq!(token.surface, Cow::Borrowed("Lindera"));
873 assert_eq!(&text[start..end], "リンデラ");
874 }
875 }
876
877 {
878 let text = "10㌎のガソリン";
879 let mut tokens = tokenizer.tokenize(text).unwrap();
880 let mut tokens_iter = tokens.iter_mut();
881 {
882 let token = tokens_iter.next().unwrap();
885 assert_eq!(token.surface, Cow::Owned::<str>("10".into()));
886 assert_eq!(token.byte_start, 0);
887 assert_eq!(token.byte_end, 6);
888 assert_eq!(token.position, 0);
889 assert_eq!(token.position_length, 1);
890 }
891 {
892 let token = tokens_iter.next().unwrap();
893 assert_eq!(token.surface, Cow::Borrowed("ガロン"));
894 assert_eq!(token.byte_start, 6);
895 assert_eq!(token.byte_end, 9);
896 assert_eq!(token.position, 1);
897 assert_eq!(token.position_length, 1);
898 assert_eq!(&token.details()[0..3], ["名詞", "接尾", "助数詞"]);
899 }
900 {
901 let token = tokens_iter.next().unwrap();
902 assert_eq!(token.surface, Cow::Borrowed("ガソリン"));
903 assert_eq!(token.byte_start, 12);
904 assert_eq!(token.byte_end, 27);
905 assert_eq!(token.position, 3);
906 assert_eq!(token.position_length, 1);
907 assert_eq!(
908 token.details,
909 Some(vec![
910 Cow::Borrowed("名詞"),
911 Cow::Borrowed("一般"),
912 Cow::Borrowed("*"),
913 Cow::Borrowed("*"),
914 Cow::Borrowed("*"),
915 Cow::Borrowed("*"),
916 Cow::Borrowed("ガソリン"),
917 Cow::Borrowed("ガソリン"),
918 Cow::Borrowed("ガソリン"),
919 ])
920 );
921 }
922
923 let mut tokens_iter = tokens.iter();
924 {
925 let token = tokens_iter.next().unwrap();
926 let start = token.byte_start;
927 let end = token.byte_end;
928 assert_eq!(token.surface, Cow::Owned::<str>("10".into()));
929 assert_eq!(&text[start..end], "10");
930 }
931 {
932 let token = tokens_iter.next().unwrap();
933 let start = token.byte_start;
934 let end = token.byte_end;
935 assert_eq!(token.surface, Cow::Borrowed("ガロン"));
936 assert_eq!(&text[start..end], "㌎");
937 }
938 {
939 let token = tokens_iter.next().unwrap();
940 let start = token.byte_start;
941 let end = token.byte_end;
942 assert_eq!(token.surface, Cow::Borrowed("ガソリン"));
943 assert_eq!(&text[start..end], "ガソリン");
944 }
945 }
946
947 {
948 let text = "お釣りは百三十四円です。";
949 let mut tokens = tokenizer.tokenize(text).unwrap();
950 let mut tokens_iter = tokens.iter_mut();
951 {
952 let token = tokens_iter.next().unwrap();
953 assert_eq!(token.surface, Cow::Borrowed("お釣り"));
954 assert_eq!(token.byte_start, 0);
955 assert_eq!(token.byte_end, 9);
956 assert_eq!(token.position, 0);
957 assert_eq!(token.position_length, 1);
958 assert_eq!(
959 token.details,
960 Some(vec![
961 Cow::Borrowed("名詞"),
962 Cow::Borrowed("一般"),
963 Cow::Borrowed("*"),
964 Cow::Borrowed("*"),
965 Cow::Borrowed("*"),
966 Cow::Borrowed("*"),
967 Cow::Borrowed("お釣り"),
968 Cow::Borrowed("オツリ"),
969 Cow::Borrowed("オツリ"),
970 ])
971 );
972 }
973 {
974 let token = tokens_iter.next().unwrap();
977 assert_eq!(token.surface, Cow::Borrowed("134"));
978 assert_eq!(token.byte_start, 12);
979 assert_eq!(token.byte_end, 24);
980 assert_eq!(token.position, 2);
981 assert_eq!(token.position_length, 4);
982 assert_eq!(
983 token.details,
984 Some(vec![
985 Cow::Borrowed("名詞"),
986 Cow::Borrowed("数"),
987 Cow::Borrowed("*"),
988 Cow::Borrowed("*"),
989 Cow::Borrowed("*"),
990 Cow::Borrowed("*"),
991 Cow::Borrowed("*"),
992 Cow::Borrowed("*"),
993 Cow::Borrowed("*"),
994 ])
995 );
996 }
997 {
998 let token = tokens_iter.next().unwrap();
999 assert_eq!(token.surface, Cow::Borrowed("円"));
1000 assert_eq!(token.byte_start, 24);
1001 assert_eq!(token.byte_end, 27);
1002 assert_eq!(token.position, 6);
1003 assert_eq!(token.position_length, 1);
1004 assert_eq!(
1005 token.details,
1006 Some(vec![
1007 Cow::Borrowed("名詞"),
1008 Cow::Borrowed("接尾"),
1009 Cow::Borrowed("助数詞"),
1010 Cow::Borrowed("*"),
1011 Cow::Borrowed("*"),
1012 Cow::Borrowed("*"),
1013 Cow::Borrowed("円"),
1014 Cow::Borrowed("エン"),
1015 Cow::Borrowed("エン"),
1016 ])
1017 );
1018 }
1019 }
1020
1021 {
1022 let text = "一万円";
1025 let tokens = tokenizer.tokenize(text).unwrap();
1026 let surfaces: Vec<&str> = tokens.iter().map(|token| token.surface.as_ref()).collect();
1027 assert_eq!(surfaces, ["10000", "円"]);
1028 }
1029
1030 {
1031 let text = "ここは騒々しい";
1032 let mut tokens = tokenizer.tokenize(text).unwrap();
1033 let mut tokens_iter = tokens.iter_mut();
1034 {
1035 let token = tokens_iter.next().unwrap();
1036 assert_eq!(token.surface, Cow::Borrowed("ここ"));
1037 assert_eq!(token.byte_start, 0);
1038 assert_eq!(token.byte_end, 6);
1039 assert_eq!(token.position, 0);
1040 assert_eq!(token.position_length, 1);
1041 assert_eq!(
1042 token.details,
1043 Some(vec![
1044 Cow::Borrowed("名詞"),
1045 Cow::Borrowed("代名詞"),
1046 Cow::Borrowed("一般"),
1047 Cow::Borrowed("*"),
1048 Cow::Borrowed("*"),
1049 Cow::Borrowed("*"),
1050 Cow::Borrowed("ここ"),
1051 Cow::Borrowed("ココ"),
1052 Cow::Borrowed("ココ"),
1053 ])
1054 );
1055 }
1056 {
1057 let token = tokens_iter.next().unwrap();
1058 assert_eq!(token.surface, Cow::Borrowed("騒騒しい"));
1059 assert_eq!(token.byte_start, 9);
1060 assert_eq!(token.byte_end, 21);
1061 assert_eq!(token.position, 2);
1062 assert_eq!(token.position_length, 1);
1063 assert_eq!(
1064 token.details,
1065 Some(vec![
1066 Cow::Borrowed("形容詞"),
1067 Cow::Borrowed("自立"),
1068 Cow::Borrowed("*"),
1069 Cow::Borrowed("*"),
1070 Cow::Borrowed("形容詞・イ段"),
1071 Cow::Borrowed("基本形"),
1072 Cow::Borrowed("騒騒しい"),
1073 Cow::Borrowed("ソウゾウシイ"),
1074 Cow::Borrowed("ソーゾーシイ"),
1075 ])
1076 );
1077 }
1078 }
1079 }
1080
1081 #[test]
1082 #[cfg(not(windows))]
1083 #[should_panic(expected = "No such file or directory")]
1084 fn test_create_tokenizer_builder_from_non_existent_file() {
1085 use std::path::PathBuf;
1086
1087 use crate::tokenizer::TokenizerBuilder;
1088
1089 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1090 .join("../resources")
1091 .join("config")
1092 .join("non_existent_file.yml");
1093
1094 TokenizerBuilder::from_file(&config_file).unwrap();
1095 }
1096
1097 #[test]
1098 #[cfg(windows)]
1099 #[should_panic(expected = "The system cannot find the file specified.")]
1100 fn test_create_tokenizer_builder_from_non_existent_file() {
1101 use std::path::PathBuf;
1102
1103 use crate::tokenizer::TokenizerBuilder;
1104
1105 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1106 .join("../resources")
1107 .join("config")
1108 .join("non_existent_file.yml");
1109
1110 TokenizerBuilder::from_file(&config_file).unwrap();
1111 }
1112
1113 #[test]
1114 #[should_panic(expected = "Invalid YAML")]
1115 fn test_create_tokenizer_builder_from_invalid_file() {
1116 use std::path::PathBuf;
1117
1118 use crate::tokenizer::TokenizerBuilder;
1119
1120 let config_file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1121 .join("../resources")
1122 .join("config")
1123 .join("invalid.yml");
1124
1125 TokenizerBuilder::from_file(&config_file).unwrap();
1126 }
1127
1128 #[test]
1129 #[cfg(feature = "embed-ipadic")]
1130 fn test_tokenize_nbest_1best_matches_tokenize() {
1131 use crate::tokenizer::TokenizerBuilder;
1132
1133 let mut builder = TokenizerBuilder::new().unwrap();
1134 builder.set_segmenter_dictionary("embedded://ipadic");
1135
1136 let tokenizer = builder.build().unwrap();
1137
1138 let text = "すもももももももものうち";
1139 let normal_tokens = tokenizer.tokenize(text).unwrap();
1140 let nbest_results = tokenizer.tokenize_nbest(text, 1, false, None).unwrap();
1141
1142 assert_eq!(nbest_results.len(), 1);
1143 let (nbest_tokens, _cost) = &nbest_results[0];
1144 assert_eq!(normal_tokens.len(), nbest_tokens.len());
1145 for (normal, nbest) in normal_tokens.iter().zip(nbest_tokens.iter()) {
1146 assert_eq!(normal.surface.as_ref(), nbest.surface.as_ref());
1147 }
1148 }
1149
1150 #[test]
1151 #[cfg(feature = "embed-ipadic")]
1152 fn test_tokenize_nbest_multiple_results() {
1153 use crate::tokenizer::TokenizerBuilder;
1154
1155 let mut builder = TokenizerBuilder::new().unwrap();
1156 builder.set_segmenter_dictionary("embedded://ipadic");
1157
1158 let tokenizer = builder.build().unwrap();
1159
1160 let text = "すもももももももものうち";
1161 let results = tokenizer.tokenize_nbest(text, 5, false, None).unwrap();
1162
1163 assert!(results.len() >= 2);
1165
1166 for (tokens, _cost) in &results {
1168 assert!(!tokens.is_empty());
1169 }
1170 }
1171}