Skip to main content

hangul_cd/
string.rs

1use thiserror::Error;
2
3use crate::{
4    jamo::{Jamo, JamoPosition},
5    word::*,
6};
7
8/// An error type for `StringComposer` operations.
9#[derive(Error, Debug, PartialEq, Eq)]
10pub enum StringError {
11    /// Occurs when there is an error related to word composition.
12    #[error("Word error: {0}")]
13    WordError(#[from] WordError),
14}
15
16/// A composer struct that manages the composition of strings of text
17/// consisting of multiple words, including both Hangul words and non-Hangul
18/// text.
19///
20/// The `StringComposer` maintains both a string of completed text and a
21/// `HangulWordComposer` for the current word being composed. If the currently
22/// active word is a Hangul word that has not yet been completed, pushing or
23/// popping characters will interact with the `HangulWordComposer` and directly
24/// update syllable blocks. Otherwise, Unicode characters will be directly
25/// added to or removed from the completed string.
26///
27/// **API:**
28/// ```rust
29/// use hangul_cd::string::StringComposer;
30///
31/// let mut composer = StringComposer::new();
32///
33/// // Push characters to form Hangul syllables
34/// composer.push_char('ㅎ').unwrap();
35/// composer.push_char('ㅏ').unwrap();
36///
37/// // Get the composed string
38/// let result = composer.as_string().unwrap();
39/// assert_eq!(result, "하".to_string());
40///
41/// // Push non-Hangul characters
42/// composer.push_char(' ').unwrap();
43/// composer.push_char('!').unwrap();
44/// assert_eq!(composer.as_string().unwrap(), "하 !".to_string());
45///
46/// // Popping non-Hangul characters removes them from the completed string
47/// composer.pop().unwrap(); // removes '!'
48/// composer.pop().unwrap(); // removes ' '
49/// assert_eq!(composer.as_string().unwrap(), "하".to_string());
50///
51/// // Popping Hangul characters after they've been completed removes entire syllables
52/// composer.pop().unwrap(); // removes '하'
53/// assert_eq!(composer.as_string().unwrap(), "".to_string());
54///
55/// // Popping characters while a Hangul word is active removes jamo
56/// composer.push_char('ㅂ').unwrap();
57/// composer.push_char('ㅏ').unwrap();
58/// composer.push_char('ㅂ').unwrap();
59/// composer.pop().unwrap(); // removes 'ㅂ'
60/// assert_eq!(composer.as_string().unwrap(), "바".to_string());
61/// ```
62#[derive(Debug)]
63pub struct StringComposer {
64    completed: String,
65    current: HangulWordComposer,
66}
67
68impl Default for StringComposer {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74impl StringComposer {
75    /// Creates a new, empty `StringComposer`.
76    pub fn new() -> Self {
77        Self {
78            completed: String::new(),
79            current: HangulWordComposer::new(),
80        }
81    }
82
83    /// Pushes a character to the `StringComposer`.
84    ///
85    /// If the character is part of a Hangul word, it will be composed into syllables.
86    /// Otherwise, it will be added directly to the completed string.
87    pub fn push_char(&mut self, c: char) -> Result<(), StringError> {
88        match self.current.push_char(c)? {
89            WordPushResult::Continue => Ok(()),
90            _ => self.handle_invalid_input(c),
91        }
92    }
93
94    /// Returns the composed string, combining completed text and the current word.
95    pub fn as_string(&self) -> Result<String, StringError> {
96        let mut result = self.completed.clone();
97        let current_string = self.current.as_string()?;
98        result.push_str(&current_string);
99        Ok(result)
100    }
101
102    /// Pops the last character from the `StringComposer` and returns it wrapped
103    /// within a `Result` and `Option`.
104    ///
105    /// If the current word is a Hangul word with uncompleted syllables, it will
106    /// remove the last jamo from the current syllable block. Otherwise, it will
107    /// remove the last character from the completed string.
108    pub fn pop(&mut self) -> Result<Option<char>, StringError> {
109        match self.current.pop()? {
110            Some(c) => Ok(c.char_modern(match c {
111                Jamo::Consonant(_) | Jamo::CompositeConsonant(_) => JamoPosition::Initial,
112                Jamo::Vowel(_) | Jamo::CompositeVowel(_) => JamoPosition::Vowel,
113            })),
114            None => match self.completed.pop() {
115                Some(c) => Ok(Some(c)),
116                None => Ok(None),
117            },
118        }
119    }
120
121    fn handle_invalid_input(&mut self, c: char) -> Result<(), StringError> {
122        let current_string = self.current.as_string()?;
123        self.completed.push_str(&current_string);
124        self.completed.push(c);
125        self.current = HangulWordComposer::new();
126        Ok(())
127    }
128}
129
130#[cfg(test)]
131mod test {
132    use super::*;
133
134    #[test]
135    fn test_no_new_words() {
136        let input = "ㅎㅏㄴㄱㅡㄹ";
137        let mut composer = StringComposer::new();
138        for c in input.chars() {
139            composer.push_char(c).unwrap();
140        }
141        let result = composer.as_string().unwrap();
142        assert_eq!(result, "한글".to_string());
143    }
144
145    #[test]
146    fn test_new_hangul_word() {
147        let input = "ㅎㅏㄴㄱㅡㄹ ㅇㅏㄴㄴㅕㅇㅎㅏㅅㅔㅇㅛ";
148        let mut composer = StringComposer::new();
149        for c in input.chars() {
150            composer.push_char(c).unwrap();
151        }
152        let result = composer.as_string().unwrap();
153        assert_eq!(result, "한글 안녕하세요".to_string());
154    }
155
156    #[test]
157    fn test_new_non_hangul_word() {
158        let input = "ㅎㅏㄴㄱㅡㄹ beans";
159        let mut composer = StringComposer::new();
160        for c in input.chars() {
161            composer.push_char(c).unwrap();
162        }
163        let result = composer.as_string().unwrap();
164        assert_eq!(result, "한글 beans".to_string());
165    }
166
167    #[test]
168    fn test_multiple_words() {
169        let input = "ㅎㅏㄴㄱㅡㄹ 123  \n ㅇㅏㄴㄴㅕㅇ!";
170        let mut composer = StringComposer::new();
171        for c in input.chars() {
172            composer.push_char(c).unwrap();
173        }
174        let result = composer.as_string().unwrap();
175        assert_eq!(result, "한글 123  \n 안녕!".to_string());
176    }
177
178    #[test]
179    fn test_backspace() {
180        let input = "ㅇㅏㄴㄴㅕㅇ ㄹㅏㅁㅕㄴ";
181        let mut composer = StringComposer::new();
182        for c in input.chars() {
183            composer.push_char(c).unwrap();
184        }
185        for _ in 0..7 {
186            composer.pop().unwrap();
187        }
188        let result = composer.as_string().unwrap();
189        assert_eq!(result, "안".to_string());
190    }
191}