use crate::segmentation::tokenize_viterbi;
use crate::types::{Finding, PosTag, TokenSpan};
use crate::unicode_utils::normalize_nfc;
pub const CRIPPLED_TOPIC_RULE_ID: &str = "CRIPPLED_PREPOSITIONAL_TOPIC";
pub const PREP_CONJ_RULE_ID: &str = "PREPOSITION_CONJUNCTION_CONFLICT";
const GOVERNING_TRANSITIVE_VERBS: &[&str] = &[
"đòi hỏi",
"yêu cầu",
"cho thấy",
"chứng minh",
"khẳng định",
"chứng tỏ",
"buộc",
"khiến",
"giúp",
"làm cho",
"phát hiện",
"chỉ ra",
"nêu rõ",
"nhấn mạnh",
"minh chứng",
"đã chứng minh",
"đã giúp",
"đã buộc",
"đã phát hiện",
"đã chỉ ra",
"đã đòi hỏi",
];
pub fn parse_topic_comment(tokens: &[TokenSpan], norm_text: &str) -> Vec<Finding> {
if tokens.is_empty() {
return Vec::new();
}
let mut findings = Vec::new();
let mut sentences: Vec<&[TokenSpan]> = Vec::new();
let mut sent_start = 0;
for (i, tok) in tokens.iter().enumerate() {
let is_punct_term = tok.pos == Some(PosTag::Punct)
&& (tok.text == "."
|| tok.text == "?"
|| tok.text == "!"
|| tok.text == "..."
|| tok.text == "…");
let is_newline_boundary = if i + 1 < tokens.len() {
let next_tok = &tokens[i + 1];
if next_tok.start > tok.end {
norm_text
.chars()
.skip(tok.end)
.take(next_tok.start - tok.end)
.any(|c| c == '\n' || c == '\r')
} else {
false
}
} else {
false
};
if is_punct_term || is_newline_boundary {
if i >= sent_start {
sentences.push(&tokens[sent_start..=i]);
}
sent_start = i + 1;
}
}
if sent_start < tokens.len() {
sentences.push(&tokens[sent_start..]);
}
for sent in sentences {
let lead_idx = sent.iter().position(|t| {
t.pos != Some(PosTag::Punct)
&& !t
.text
.chars()
.all(|c| c.is_ascii_punctuation() || "“„”«»'\"()[]".contains(c))
});
let lead_idx = match lead_idx {
Some(idx) => idx,
None => continue,
};
let first_token = &sent[lead_idx];
let first_lower = first_token.text.to_lowercase();
let is_prep_opener = matches!(
first_lower.as_str(),
"trong" | "qua" | "bằng" | "với" | "với những" | "dưới" | "theo" | "ở" | "tại" | "từ"
);
if !is_prep_opener {
continue;
}
let mut inter_count = 0;
let mut prev_is_aspect = false;
for tok in &sent[lead_idx + 1..] {
let lower = tok.text.to_lowercase();
if tok.text == "," || tok.text == ";" || tok.text == "—" {
break;
}
if lower == "thì" || lower == "mà" {
break;
}
if (lower == "nhưng" || lower == "song") && inter_count >= 1 {
let start_char = first_token.start;
let end_char = tok.end;
let matched_span: String = norm_text
.chars()
.skip(start_char)
.take(end_char - start_char)
.collect();
findings.push(
Finding::new(
PREP_CONJ_RULE_ID,
"topic_comment",
"major",
start_char,
end_char,
0.95,
"Xung đột cú pháp: Giới từ mở đầu trạng ngữ không thể kết hợp trực tiếp với liên từ phản đề 'nhưng'.",
"linguistic",
matched_span,
)
.with_suggestion("Tách thành hai câu độc lập hoặc dùng cặp liên từ tương ứng 'Mặc dù... nhưng...' thay vì dùng giới từ 'Với/Bằng'"),
);
break;
}
let is_gov = GOVERNING_TRANSITIVE_VERBS
.iter()
.any(|&verb| lower == verb);
if (is_gov || (prev_is_aspect && is_gov)) && inter_count >= 1 {
let start_char = first_token.start;
let end_char = tok.end;
let matched_span: String = norm_text
.chars()
.skip(start_char)
.take(end_char - start_char)
.collect();
findings.push(
Finding::new(
CRIPPLED_TOPIC_RULE_ID,
"topic_comment",
"major",
start_char,
end_char,
0.95,
"Lẫn lộn Khung đề và Chủ đề: Giới từ biến cụm từ thành trạng ngữ/phương tiện, không thể làm chủ thể cho vị từ.",
"linguistic",
matched_span,
)
.with_suggestion("Bỏ giới từ mở đầu để biến Khung đề thành Chủ ngữ tự nhiên, hoặc bổ sung chủ ngữ sau dấu phẩy"),
);
break;
}
if lower == "đã" || lower == "đang" || lower == "sẽ" {
prev_is_aspect = true;
} else {
prev_is_aspect = false;
inter_count += 1;
}
}
}
findings
}
pub fn evaluate_crippled_prepositional_topic(text: &str) -> Vec<Finding> {
let norm = normalize_nfc(text);
let tokens = tokenize_viterbi(&norm);
parse_topic_comment(&tokens, &norm)
.into_iter()
.filter(|f| f.pattern_id == CRIPPLED_TOPIC_RULE_ID)
.collect()
}
pub fn evaluate_preposition_conjunction_conflict(text: &str) -> Vec<Finding> {
let norm = normalize_nfc(text);
let tokens = tokenize_viterbi(&norm);
parse_topic_comment(&tokens, &norm)
.into_iter()
.filter(|f| f.pattern_id == PREP_CONJ_RULE_ID)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crippled_topic_detection() {
let text = "Trong tình hình kinh tế hiện nay đòi hỏi doanh nghiệp phải tái cơ cấu toàn diện.";
let findings = evaluate_crippled_prepositional_topic(text);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].pattern_id, CRIPPLED_TOPIC_RULE_ID);
assert_eq!(
findings[0].matched_span,
"Trong tình hình kinh tế hiện nay đòi hỏi"
);
}
#[test]
fn test_prep_conj_conflict_detection() {
let text = "Với những nỗ lực phi thường nhưng họ vẫn không thể hoàn thành dự án đúng hạn.";
let findings = evaluate_preposition_conjunction_conflict(text);
assert_eq!(findings.len(), 1);
assert_eq!(findings[0].pattern_id, PREP_CONJ_RULE_ID);
assert_eq!(
findings[0].matched_span,
"Với những nỗ lực phi thường nhưng"
);
}
#[test]
fn test_clean_topic_sentence() {
let text = "Tình hình kinh tế hiện nay đòi hỏi doanh nghiệp phải tái cơ cấu.";
let findings = evaluate_crippled_prepositional_topic(text);
assert!(findings.is_empty());
}
#[test]
fn test_clean_with_comma_boundary() {
let text = "Trong tình hình kinh tế hiện nay, các doanh nghiệp đang nỗ lực tái cơ cấu.";
let findings = evaluate_crippled_prepositional_topic(text);
assert!(findings.is_empty());
}
#[test]
fn test_clean_with_topic_marker_thi() {
let text = "Trong hoàn cảnh khó khăn này thì mọi người cần đoàn kết gắn bó.";
let findings = evaluate_crippled_prepositional_topic(text);
assert!(findings.is_empty());
}
#[test]
fn test_clean_existential_sentence() {
let text = "Trong nhà có năm người đang ngồi thảo luận kế hoạch.";
let findings = evaluate_crippled_prepositional_topic(text);
assert!(findings.is_empty());
}
#[test]
fn test_multi_sentence_detection() {
let text = "Hôm nay thời tiết rất đẹp. Qua phân tích chuỗi cung ứng linh kiện khiến các nhà máy phải điều chỉnh ca làm.";
let findings = evaluate_crippled_prepositional_topic(text);
assert_eq!(findings.len(), 1);
assert_eq!(
findings[0].matched_span,
"Qua phân tích chuỗi cung ứng linh kiện khiến"
);
}
#[test]
fn test_aspect_prefixed_verb() {
let text = "Bằng những số liệu thực nghiệm đã chứng minh giả thuyết của nhóm là chính xác.";
let findings = evaluate_crippled_prepositional_topic(text);
assert_eq!(findings.len(), 1);
assert_eq!(
findings[0].matched_span,
"Bằng những số liệu thực nghiệm đã chứng minh"
);
}
}