use crate::utils::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CJKLanguage {
ChineseSimplified,
ChineseTraditional,
Japanese,
Korean,
Vietnamese,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CJKCharacterCategory {
ChineseSimplified,
ChineseTraditional,
ChineseCommon,
Hiragana,
Katakana,
Kanji,
HangulSyllables,
HangulJamo,
HangulCompatibilityJamo,
CJKPunctuation,
CJKSymbols,
CJKRadicals,
CJKStrokes,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CJKSegmentationResult {
pub segments: Vec<CJKSegment>,
pub language: CJKLanguage,
pub confidence: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CJKSegment {
pub text: String,
pub start_pos: usize,
pub end_pos: usize,
pub category: CJKCharacterCategory,
pub confidence: f32,
}
pub struct CJKProcessor {
chinese_dictionary: Option<ChineseDictionary>,
japanese_dictionary: Option<JapaneseDictionary>,
korean_dictionary: Option<KoreanDictionary>,
}
impl CJKProcessor {
pub fn new() -> Self {
Self {
chinese_dictionary: None,
japanese_dictionary: None,
korean_dictionary: None,
}
}
pub async fn load_chinese_dictionary(&mut self, _path: &str) -> Result<()> {
self.chinese_dictionary = Some(ChineseDictionary::new());
Ok(())
}
pub async fn load_japanese_dictionary(&mut self, _path: &str) -> Result<()> {
self.japanese_dictionary = Some(JapaneseDictionary::new());
Ok(())
}
pub async fn load_korean_dictionary(&mut self, _path: &str) -> Result<()> {
self.korean_dictionary = Some(KoreanDictionary::new());
Ok(())
}
pub fn detect_cjk_language(&self, text: &str) -> Vec<(CJKLanguage, f32)> {
let mut scores = HashMap::new();
for c in text.chars() {
if Self::is_chinese_character(c) {
*scores.entry(CJKLanguage::ChineseSimplified).or_insert(0.0) += 1.0;
*scores.entry(CJKLanguage::ChineseTraditional).or_insert(0.0) += 1.0;
} else if Self::is_japanese_character(c) {
*scores.entry(CJKLanguage::Japanese).or_insert(0.0) += 1.0;
} else if Self::is_korean_character(c) {
*scores.entry(CJKLanguage::Korean).or_insert(0.0) += 1.0;
}
}
let total_chars = text.chars().count() as f32;
if total_chars == 0.0 {
return vec![];
}
scores
.into_iter()
.map(|(lang, score)| (lang, score / total_chars))
.collect()
}
pub fn segment_text(&self, text: &str, language: CJKLanguage) -> Result<CJKSegmentationResult> {
match language {
CJKLanguage::ChineseSimplified | CJKLanguage::ChineseTraditional => {
self.segment_chinese(text, language)
}
CJKLanguage::Japanese => self.segment_japanese(text),
CJKLanguage::Korean => self.segment_korean(text),
CJKLanguage::Vietnamese => self.segment_vietnamese(text),
}
}
fn segment_chinese(&self, text: &str, language: CJKLanguage) -> Result<CJKSegmentationResult> {
let mut segments = Vec::new();
let mut current_segment = String::new();
let mut start_pos = 0;
for (i, c) in text.char_indices() {
if Self::is_chinese_character(c) {
if current_segment.is_empty() {
start_pos = i;
}
current_segment.push(c);
} else {
if !current_segment.is_empty() {
let category = match language {
CJKLanguage::ChineseSimplified => CJKCharacterCategory::ChineseSimplified,
CJKLanguage::ChineseTraditional => CJKCharacterCategory::ChineseTraditional,
_ => CJKCharacterCategory::ChineseCommon,
};
segments.push(CJKSegment {
text: current_segment.clone(),
start_pos,
end_pos: i,
category,
confidence: 1.0,
});
current_segment.clear();
}
}
}
if !current_segment.is_empty() {
let category = match language {
CJKLanguage::ChineseSimplified => CJKCharacterCategory::ChineseSimplified,
CJKLanguage::ChineseTraditional => CJKCharacterCategory::ChineseTraditional,
_ => CJKCharacterCategory::ChineseCommon,
};
segments.push(CJKSegment {
text: current_segment,
start_pos,
end_pos: text.len(),
category,
confidence: 1.0,
});
}
Ok(CJKSegmentationResult {
segments,
language,
confidence: 1.0,
})
}
fn segment_japanese(&self, text: &str) -> Result<CJKSegmentationResult> {
let mut segments = Vec::new();
let mut current_segment = String::new();
let mut start_pos = 0;
let mut current_category = None;
for (i, c) in text.char_indices() {
let category = if Self::is_hiragana(c) {
Some(CJKCharacterCategory::Hiragana)
} else if Self::is_katakana(c) {
Some(CJKCharacterCategory::Katakana)
} else if Self::is_kanji(c) {
Some(CJKCharacterCategory::Kanji)
} else {
None
};
if let Some(cat) = category {
if current_category == Some(cat) {
current_segment.push(c);
} else {
if !current_segment.is_empty() {
segments.push(CJKSegment {
text: current_segment.clone(),
start_pos,
end_pos: i,
category: current_category.unwrap(),
confidence: 1.0,
});
}
current_segment = c.to_string();
start_pos = i;
current_category = Some(cat);
}
} else {
if !current_segment.is_empty() {
segments.push(CJKSegment {
text: current_segment.clone(),
start_pos,
end_pos: i,
category: current_category.unwrap(),
confidence: 1.0,
});
current_segment.clear();
current_category = None;
}
}
}
if !current_segment.is_empty() {
segments.push(CJKSegment {
text: current_segment,
start_pos,
end_pos: text.len(),
category: current_category.unwrap(),
confidence: 1.0,
});
}
Ok(CJKSegmentationResult {
segments,
language: CJKLanguage::Japanese,
confidence: 1.0,
})
}
fn segment_korean(&self, text: &str) -> Result<CJKSegmentationResult> {
let mut segments = Vec::new();
let mut current_segment = String::new();
let mut start_pos = 0;
for (i, c) in text.char_indices() {
if Self::is_korean_character(c) {
if current_segment.is_empty() {
start_pos = i;
}
current_segment.push(c);
} else {
if !current_segment.is_empty() {
let category =
if Self::is_hangul_syllable(current_segment.chars().next().unwrap()) {
CJKCharacterCategory::HangulSyllables
} else {
CJKCharacterCategory::HangulJamo
};
segments.push(CJKSegment {
text: current_segment.clone(),
start_pos,
end_pos: i,
category,
confidence: 1.0,
});
current_segment.clear();
}
}
}
if !current_segment.is_empty() {
let category = if Self::is_hangul_syllable(current_segment.chars().next().unwrap()) {
CJKCharacterCategory::HangulSyllables
} else {
CJKCharacterCategory::HangulJamo
};
segments.push(CJKSegment {
text: current_segment,
start_pos,
end_pos: text.len(),
category,
confidence: 1.0,
});
}
Ok(CJKSegmentationResult {
segments,
language: CJKLanguage::Korean,
confidence: 1.0,
})
}
fn segment_vietnamese(&self, text: &str) -> Result<CJKSegmentationResult> {
Ok(CJKSegmentationResult {
segments: vec![CJKSegment {
text: text.to_string(),
start_pos: 0,
end_pos: text.len(),
category: CJKCharacterCategory::CJKSymbols, confidence: 1.0,
}],
language: CJKLanguage::Vietnamese,
confidence: 1.0,
})
}
pub fn is_chinese_character(c: char) -> bool {
let code = c as u32;
matches!(
code,
0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0x20000..=0x2A6DF | 0x2A700..=0x2B73F | 0x2B740..=0x2B81F | 0x2B820..=0x2CEAF | 0x2CEB0..=0x2EBEF | 0x30000..=0x3134F | 0x31350..=0x323AF | 0x323B0..=0x32B2F )
}
pub fn is_hiragana(c: char) -> bool {
let code = c as u32;
matches!(code, 0x3040..=0x309F)
}
pub fn is_katakana(c: char) -> bool {
let code = c as u32;
matches!(code, 0x30A0..=0x30FF)
}
pub fn is_kanji(c: char) -> bool {
Self::is_chinese_character(c) }
pub fn is_japanese_character(c: char) -> bool {
Self::is_hiragana(c) || Self::is_katakana(c) || Self::is_kanji(c)
}
pub fn is_korean_character(c: char) -> bool {
let code = c as u32;
matches!(
code,
0xAC00..=0xD7AF | 0x1100..=0x11FF | 0x3130..=0x318F )
}
pub fn is_hangul_syllable(c: char) -> bool {
let code = c as u32;
matches!(code, 0xAC00..=0xD7AF)
}
pub fn is_cjk_character(c: char) -> bool {
Self::is_chinese_character(c)
|| Self::is_japanese_character(c)
|| Self::is_korean_character(c)
}
pub fn get_character_category(c: char) -> Option<CJKCharacterCategory> {
if Self::is_chinese_character(c) {
Some(CJKCharacterCategory::ChineseCommon)
} else if Self::is_hiragana(c) {
Some(CJKCharacterCategory::Hiragana)
} else if Self::is_katakana(c) {
Some(CJKCharacterCategory::Katakana)
} else if Self::is_kanji(c) {
Some(CJKCharacterCategory::Kanji)
} else if Self::is_hangul_syllable(c) {
Some(CJKCharacterCategory::HangulSyllables)
} else if Self::is_korean_character(c) {
Some(CJKCharacterCategory::HangulJamo)
} else {
None
}
}
}
pub struct ChineseDictionary;
impl ChineseDictionary {
pub fn new() -> Self {
Self
}
}
pub struct JapaneseDictionary;
impl JapaneseDictionary {
pub fn new() -> Self {
Self
}
}
pub struct KoreanDictionary;
impl KoreanDictionary {
pub fn new() -> Self {
Self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cjk_character_detection() {
assert!(CJKProcessor::is_chinese_character('中'));
assert!(CJKProcessor::is_hiragana('ひ'));
assert!(CJKProcessor::is_katakana('カ'));
assert!(CJKProcessor::is_korean_character('한'));
assert!(CJKProcessor::is_cjk_character('中'));
assert!(CJKProcessor::is_cjk_character('ひ'));
assert!(CJKProcessor::is_cjk_character('한'));
}
#[test]
fn test_language_detection() {
let processor = CJKProcessor::new();
let scores = processor.detect_cjk_language("中文");
assert!(scores
.iter()
.any(|(lang, _)| *lang == CJKLanguage::ChineseSimplified));
let scores = processor.detect_cjk_language("ひらがな");
assert!(scores
.iter()
.any(|(lang, _)| *lang == CJKLanguage::Japanese));
let scores = processor.detect_cjk_language("한글");
assert!(scores.iter().any(|(lang, _)| *lang == CJKLanguage::Korean));
}
#[test]
fn test_text_segmentation() {
let processor = CJKProcessor::new();
let result = processor
.segment_text("中文测试", CJKLanguage::ChineseSimplified)
.unwrap();
assert_eq!(result.language, CJKLanguage::ChineseSimplified);
assert!(!result.segments.is_empty());
let result = processor
.segment_text("ひらがなカタカナ", CJKLanguage::Japanese)
.unwrap();
assert_eq!(result.language, CJKLanguage::Japanese);
assert!(!result.segments.is_empty());
let result = processor
.segment_text("한글테스트", CJKLanguage::Korean)
.unwrap();
assert_eq!(result.language, CJKLanguage::Korean);
assert!(!result.segments.is_empty());
}
}