Skip to main content

vwi_core/
unicode_utils.rs

1use unicode_normalization::UnicodeNormalization;
2
3/// Chuẩn hóa chuỗi văn bản sang Unicode NFC.
4pub fn normalize_nfc(text: &str) -> String {
5    text.nfc().collect()
6}
7
8/// Cấu trúc hỗ trợ chuyển đổi $O(1)$ giữa UTF-8 byte offset và Unicode char offset.
9pub struct CharOffsetMap {
10    byte_to_char: Vec<usize>,
11    total_chars: usize,
12}
13
14impl CharOffsetMap {
15    pub fn new(text: &str) -> Self {
16        let mut byte_to_char = Vec::with_capacity(text.len() + 1);
17        let mut char_count: usize = 0;
18
19        for (byte_idx, _) in text.char_indices() {
20            while byte_to_char.len() < byte_idx {
21                byte_to_char.push(char_count.saturating_sub(1));
22            }
23            byte_to_char.push(char_count);
24            char_count += 1;
25        }
26
27        while byte_to_char.len() <= text.len() {
28            byte_to_char.push(char_count);
29        }
30
31        Self {
32            byte_to_char,
33            total_chars: char_count,
34        }
35    }
36
37    /// Trả về char offset tương ứng với byte offset.
38    #[inline]
39    pub fn byte_to_char(&self, byte_offset: usize) -> usize {
40        if byte_offset >= self.byte_to_char.len() {
41            self.total_chars
42        } else {
43            self.byte_to_char[byte_offset]
44        }
45    }
46
47    /// Chuyển đổi tuple byte range `(start_byte, end_byte)` sang char range `(start_char, end_char)`.
48    #[inline]
49    pub fn convert_range(&self, start_byte: usize, end_byte: usize) -> (usize, usize) {
50        (self.byte_to_char(start_byte), self.byte_to_char(end_byte))
51    }
52
53    /// Alias cho convert_range để đồng nhất phong cách gọi.
54    #[inline]
55    pub fn byte_range_to_char_range(&self, start_byte: usize, end_byte: usize) -> (usize, usize) {
56        self.convert_range(start_byte, end_byte)
57    }
58}
59
60/// Kiểm tra xem một span [start_byte..end_byte] trong text có nằm ở ranh giới từ (word boundary) hay không.
61#[inline]
62pub fn is_word_boundary(text: &str, start_byte: usize, end_byte: usize) -> bool {
63    let bytes = text.as_bytes();
64    if start_byte > 0 {
65        if let Some(prev_char) = text[..start_byte].chars().next_back() {
66            if prev_char.is_alphanumeric() || prev_char == '_' || prev_char == '-' {
67                return false;
68            }
69        }
70    }
71    if end_byte < bytes.len() {
72        if let Some(next_char) = text[end_byte..].chars().next() {
73            if next_char.is_alphanumeric() || next_char == '_' || next_char == '-' {
74                return false;
75            }
76        }
77    }
78    true
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_char_offset_mapping_vietnamese() {
87        let text = "Trí tuệ nhân tạo";
88        let mapper = CharOffsetMap::new(text);
89
90        // "Trí " -> 'í' là 2 bytes trong UTF-8
91        // Byte: T(0), r(1), í(2,3), ' '(4) -> char 3 (khoảng trắng)
92        assert_eq!(mapper.byte_to_char(0), 0);
93        assert_eq!(mapper.byte_to_char(4), 3);
94        assert_eq!(mapper.byte_to_char(5), 4); // 't'
95
96        // Ký tự cuối cùng:
97        assert_eq!(mapper.byte_to_char(text.len()), 16);
98    }
99}