hangul_cd/
word.rs

1use std::fmt::Debug;
2
3use thiserror::Error;
4
5use crate::{block::*, jamo::*};
6
7/// A composer for a single Hangul word, made up of multiple syllable blocks.
8#[derive(Error, Debug, PartialEq, Eq)]
9pub enum WordError {
10    /// Occurs when there is an error related to syllable blocks.
11    #[error("Block error: {0}")]
12    BlockError(#[from] BlockError),
13
14    /// Occurs when there is an error related to Jamo letters.
15    #[error("Jamo error: {0}")]
16    JamoError(#[from] JamoError),
17
18    /// Tried to start a new block while pushing Jamo, but it was not possible.
19    /// The reason is provided in the `BlockPushResult`.
20    #[error("Could not start new block with character '{0}'; reason: {1:?}")]
21    CouldNotStartNewBlock(char, BlockPushResult),
22
23    /// Tried popping from an empty word (no Jamo to pop).
24    #[error("Tried popping from empty word")]
25    NothingToPop,
26
27    /// Tried to complete the current block, but it only contains one Jamo.
28    #[error("Cannot complete current block; currently contains only one Jamo: {0:?}")]
29    CannotCompleteCurrentBlock(Jamo),
30}
31
32/// A composer for a single Hangul word, made up of multiple syllable blocks.
33/// The `HangulWordComposer` maintains a list of completed `HangulBlock`s and a
34/// `BlockComposer` for the current syllable block being composed.
35///
36/// **API:**
37/// ```rust
38/// use hangul::word::{HangulWordComposer, WordPushResult};
39/// use hangul::jamo::{Jamo, JamoConsonantSingular, JamoVowelSingular};
40///
41/// let mut composer = HangulWordComposer::new();
42///
43/// // Push characters to form Hangul syllables
44/// assert_eq!(composer.push_char('ㅇ').unwrap(), WordPushResult::Continue);
45/// assert_eq!(composer.push_char('ㅏ').unwrap(), WordPushResult::Continue);
46/// assert_eq!(composer.push_char('ㄴ').unwrap(), WordPushResult::Continue);
47/// assert_eq!(composer.push_char('ㄴ').unwrap(), WordPushResult::Continue);
48/// assert_eq!(composer.push_char('ㅕ').unwrap(), WordPushResult::Continue);
49/// assert_eq!(composer.push_char('ㅇ').unwrap(), WordPushResult::Continue);
50///
51/// // Get the composed string
52/// let result = composer.as_string().unwrap();
53/// assert_eq!(result, "안녕".to_string());
54///
55/// // Popping characters removes jamo in reverse order
56/// assert_eq!(
57///     composer.pop().unwrap(),
58///     Some(Jamo::Consonant(JamoConsonantSingular::Ieung))
59/// );
60/// assert_eq!(
61///     composer.pop().unwrap(),
62///     Some(Jamo::Vowel(JamoVowelSingular::Yeo))
63/// );
64/// assert_eq!(
65///     composer.pop().unwrap(),
66///     Some(Jamo::Consonant(JamoConsonantSingular::Nieun))
67/// );
68/// assert_eq!(composer.as_string().unwrap(), "안".to_string());
69/// ```
70#[derive(Debug)]
71pub struct HangulWordComposer {
72    prev_blocks: Vec<HangulBlock>,
73    cur_block: BlockComposer,
74}
75
76/// The result of attempting to push a character into the `HangulWordComposer`.
77#[derive(Debug, PartialEq, Eq)]
78pub enum WordPushResult {
79    /// The character was successfully pushed and composition can continue.
80    Continue,
81
82    /// The character could not be pushed because it would result in an invalid
83    /// Hangul syllable.
84    InvalidHangul,
85
86    /// The character was not pushed because it is not a Hangul character.
87    NonHangul,
88}
89
90impl HangulWordComposer {
91    /// Creates a new, empty `HangulWordComposer`.
92    pub fn new() -> Self {
93        HangulWordComposer {
94            prev_blocks: Vec::new(),
95            cur_block: BlockComposer::new(),
96        }
97    }
98
99    /// Pushes a character into the `HangulWordComposer` if valid and returns a
100    /// result indicating the outcome.
101    ///
102    /// If pushing would make a valid Hangul syllable, the new character is
103    /// appended and `WordPushResult::Continue` is returned.
104    ///
105    /// If pushing the character would result in an invalid Hangul syllable,
106    /// but the character is Hangul and can start a new syllable block, the current
107    /// block is completed, a new block is started with the pushed character,
108    /// and `WordPushResult::Continue` is returned.
109    ///
110    /// If the character is Hangul but cannot form a valid syllable in either
111    /// the current or a new block, `WordPushResult::InvalidHangul` is returned.
112    ///
113    /// If the character is not Hangul, `WordPushResult::NonHangul` is returned.
114    pub fn push_char(&mut self, c: char) -> Result<WordPushResult, WordError> {
115        match Character::from_char(c)? {
116            Character::Hangul(jamo) => self.push(&jamo),
117            Character::NonHangul(_) => Ok(WordPushResult::NonHangul),
118        }
119    }
120
121    /// Pushes a Jamo letter into the `HangulWordComposer`. Acts the same as
122    /// `push_char`, but takes a `Jamo` instead of a `char`.
123    /// Pushing appends to the current syllable block if that would make a
124    /// valid Hangul syllable; otherwise, it completes the current block and
125    /// creates a new block with the pushed character.
126    pub fn push(&mut self, letter: &Jamo) -> Result<WordPushResult, WordError> {
127        match self.cur_block.push(letter) {
128            BlockPushResult::Success => Ok(WordPushResult::Continue),
129            BlockPushResult::InvalidHangul => Ok(WordPushResult::InvalidHangul),
130            BlockPushResult::NonHangul => Ok(WordPushResult::NonHangul),
131            BlockPushResult::StartNewBlockNoPop => match self.start_new_block(letter.clone()) {
132                Ok(_) => Ok(WordPushResult::Continue),
133                Err(e) => Err(e),
134            },
135            BlockPushResult::PopAndStartNewBlock => {
136                match self.pop_and_start_new_block(letter.clone()) {
137                    Ok(_) => Ok(WordPushResult::Continue),
138                    Err(e) => Err(e),
139                }
140            }
141        }
142    }
143
144    /// Pops the last Jamo letter from the `HangulWordComposer`.
145    /// If the current syllable block has letters, it will remove the last
146    /// letter from it. If the current block is empty, it will set the last
147    /// completed block as the currently-active block and remove one Jamo
148    /// from it if possible.
149    ///
150    /// Returns `Ok(Some(Jamo))` if a letter was successfully removed,
151    /// `Ok(None)` if there are no letters to remove, or `Err(String)` if an
152    /// error occurred during the operation.
153    pub fn pop(&mut self) -> Result<Option<Jamo>, WordError> {
154        match self.cur_block.pop() {
155            BlockPopStatus::PoppedAndNonEmpty(l) => Ok(Some(l)),
156            BlockPopStatus::PoppedAndEmpty(l) => {
157                self.prev_block_to_cur()?;
158                Ok(Some(l))
159            }
160            BlockPopStatus::None => {
161                self.prev_block_to_cur()?;
162                Ok(None)
163            }
164        }
165    }
166
167    fn prev_block_to_cur(&mut self) -> Result<(), WordError> {
168        if let Some(last_block) = self.prev_blocks.pop() {
169            self.cur_block = BlockComposer::from_composed_block(&last_block)?;
170            Ok(())
171        } else {
172            Ok(())
173        }
174    }
175
176    fn pop_and_start_new_block(&mut self, letter: Jamo) -> Result<(), WordError> {
177        match self.cur_block.pop_end_consonant() {
178            Some(l) => {
179                self.complete_current_block()?;
180                self.cur_block.push(&l);
181                match self.cur_block.push(&letter) {
182                    BlockPushResult::Success => Ok(()),
183                    other => Err(WordError::CouldNotStartNewBlock(
184                        letter.char_compatibility(),
185                        other,
186                    )),
187                }
188            }
189            None => Err(WordError::NothingToPop),
190        }
191    }
192
193    fn start_new_block(&mut self, letter: Jamo) -> Result<(), WordError> {
194        self.complete_current_block()?;
195        match self.cur_block.push(&letter) {
196            BlockPushResult::Success => Ok(()),
197            other => Err(WordError::CouldNotStartNewBlock(
198                letter.char_compatibility(),
199                other,
200            )),
201        }
202    }
203
204    /// Returns the composed string for the current Hangul word.
205    /// This includes all completed syllable blocks and the current block,
206    /// even if it is incomplete.
207    pub fn as_string(&self) -> Result<String, WordError> {
208        let mut result = hangul_blocks_vec_to_string(&self.prev_blocks)?;
209        let cur_as_char = self.cur_block.block_as_string()?;
210        if let Some(c) = cur_as_char {
211            result.push(c);
212        }
213        Ok(result)
214    }
215
216    fn complete_current_block(&mut self) -> Result<(), WordError> {
217        match self.cur_block.try_as_complete_block()? {
218            BlockCompletionStatus::Complete(block) => {
219                self.prev_blocks.push(block);
220                self.cur_block = BlockComposer::new();
221                Ok(())
222            }
223            BlockCompletionStatus::Incomplete(c) => Err(WordError::CannotCompleteCurrentBlock(c)),
224            BlockCompletionStatus::Empty => {
225                // Nothing to complete
226                Ok(())
227            }
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn start_new_block_valid() {
238        let mut composer = HangulWordComposer::new();
239
240        assert_eq!(composer.push_char('ㄱ'), Ok(WordPushResult::Continue));
241        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
242        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue),);
243        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue),);
244        assert_eq!(
245            composer.prev_blocks,
246            vec![HangulBlock {
247                initial: Jamo::Consonant(JamoConsonantSingular::Giyeok),
248                vowel: Jamo::Vowel(JamoVowelSingular::A),
249                final_optional: Some(Jamo::Consonant(JamoConsonantSingular::Nieun)),
250            }]
251        );
252        assert_eq!(composer.push_char('ㅛ'), Ok(WordPushResult::Continue));
253        assert_eq!(composer.push_char('ㅉ'), Ok(WordPushResult::Continue),);
254        assert_eq!(
255            composer.prev_blocks,
256            vec![
257                HangulBlock {
258                    initial: Jamo::Consonant(JamoConsonantSingular::Giyeok),
259                    vowel: Jamo::Vowel(JamoVowelSingular::A),
260                    final_optional: Some(Jamo::Consonant(JamoConsonantSingular::Nieun)),
261                },
262                HangulBlock {
263                    initial: Jamo::Consonant(JamoConsonantSingular::Ieung),
264                    vowel: Jamo::Vowel(JamoVowelSingular::Yo),
265                    final_optional: None,
266                }
267            ]
268        );
269    }
270
271    #[test]
272    fn start_new_block_invalid() {
273        let mut composer = HangulWordComposer::new();
274
275        assert_eq!(
276            composer.start_new_block(Jamo::Vowel(JamoVowelSingular::A)),
277            Err(WordError::CouldNotStartNewBlock(
278                'ㅏ',
279                BlockPushResult::InvalidHangul
280            ))
281        );
282        let _ = composer.push_char('ㄱ');
283        assert_eq!(
284            composer.start_new_block(Jamo::CompositeVowel(JamoVowelComposite::Wae)),
285            Err(WordError::CannotCompleteCurrentBlock(Jamo::Consonant(
286                JamoConsonantSingular::Giyeok
287            )))
288        );
289    }
290
291    #[test]
292    fn push_char_valid() {
293        let mut composer = HangulWordComposer::new();
294
295        assert_eq!(composer.push_char('ㄱ'), Ok(WordPushResult::Continue));
296        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
297        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue),);
298    }
299
300    #[test]
301    fn push_char_invalid_hangul() {
302        let mut composer = HangulWordComposer::new();
303
304        assert_eq!(composer.push_char('ㄱ'), Ok(WordPushResult::Continue));
305        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
306        assert_eq!(composer.push_char('ㄹ'), Ok(WordPushResult::Continue));
307        assert_eq!(composer.push_char('ㄽ'), Ok(WordPushResult::InvalidHangul));
308    }
309
310    #[test]
311    fn push_char_next_block() {
312        let mut composer = HangulWordComposer::new();
313
314        assert_eq!(composer.push_char('ㄱ'), Ok(WordPushResult::Continue));
315        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
316        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
317        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
318    }
319
320    #[test]
321    fn push_char_non_hangul() {
322        let mut composer = HangulWordComposer::new();
323
324        assert_eq!(composer.push_char('ㄱ'), Ok(WordPushResult::Continue));
325        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
326        assert_eq!(composer.push_char('A'), Ok(WordPushResult::NonHangul));
327    }
328
329    #[test]
330    fn test_single_word_안녕하세요_as_string() {
331        let mut composer = HangulWordComposer::new();
332
333        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
334        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
335        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
336        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
337        assert_eq!(composer.push_char('ㅕ'), Ok(WordPushResult::Continue));
338        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
339        assert_eq!(composer.push_char('ㅎ'), Ok(WordPushResult::Continue));
340        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
341        assert_eq!(composer.push_char('ㅅ'), Ok(WordPushResult::Continue));
342        assert_eq!(composer.push_char('ㅔ'), Ok(WordPushResult::Continue));
343        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
344        assert_eq!(composer.push_char('ㅛ'), Ok(WordPushResult::Continue));
345
346        let result_string = composer.as_string().unwrap();
347        assert_eq!(result_string, "안녕하세요".to_string());
348    }
349
350    #[test]
351    fn test_single_word_앖어요_as_string() {
352        let mut composer = HangulWordComposer::new();
353
354        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
355        assert_eq!(composer.push_char('ㅓ'), Ok(WordPushResult::Continue));
356        assert_eq!(composer.push_char('ㅂ'), Ok(WordPushResult::Continue));
357        assert_eq!(composer.push_char('ㅅ'), Ok(WordPushResult::Continue));
358        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
359        assert_eq!(composer.push_char('ㅓ'), Ok(WordPushResult::Continue));
360        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
361        assert_eq!(composer.push_char('ㅛ'), Ok(WordPushResult::Continue));
362
363        let result_string = composer.as_string().unwrap();
364        assert_eq!(result_string, "없어요".to_string());
365    }
366
367    #[test]
368    fn test_incomplete_block_as_string() {
369        let mut composer = HangulWordComposer::new();
370
371        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
372
373        let result_string = composer.as_string().unwrap();
374        assert_eq!(result_string, "ᄋ".to_string());
375    }
376
377    #[test]
378    fn test_deletions() {
379        let mut composer = HangulWordComposer::new();
380        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
381        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
382        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
383        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
384        assert_eq!(composer.push_char('ㅕ'), Ok(WordPushResult::Continue));
385        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㅕ');
386        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㄴ');
387        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㄴ');
388        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㅏ');
389        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㅇ');
390        assert_eq!(composer.pop(), Ok(None));
391    }
392
393    #[test]
394    fn test_deletion_then_write_again() {
395        let mut composer = HangulWordComposer::new();
396        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
397        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
398        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
399
400        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㄴ');
401        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㅏ');
402        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㅇ');
403
404        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
405        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
406        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
407
408        let result_string = composer.as_string().unwrap();
409        assert_eq!(result_string, "안".to_string());
410    }
411
412    #[test]
413    fn deletion_removes_empty_block() {
414        let mut composer = HangulWordComposer::new();
415        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
416        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
417        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
418        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
419
420        assert_eq!(composer.pop().unwrap().unwrap().char_compatibility(), 'ㄴ');
421        // if current block is still empty, as_string should fail
422        assert_eq!(composer.as_string().unwrap(), "안".to_string());
423    }
424
425    #[test]
426    fn test_complete_current_block() {
427        let mut composer = HangulWordComposer::new();
428        assert_eq!(composer.push_char('ㅇ'), Ok(WordPushResult::Continue));
429        assert_eq!(composer.push_char('ㅏ'), Ok(WordPushResult::Continue));
430        assert_eq!(composer.push_char('ㄴ'), Ok(WordPushResult::Continue));
431
432        assert!(composer.complete_current_block().is_ok());
433
434        assert_eq!(composer.prev_blocks.len(), 1);
435        assert_eq!(composer.cur_block, BlockComposer::new());
436
437        let result_string = composer.as_string().unwrap();
438        assert_eq!(result_string, "안".to_string());
439    }
440}