hangul_cd/
block.rs

1use thiserror::Error;
2
3use crate::jamo::*;
4use std::fmt::Debug;
5
6/// Errors that can occur when working with Hangul syllable blocks.
7#[derive(Error, Debug, PartialEq, Eq)]
8pub enum BlockError {
9    /// An error related to Jamo operations.
10    #[error("Jamo error: {0:?}")]
11    JamoError(#[from] JamoError),
12
13    /// Occurs when a codepoint to represent a Hangul syllable block is invalid.
14    /// This happens when converting a block to a codepoint, or when trying
15    /// to create a block from a codepoint that does not correspond to a valid
16    /// Hangul syllable.
17    #[error("Could not convert unknown codepoint U+{0:04X} to Hangul syllable block")]
18    InvalidBlockRepresentation(u32),
19
20    /// Occurs when a Jamo letter is not valid in the context of Hangul
21    /// syllable composition, given its position and the Unicode era (modern
22    /// or compatibility) being used.
23    #[error("Jamo '{0:?}' is not valid in position '{1:?}' for Unicode era '{2:?}'")]
24    InvalidJamoContext(Jamo, JamoPosition, JamoUnicodeType),
25
26    /// Occurs when a codepoint meant to represent a Jamo letter is invalid
27    /// and cannot be used in Hangul syllable composition or decomposition.
28    #[error("Could not convert codepoint U+{0:04X} to valid Unicode character")]
29    InvalidComponentRepresentation(u32),
30
31    /// Occurs when a Jamo letter is in an invalid position for Hangul;
32    /// for example, a vowel in the initial position.
33    #[error("Jamo '{0:?}' is in invalid position; expected '{1:?}'")]
34    JamoInInvalidPosition(Jamo, JamoPosition),
35}
36
37/// A struct representing a composed Hangul syllable block,
38/// consisting of an initial Jamo, a vowel Jamo,
39/// and an optional final Jamo.
40///
41/// **API:**
42/// ```rust
43/// use hangul::block::{HangulBlock, HangulBlockDecompositionOptions};
44/// use hangul::jamo::{
45///     Jamo,
46///     JamoConsonantSingular,
47///     JamoVowelSingular,
48///     JamoUnicodeType,
49/// };
50///
51/// // `HangulBlock`s can be directly constructed; to ensure validity,
52/// // use a `BlockComposer` to build them instead. See `BlockComposer`
53/// // documentation for more details.
54/// let block = HangulBlock {
55///     initial: Jamo::from_compatibility_jamo('ㄱ').unwrap(),
56///     vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
57///     final_optional: None,
58/// };
59///
60/// // Convert the block to a Hangul syllable character
61/// let syllable = block.to_char().unwrap();
62/// assert_eq!(syllable, '가');
63///
64/// // Decompose the block into its constituent Jamo characters as a tuple
65/// assert_eq!(
66///     block.decomposed_tuple().unwrap(),
67///    (Some(Jamo::Consonant(JamoConsonantSingular::Giyeok)),
68///     None,
69///     Some(Jamo::Vowel(JamoVowelSingular::A)),
70///     None,
71///     None,
72///     None)
73/// );
74///
75/// // Decompose the block into its constituent Jamo characters as a vector
76/// let options = HangulBlockDecompositionOptions {
77///    decompose_composites: false,
78///    jamo_era: JamoUnicodeType::Modern,
79/// };
80/// let decomposed_vec = block.decomposed_vec(&options).unwrap();
81/// assert_eq!(decomposed_vec, vec!['ᄀ', 'ᅡ']);
82/// ```
83#[derive(Debug, PartialEq, Eq)]
84pub struct HangulBlock {
85    pub initial: Jamo,
86    pub vowel: Jamo,
87    pub final_optional: Option<Jamo>,
88}
89
90impl HangulBlock {
91    /// Converts the `HangulBlock` into a composed Hangul syllable unicode
92    /// character. Assumes all chars are valid Jamo.
93    pub fn to_char(&self) -> Result<char, BlockError> {
94        // Ensure the initial, vowel, and final are modern Jamo and not
95        // compatibility jamo
96        let initial = match self.initial.char_modern(JamoPosition::Initial) {
97            Some(c) => c,
98            None => {
99                return Err(BlockError::InvalidJamoContext(
100                    self.initial.clone(),
101                    JamoPosition::Initial,
102                    JamoUnicodeType::Modern,
103                ));
104            }
105        };
106        let vowel = match self.vowel.char_modern(JamoPosition::Vowel) {
107            Some(c) => c,
108            None => {
109                return Err(BlockError::InvalidJamoContext(
110                    self.vowel.clone(),
111                    JamoPosition::Vowel,
112                    JamoUnicodeType::Modern,
113                ));
114            }
115        };
116        let final_optional = match &self.final_optional {
117            Some(c) => c.char_modern(JamoPosition::Final),
118            None => None,
119        };
120
121        // Get u32 representation of chars
122        let initial_num = initial as u32;
123        let vowel_num = vowel as u32;
124        let final_num = match final_optional {
125            Some(c) => c as u32,
126            None => 0,
127        };
128
129        // Calculate indices
130        let l_index = initial_num - L_BASE;
131        let v_index = vowel_num - V_BASE;
132        let t_index = if final_num == 0 {
133            0
134        } else {
135            final_num - T_BASE
136        };
137        let s_index = (l_index * N_COUNT) + (v_index * T_COUNT) + t_index;
138
139        // Unwrapping because this should only ever be called with valid Hangul
140        if let Some(c) = std::char::from_u32(S_BASE + s_index) {
141            Ok(c)
142        } else {
143            Err(BlockError::InvalidBlockRepresentation(S_BASE + s_index))
144        }
145    }
146
147    /// Creates a `HangulBlock` from a composed Hangul syllable unicode character.
148    pub fn from_char(c: char) -> Result<Self, BlockError> {
149        let codepoint = c as u32;
150        if codepoint < S_BASE || codepoint > S_BASE + S_COUNT {
151            return Err(BlockError::InvalidBlockRepresentation(codepoint));
152        }
153
154        let s_index = codepoint - S_BASE;
155        let l_index = s_index / N_COUNT;
156        let v_index = (s_index % N_COUNT) / T_COUNT;
157        let t_index = s_index % T_COUNT;
158
159        let initial = Jamo::from_modern_jamo(
160            std::char::from_u32(L_BASE + l_index)
161                .ok_or(BlockError::InvalidComponentRepresentation(L_BASE + l_index))?,
162        )?;
163        let vowel = Jamo::from_modern_jamo(
164            std::char::from_u32(V_BASE + v_index)
165                .ok_or(BlockError::InvalidComponentRepresentation(V_BASE + v_index))?,
166        )?;
167        let final_optional = if t_index > 0 {
168            Some(Jamo::from_modern_jamo(
169                std::char::from_u32(T_BASE + t_index)
170                    .ok_or(BlockError::InvalidComponentRepresentation(T_BASE + t_index))?,
171            )?)
172        } else {
173            None
174        };
175
176        Ok(HangulBlock {
177            initial,
178            vowel,
179            final_optional,
180        })
181    }
182
183    /// Decomposes the `HangulBlock` into its constituent Jamo characters.
184    /// Returns a tuple containing six `Option<Jamo>` values representing
185    /// the decomposed characters:
186    /// - First initial consonant
187    /// - Second initial consonant (if composite)
188    /// - First vowel
189    /// - Second vowel (if composite)
190    /// - First final consonant (if any)
191    /// - Second final consonant (if composite)
192    pub fn decomposed_tuple(
193        &self,
194    ) -> Result<
195        (
196            Option<Jamo>,
197            Option<Jamo>,
198            Option<Jamo>,
199            Option<Jamo>,
200            Option<Jamo>,
201            Option<Jamo>,
202        ),
203        BlockError,
204    > {
205        let (i1, i2) = match &self.initial {
206            Jamo::CompositeConsonant(c) => match c.decompose() {
207                (a, b) => (Some(a), Some(b)),
208            },
209            Jamo::Consonant(c) => (Some(Jamo::Consonant(c.clone())), None),
210            _ => (None, None),
211        };
212
213        let (v1, v2) = match &self.vowel {
214            Jamo::CompositeVowel(c) => match c.decompose() {
215                (a, b) => (Some(a), Some(b)),
216            },
217            Jamo::Vowel(c) => (Some(Jamo::Vowel(c.clone())), None),
218            _ => (None, None),
219        };
220
221        let (f1, f2) = match &self.final_optional {
222            Some(Jamo::CompositeConsonant(c)) => match c.decompose() {
223                (a, b) => (Some(a), Some(b)),
224            },
225            Some(Jamo::Consonant(c)) => (Some(Jamo::Consonant(c.clone())), None),
226            _ => (None, None),
227        };
228
229        Ok((i1, i2, v1, v2, f1, f2))
230    }
231
232    /// Decomposes the `HangulBlock` into its constituent Jamo characters
233    /// as a vector of `char`s, according to the specified decomposition options.
234    pub fn decomposed_vec(
235        &self,
236        options: &HangulBlockDecompositionOptions,
237    ) -> Result<Vec<char>, BlockError> {
238        let mut result = Vec::new();
239
240        match (&self.initial, &options.jamo_era) {
241            (Jamo::CompositeConsonant(c), JamoUnicodeType::Modern) => {
242                if options.decompose_composites {
243                    let (a, b) = c.decompose();
244                    result.push(a.char_modern(JamoPosition::Initial).ok_or(
245                        BlockError::InvalidJamoContext(
246                            a,
247                            JamoPosition::Initial,
248                            JamoUnicodeType::Modern,
249                        ),
250                    )?);
251                    result.push(b.char_modern(JamoPosition::Initial).ok_or(
252                        BlockError::InvalidJamoContext(
253                            b,
254                            JamoPosition::Initial,
255                            JamoUnicodeType::Modern,
256                        ),
257                    )?);
258                } else {
259                    result.push(c.char_modern(JamoPosition::Initial).ok_or(
260                        BlockError::InvalidJamoContext(
261                            Jamo::CompositeConsonant(c.clone()),
262                            JamoPosition::Initial,
263                            JamoUnicodeType::Modern,
264                        ),
265                    )?);
266                }
267            }
268            (Jamo::CompositeConsonant(c), JamoUnicodeType::Compatibility) => {
269                if options.decompose_composites {
270                    match c.decompose() {
271                        (a, b) => {
272                            result.push(a.char_compatibility());
273                            result.push(b.char_compatibility());
274                        }
275                    }
276                } else {
277                    result.push(c.char_compatibility());
278                }
279            }
280            (Jamo::Consonant(c), JamoUnicodeType::Modern) => {
281                result.push(c.char_modern(JamoPosition::Initial).ok_or(
282                    BlockError::InvalidJamoContext(
283                        Jamo::Consonant(c.clone()),
284                        JamoPosition::Initial,
285                        JamoUnicodeType::Modern,
286                    ),
287                )?);
288            }
289            (Jamo::Consonant(c), JamoUnicodeType::Compatibility) => {
290                result.push(c.char_compatibility());
291            }
292            (j, _) => {
293                return Err(BlockError::JamoInInvalidPosition(
294                    j.clone(),
295                    JamoPosition::Initial,
296                ));
297            }
298        }
299
300        match (&self.vowel, &options.jamo_era) {
301            (Jamo::CompositeVowel(c), JamoUnicodeType::Modern) => {
302                if options.decompose_composites {
303                    let (a, b) = c.decompose();
304                    result.push(a.char_modern(JamoPosition::Vowel).ok_or(
305                        BlockError::InvalidJamoContext(
306                            Jamo::CompositeVowel(c.clone()),
307                            JamoPosition::Vowel,
308                            JamoUnicodeType::Modern,
309                        ),
310                    )?);
311                    result.push(b.char_modern(JamoPosition::Vowel).ok_or(
312                        BlockError::InvalidJamoContext(
313                            Jamo::CompositeVowel(c.clone()),
314                            JamoPosition::Vowel,
315                            JamoUnicodeType::Modern,
316                        ),
317                    )?);
318                } else {
319                    result.push(c.char_modern());
320                }
321            }
322            (Jamo::CompositeVowel(c), JamoUnicodeType::Compatibility) => {
323                if options.decompose_composites {
324                    match c.decompose() {
325                        (a, b) => {
326                            result.push(a.char_compatibility());
327                            result.push(b.char_compatibility());
328                        }
329                    }
330                } else {
331                    result.push(c.char_compatibility());
332                }
333            }
334            (Jamo::Vowel(c), JamoUnicodeType::Modern) => {
335                result.push(c.char_modern());
336            }
337            (Jamo::Vowel(c), JamoUnicodeType::Compatibility) => {
338                result.push(c.char_compatibility());
339            }
340            _ => {
341                return Err(BlockError::JamoInInvalidPosition(
342                    self.vowel.clone(),
343                    JamoPosition::Vowel,
344                ));
345            }
346        }
347
348        if let Some(final_jamo) = &self.final_optional {
349            match (&final_jamo, &options.jamo_era) {
350                (Jamo::CompositeConsonant(c), JamoUnicodeType::Modern) => {
351                    if options.decompose_composites {
352                        let (a, b) = c.decompose();
353                        result.push(a.char_modern(JamoPosition::Final).ok_or(
354                            BlockError::InvalidJamoContext(
355                                Jamo::CompositeConsonant(c.clone()),
356                                JamoPosition::Final,
357                                JamoUnicodeType::Modern,
358                            ),
359                        )?);
360                        result.push(b.char_modern(JamoPosition::Final).ok_or(
361                            BlockError::InvalidJamoContext(
362                                Jamo::CompositeConsonant(c.clone()),
363                                JamoPosition::Final,
364                                JamoUnicodeType::Modern,
365                            ),
366                        )?);
367                    } else {
368                        result.push(c.char_modern(JamoPosition::Final).ok_or(
369                            BlockError::InvalidJamoContext(
370                                Jamo::CompositeConsonant(c.clone()),
371                                JamoPosition::Final,
372                                JamoUnicodeType::Modern,
373                            ),
374                        )?);
375                    }
376                }
377                (Jamo::CompositeConsonant(c), JamoUnicodeType::Compatibility) => {
378                    if options.decompose_composites {
379                        match c.decompose() {
380                            (a, b) => {
381                                result.push(a.char_compatibility());
382                                result.push(b.char_compatibility());
383                            }
384                        }
385                    } else {
386                        result.push(c.char_compatibility());
387                    }
388                }
389                (Jamo::Consonant(c), JamoUnicodeType::Modern) => {
390                    result.push(c.char_modern(JamoPosition::Final).ok_or(
391                        BlockError::InvalidJamoContext(
392                            Jamo::Consonant(c.clone()),
393                            JamoPosition::Final,
394                            JamoUnicodeType::Modern,
395                        ),
396                    )?);
397                }
398                (Jamo::Consonant(c), JamoUnicodeType::Compatibility) => {
399                    result.push(c.char_compatibility());
400                }
401                _ => {
402                    return Err(BlockError::JamoInInvalidPosition(
403                        final_jamo.clone(),
404                        JamoPosition::Final,
405                    ));
406                }
407            }
408        }
409
410        Ok(result)
411    }
412}
413
414/// Options for decomposing a `HangulBlock` into its constituent Jamo characters.
415///
416/// **Example:**
417/// ```rust
418/// use hangul::block::{HangulBlock, HangulBlockDecompositionOptions};
419/// use hangul::jamo::{Jamo, JamoUnicodeType};
420///
421/// let block = HangulBlock {
422///    initial: Jamo::from_compatibility_jamo('ㄱ').unwrap(),
423///   vowel: Jamo::from_compatibility_jamo('ㅘ').unwrap(),
424///   final_optional: Some(Jamo::from_compatibility_jamo('ㄳ').unwrap()),
425/// };
426///
427/// let options = HangulBlockDecompositionOptions {
428///     decompose_composites: true,
429///     jamo_era: JamoUnicodeType::Compatibility,
430/// };
431///
432/// let decomposed = block.decomposed_vec(&options).unwrap();
433/// assert_eq!(decomposed, vec!['ㄱ', 'ㅗ', 'ㅏ', 'ㄱ', 'ㅅ']);
434/// ```
435pub struct HangulBlockDecompositionOptions {
436    /// Whether to decompose composite Jamo into their singular components.
437    pub decompose_composites: bool,
438
439    /// The Unicode era of the Jamo characters to use in decomposition.
440    pub jamo_era: JamoUnicodeType,
441}
442
443/// Result of pushing a Jamo letter into a Hangul syllable block composer.
444#[derive(Debug, PartialEq, Eq)]
445pub enum BlockPushResult {
446    /// The Jamo letter was successfully pushed into the block composer.
447    Success,
448
449    /// The Jamo letter could not be pushed because it would create
450    /// an invalid Hangul syllable. However, the letter is a valid
451    /// initial consonant to begin a new syllable block, so the caller
452    /// should start a new block without popping any Jamo from this one.
453    StartNewBlockNoPop,
454
455    /// The Jamo letter could not be pushed because it would create
456    /// an invalid Hangul syllable. The letter is not a valid initial
457    /// consonant, so the caller should pop the last Jamo from this block
458    /// and use it to start a new block.
459    PopAndStartNewBlock,
460
461    /// The Jamo letter is not valid in the context of Hangul syllable
462    /// composition. For example, pushing a vowel when an initial consonant
463    /// is expected.
464    InvalidHangul,
465
466    /// The Jamo letter is not valid Hangul.
467    NonHangul,
468}
469
470#[derive(Debug, PartialEq, Eq)]
471enum BlockCompositionState {
472    /// nothing, waiting for first consonant
473    ExpectingInitial,
474
475    /// ex. ㄷ -> ㄸ or 다
476    ExpectingDoubleInitialOrVowel,
477
478    /// ex. ㄸ -> 따
479    ExpectingVowel,
480
481    /// ex. 두 -> 둬 or 둔
482    ExpectingCompositeVowelOrFinal,
483
484    /// ex. 둬 -> 뒁
485    ExpectingFinal,
486
487    /// ex. 달 -> 닳 or 다래
488    ExpectingCompositeFinal,
489
490    /// ex. 닳 -> 달하
491    ExpectingNextBlock,
492}
493
494/// A composer for a single Hangul syllable block. Used to build a block
495/// by pushing and popping Jamo letters.
496///
497/// **API:**
498/// ```rust
499/// use hangul::block::{BlockComposer, BlockPushResult};
500/// use hangul::jamo::{Jamo, JamoConsonantSingular, JamoVowelSingular};
501///
502/// let mut composer = BlockComposer::new();
503///
504/// // Push letters to form the syllable '강'
505/// assert_eq!(
506///     composer.push(&Jamo::Consonant(JamoConsonantSingular::Giyeok)),
507///     BlockPushResult::Success
508/// );
509/// assert_eq!(
510///     composer.push(&Jamo::Vowel(JamoVowelSingular::A)),
511///     BlockPushResult::Success
512/// );
513/// assert_eq!(
514///     composer.push(&Jamo::Consonant(JamoConsonantSingular::Ieung)),
515///     BlockPushResult::Success
516/// );
517///
518/// // Try to push another character that would not fit in the current block
519/// assert_eq!(
520///   composer.push(&Jamo::Vowel(JamoVowelSingular::A)),
521///   BlockPushResult::PopAndStartNewBlock
522/// );
523///
524/// // Get the composed block as a character
525/// let block_char = composer.block_as_string().unwrap();
526/// assert_eq!(block_char, Some('강'));
527/// ```
528#[derive(Debug, PartialEq, Eq)]
529pub struct BlockComposer {
530    state: BlockCompositionState,
531    initial_first: Option<Jamo>,
532    initial_second: Option<Jamo>,
533    vowel_first: Option<Jamo>,
534    vowel_second: Option<Jamo>,
535    final_first: Option<Jamo>,
536    final_second: Option<Jamo>,
537}
538
539/// The status of attempting to complete a Hangul syllable block.
540#[derive(Debug, PartialEq, Eq)]
541pub enum BlockCompletionStatus {
542    /// The block is complete and can be represented as a `HangulBlock`.
543    Complete(HangulBlock),
544
545    /// The block is incomplete, but contains at one Jamo character.
546    Incomplete(Jamo),
547
548    /// The block is empty and contains no Jamo characters.
549    Empty,
550}
551
552/// The status of popping a Jamo letter from a Hangul syllable block composer.
553#[derive(Debug, PartialEq, Eq)]
554pub enum BlockPopStatus {
555    /// A Jamo letter was popped and the block still has letters remaining.
556    PoppedAndNonEmpty(Jamo),
557
558    /// A Jamo letter was popped and the block is now empty.
559    PoppedAndEmpty(Jamo),
560
561    /// The block is already empty; no letters to pop.
562    None,
563}
564
565impl BlockComposer {
566    /// Creates a new, empty `BlockComposer`.
567    pub fn new() -> Self {
568        BlockComposer {
569            state: BlockCompositionState::ExpectingInitial,
570            initial_first: None,
571            initial_second: None,
572            vowel_first: None,
573            vowel_second: None,
574            final_first: None,
575            final_second: None,
576        }
577    }
578
579    /// Tries to push a Jamo letter into the `BlockComposer`.
580    /// Returns a `BlockPushResult` indicating the outcome of the operation.
581    /// If the letter could not be pushed, the state of the current block will
582    /// remain unchanged.
583    pub fn push(&mut self, letter: &Jamo) -> BlockPushResult {
584        match self.state {
585            BlockCompositionState::ExpectingInitial => self.try_push_initial(letter),
586            BlockCompositionState::ExpectingDoubleInitialOrVowel => {
587                self.try_push_double_initial_or_vowel(letter)
588            }
589            BlockCompositionState::ExpectingVowel => self.try_push_vowel(letter),
590            BlockCompositionState::ExpectingCompositeVowelOrFinal => {
591                self.try_push_composite_vowel_or_final(letter)
592            }
593            BlockCompositionState::ExpectingFinal => self.try_push_final(letter),
594            BlockCompositionState::ExpectingCompositeFinal => self.try_push_composite_final(letter),
595            BlockCompositionState::ExpectingNextBlock => self.try_push_next_block(letter),
596        }
597    }
598
599    /// Tries to push a character into the `BlockComposer`. If the character
600    /// corresponds to a Hangul Jamo letter, it is pushed into the composer.
601    /// If the character is not Hangul, `BlockPushResult::NonHangul` is returned.
602    ///
603    /// **Example:**
604    /// ```rust
605    /// use hangul::block::{BlockComposer, BlockPushResult};
606    /// use hangul::jamo::{Jamo, JamoConsonantSingular, JamoVowelSingular};
607    ///
608    /// let mut composer = BlockComposer::new();
609    ///
610    /// // Push letters to form the syllable '강'
611    /// assert_eq!(
612    ///     composer.push_char('ㄱ').unwrap(),
613    ///     BlockPushResult::Success
614    /// );
615    /// assert_eq!(
616    ///     composer.push_char('ㅏ').unwrap(),
617    ///     BlockPushResult::Success
618    /// );
619    /// assert_eq!(
620    ///     composer.push_char('ㅇ').unwrap(),
621    ///     BlockPushResult::Success
622    /// );
623    ///
624    /// // Try to push another character that would not fit in the current block
625    /// assert_eq!(
626    ///   composer.push_char('ㅏ').unwrap(),
627    ///   BlockPushResult::PopAndStartNewBlock
628    /// );
629    pub fn push_char(&mut self, c: char) -> Result<BlockPushResult, BlockError> {
630        match Character::from_char(c)?.jamo() {
631            Some(jamo) => Ok(self.push(&jamo)),
632            None => Ok(BlockPushResult::NonHangul),
633        }
634    }
635
636    /// Pops a Jamo letter from the `BlockComposer`. Returns a `BlockPopStatus`
637    /// indicating the outcome of the operation, with values:
638    /// - `PoppedAndNonEmpty(Jamo)`: A Jamo letter was popped and the block still has letters remaining.
639    /// - `PoppedAndEmpty(Jamo)`: A Jamo letter was popped and the block is now empty.
640    /// - `None`: The block is already empty; no letters to pop.
641    ///
642    /// **Example:**
643    /// ```rust
644    /// use hangul::block::{BlockComposer, BlockPopStatus};
645    /// use hangul::jamo::{Jamo, JamoConsonantSingular, JamoVowelSingular};
646    ///
647    /// let mut composer = BlockComposer::new();
648    /// composer.push(&Jamo::from_compatibility_jamo('ㄱ').unwrap());
649    /// composer.push(&Jamo::from_compatibility_jamo('ㅏ').unwrap());
650    ///
651    /// assert_eq!(
652    ///     composer.pop(),
653    ///     BlockPopStatus::PoppedAndNonEmpty(Jamo::Vowel(JamoVowelSingular::A))
654    /// );
655    /// assert_eq!(
656    ///     composer.pop(),
657    ///     BlockPopStatus::PoppedAndEmpty(Jamo::Consonant(JamoConsonantSingular::Giyeok))
658    /// );
659    /// assert_eq!(
660    ///     composer.pop(),
661    ///     BlockPopStatus::None
662    /// );
663    /// ```
664    pub fn pop(&mut self) -> BlockPopStatus {
665        if let Some(c) = self.final_second.take() {
666            self.state = BlockCompositionState::ExpectingCompositeFinal;
667            BlockPopStatus::PoppedAndNonEmpty(c)
668        } else if let Some(c) = self.final_first.take() {
669            self.state = match self.vowel_second {
670                Some(_) => BlockCompositionState::ExpectingFinal,
671                None => BlockCompositionState::ExpectingCompositeVowelOrFinal,
672            };
673            BlockPopStatus::PoppedAndNonEmpty(c)
674        } else if let Some(c) = self.vowel_second.take() {
675            self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
676            BlockPopStatus::PoppedAndNonEmpty(c)
677        } else if let Some(c) = self.vowel_first.take() {
678            self.state = match self.initial_second {
679                Some(_) => BlockCompositionState::ExpectingVowel,
680                None => BlockCompositionState::ExpectingDoubleInitialOrVowel,
681            };
682            BlockPopStatus::PoppedAndNonEmpty(c)
683        } else if let Some(c) = self.initial_second.take() {
684            self.state = BlockCompositionState::ExpectingVowel;
685            BlockPopStatus::PoppedAndNonEmpty(c)
686        } else if let Some(c) = self.initial_first.take() {
687            self.state = BlockCompositionState::ExpectingInitial;
688            BlockPopStatus::PoppedAndEmpty(c)
689        } else {
690            self.state = BlockCompositionState::ExpectingInitial;
691            BlockPopStatus::None
692        }
693    }
694
695    pub(crate) fn pop_end_consonant(&mut self) -> Option<Jamo> {
696        if let Some(c) = self.final_second.take() {
697            Some(c)
698        } else if let Some(c) = self.final_first.take() {
699            Some(c)
700        } else {
701            None
702        }
703    }
704
705    fn try_push_initial(&mut self, letter: &Jamo) -> BlockPushResult {
706        match letter {
707            Jamo::Consonant(_) => {
708                self.initial_first = Some(letter.clone());
709                self.state = BlockCompositionState::ExpectingDoubleInitialOrVowel;
710                BlockPushResult::Success
711            }
712            Jamo::CompositeConsonant(c) => {
713                if c.is_valid_initial() {
714                    self.initial_first = Some(letter.clone());
715                    self.state = BlockCompositionState::ExpectingVowel;
716                    BlockPushResult::Success
717                } else {
718                    BlockPushResult::InvalidHangul
719                }
720            }
721            _ => BlockPushResult::InvalidHangul,
722        }
723    }
724
725    fn try_push_double_initial_or_vowel(&mut self, letter: &Jamo) -> BlockPushResult {
726        match letter {
727            Jamo::Consonant(c) => match &self.initial_first {
728                Some(Jamo::Consonant(i1)) => {
729                    if i1.combine_for_initial(c).is_some() {
730                        self.initial_second = Some(letter.clone());
731                        self.state = BlockCompositionState::ExpectingVowel;
732                        BlockPushResult::Success
733                    } else {
734                        BlockPushResult::InvalidHangul
735                    }
736                }
737                _ => BlockPushResult::InvalidHangul,
738            },
739            Jamo::Vowel(_) => {
740                self.vowel_first = Some(letter.clone());
741                self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
742                BlockPushResult::Success
743            }
744            Jamo::CompositeVowel(c) => match c.decompose() {
745                (v1, v2) => {
746                    self.vowel_first = Some(v1);
747                    self.vowel_second = Some(v2);
748                    self.state = BlockCompositionState::ExpectingFinal;
749                    BlockPushResult::Success
750                }
751            },
752            Jamo::CompositeConsonant(_) => BlockPushResult::InvalidHangul,
753        }
754    }
755
756    fn try_push_vowel(&mut self, letter: &Jamo) -> BlockPushResult {
757        match letter {
758            Jamo::Vowel(_) => {
759                self.vowel_first = Some(letter.clone());
760                self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
761                BlockPushResult::Success
762            }
763            Jamo::CompositeVowel(c) => match c.decompose() {
764                (v1, v2) => {
765                    self.vowel_first = Some(v1);
766                    self.vowel_second = Some(v2);
767                    self.state = BlockCompositionState::ExpectingFinal;
768                    BlockPushResult::Success
769                }
770            },
771            _ => BlockPushResult::InvalidHangul,
772        }
773    }
774
775    fn try_push_composite_vowel_or_final(&mut self, letter: &Jamo) -> BlockPushResult {
776        match letter {
777            Jamo::Vowel(c) => match &self.vowel_first {
778                Some(Jamo::Vowel(v1)) => {
779                    if v1.combine(c).is_some() {
780                        self.vowel_second = Some(letter.clone());
781                        self.state = BlockCompositionState::ExpectingFinal;
782                        BlockPushResult::Success
783                    } else {
784                        BlockPushResult::InvalidHangul
785                    }
786                }
787                _ => BlockPushResult::InvalidHangul,
788            },
789            Jamo::Consonant(_) => {
790                self.final_first = Some(letter.clone());
791                self.state = BlockCompositionState::ExpectingCompositeFinal;
792                BlockPushResult::Success
793            }
794            Jamo::CompositeConsonant(c) => {
795                if c.is_valid_final() {
796                    match c.decompose() {
797                        (f1, f2) => {
798                            self.final_first = Some(f1);
799                            self.final_second = Some(f2);
800                            self.state = BlockCompositionState::ExpectingNextBlock;
801                            BlockPushResult::Success
802                        }
803                    }
804                } else if c.is_valid_initial() {
805                    BlockPushResult::StartNewBlockNoPop
806                } else {
807                    BlockPushResult::InvalidHangul
808                }
809            }
810            _ => BlockPushResult::InvalidHangul,
811        }
812    }
813
814    fn try_push_final(&mut self, letter: &Jamo) -> BlockPushResult {
815        match letter {
816            Jamo::Consonant(_) => {
817                self.final_first = Some(letter.clone());
818                self.state = BlockCompositionState::ExpectingCompositeFinal;
819                BlockPushResult::Success
820            }
821            Jamo::CompositeConsonant(c) => {
822                if c.is_valid_final() {
823                    match c.decompose() {
824                        (f1, f2) => {
825                            self.final_first = Some(f1);
826                            self.final_second = Some(f2);
827                            self.state = BlockCompositionState::ExpectingNextBlock;
828                            BlockPushResult::Success
829                        }
830                    }
831                } else if c.is_valid_initial() {
832                    BlockPushResult::StartNewBlockNoPop
833                } else {
834                    BlockPushResult::InvalidHangul
835                }
836            }
837            _ => BlockPushResult::InvalidHangul,
838        }
839    }
840
841    fn try_push_composite_final(&mut self, letter: &Jamo) -> BlockPushResult {
842        match letter {
843            Jamo::Consonant(c) => match &self.final_first {
844                Some(Jamo::Consonant(f1)) => {
845                    if f1.combine_for_final(c).is_some() {
846                        self.final_second = Some(letter.clone());
847                        self.state = BlockCompositionState::ExpectingNextBlock;
848                        BlockPushResult::Success
849                    } else {
850                        BlockPushResult::StartNewBlockNoPop
851                    }
852                }
853                _ => BlockPushResult::InvalidHangul,
854            },
855            Jamo::CompositeConsonant(c) => {
856                if c.is_valid_initial() {
857                    BlockPushResult::StartNewBlockNoPop
858                } else {
859                    BlockPushResult::InvalidHangul
860                }
861            }
862            _ => BlockPushResult::PopAndStartNewBlock,
863        }
864    }
865
866    fn try_push_next_block(&mut self, letter: &Jamo) -> BlockPushResult {
867        match letter {
868            Jamo::Consonant(_) | Jamo::CompositeConsonant(_) => BlockPushResult::StartNewBlockNoPop,
869            Jamo::Vowel(_) | Jamo::CompositeVowel(_) => BlockPushResult::PopAndStartNewBlock,
870        }
871    }
872
873    /// Attempts to convert the current state of the `BlockComposer`
874    /// into a complete `HangulBlock`. If the block is incomplete,
875    /// it returns an `Incomplete` status with the last Jamo character
876    /// added. If the block is empty, it returns an `Empty` status.
877    ///
878    /// **Example:**
879    /// ```rust
880    /// use hangul::block::{BlockComposer, BlockCompletionStatus, HangulBlock};
881    /// use hangul::jamo::{Jamo, JamoConsonantSingular, JamoVowelSingular};
882    ///
883    /// let mut composer = BlockComposer::new();
884    ///
885    /// composer.push(&Jamo::from_compatibility_jamo('ㄱ').unwrap());
886    ///
887    /// // Attempt to complete incomplete block
888    /// assert_eq!(
889    ///     composer.try_as_complete_block(),
890    ///     Ok(BlockCompletionStatus::Incomplete(Jamo::Consonant(JamoConsonantSingular::Giyeok)))
891    /// );
892    ///
893    /// composer.push(&Jamo::from_compatibility_jamo('ㅏ').unwrap());
894    ///
895    /// // Get the complete block now that a vowel has been added
896    /// assert_eq!(
897    ///    composer.try_as_complete_block(),
898    ///    Ok(BlockCompletionStatus::Complete(HangulBlock {
899    ///        initial: Jamo::Consonant(JamoConsonantSingular::Giyeok),
900    ///        vowel: Jamo::Vowel(JamoVowelSingular::A),
901    ///        final_optional: None,
902    ///    }))
903    /// );
904    /// ```
905    pub fn try_as_complete_block(&self) -> Result<BlockCompletionStatus, BlockError> {
906        let initial_optional = match (&self.initial_first, &self.initial_second) {
907            (Some(Jamo::Consonant(i1)), Some(Jamo::Consonant(i2))) => {
908                match i1.combine_for_initial(&i2) {
909                    Some(composite) => Some(Jamo::CompositeConsonant(composite)),
910                    None => {
911                        return Err(BlockError::JamoInInvalidPosition(
912                            Jamo::Consonant(i2.clone()),
913                            JamoPosition::Initial,
914                        ));
915                    }
916                }
917            }
918            (Some(i1), None) => Some(i1.clone()),
919            _ => None,
920        };
921        let vowel_optional = match (&self.vowel_first, &self.vowel_second) {
922            (Some(Jamo::Vowel(v1)), Some(Jamo::Vowel(v2))) => match v1.combine(&v2) {
923                Some(composite) => Some(Jamo::CompositeVowel(composite)),
924                None => {
925                    return Err(BlockError::JamoInInvalidPosition(
926                        Jamo::Vowel(v2.clone()),
927                        JamoPosition::Vowel,
928                    ));
929                }
930            },
931            (Some(v1), None) => Some(v1.clone()),
932            _ => None,
933        };
934        let final_optional = match (&self.final_first, &self.final_second) {
935            (Some(Jamo::Consonant(f1)), Some(Jamo::Consonant(f2))) => {
936                match f1.combine_for_final(&f2) {
937                    Some(composite) => Some(Jamo::CompositeConsonant(composite)),
938                    None => {
939                        return Err(BlockError::JamoInInvalidPosition(
940                            Jamo::Consonant(f2.clone()),
941                            JamoPosition::Final,
942                        ));
943                    }
944                }
945            }
946            (Some(f1), None) => Some(f1.clone()),
947            _ => None,
948        };
949
950        match (initial_optional, vowel_optional) {
951            (Some(initial), Some(vowel)) => Ok(BlockCompletionStatus::Complete(HangulBlock {
952                initial,
953                vowel,
954                final_optional,
955            })),
956            (Some(initial), None) => Ok(BlockCompletionStatus::Incomplete(initial)),
957            (None, Some(vowel)) => Ok(BlockCompletionStatus::Incomplete(vowel)),
958            (None, None) => match final_optional {
959                Some(f) => Ok(BlockCompletionStatus::Incomplete(f)),
960                None => Ok(BlockCompletionStatus::Empty),
961            },
962        }
963    }
964
965    /// Returns the composed Hangul syllable character as an `Option<char>`
966    /// wrapped in a `Result`. If the block is complete, it returns the composed
967    /// character. If the block is incomplete, it returns the Jamo currently in
968    /// the block (in modern Unicode form, not compatibility form). If the block is empty,
969    /// it returns `None`.
970    pub fn block_as_string(&self) -> Result<Option<char>, BlockError> {
971        match self.try_as_complete_block()? {
972            BlockCompletionStatus::Complete(block) => match block.to_char()? {
973                c => Ok(Some(c)),
974            },
975            BlockCompletionStatus::Incomplete(c) => Ok(c.char_modern(match c {
976                Jamo::Consonant(_) | Jamo::CompositeConsonant(_) => JamoPosition::Initial,
977                Jamo::Vowel(_) | Jamo::CompositeVowel(_) => JamoPosition::Vowel,
978            })),
979            BlockCompletionStatus::Empty => Ok(None),
980        }
981    }
982
983    /// Creates a `BlockComposer` from an existing `HangulBlock`,
984    /// decomposing it into its constituent Jamo characters.
985    /// Returns an error if decomposition fails.
986    pub fn from_composed_block(block: &HangulBlock) -> Result<Self, BlockError> {
987        let mut result = BlockComposer::new();
988        let (i1, i2, v1, v2, f1, f2) = block.decomposed_tuple()?;
989
990        if f2.is_some() {
991            result.state = BlockCompositionState::ExpectingNextBlock;
992        } else if f1.is_some() {
993            result.state = BlockCompositionState::ExpectingCompositeFinal;
994        } else if v2.is_some() {
995            result.state = BlockCompositionState::ExpectingFinal;
996        } else if v1.is_some() {
997            result.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
998        }
999        // Anything after this shouldn't happen. But this won't return an error
1000        // because it's conceivable that a manually constructed HangulBlock
1001        // leads to one of these states occuring. This may lead to undefined
1002        // behavior.
1003        else if i2.is_some() {
1004            result.state = BlockCompositionState::ExpectingVowel;
1005        } else if i1.is_some() {
1006            result.state = BlockCompositionState::ExpectingDoubleInitialOrVowel;
1007        } else {
1008            result.state = BlockCompositionState::ExpectingInitial;
1009        }
1010
1011        result.initial_first = i1;
1012        result.initial_second = i2;
1013        result.vowel_first = v1;
1014        result.vowel_second = v2;
1015        result.final_first = f1;
1016        result.final_second = f2;
1017
1018        Ok(result)
1019    }
1020}
1021
1022/// Converts a vector of `HangulBlock` structs into a composed Hangul string.
1023/// Returns an `Err` if any block cannot be converted into a valid Hangul syllable.
1024pub fn hangul_blocks_vec_to_string(blocks: &Vec<HangulBlock>) -> Result<String, BlockError> {
1025    let mut result = String::new();
1026    for block in blocks {
1027        result.push(block.to_char()?);
1028    }
1029    Ok(result)
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034    use super::*;
1035
1036    #[test]
1037    fn test_hangul_block_to_char() {
1038        let block = HangulBlock {
1039            initial: Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1040            vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1041            final_optional: Some(Jamo::from_compatibility_jamo('ㄴ').unwrap()),
1042        };
1043        let result = block.to_char();
1044        assert_eq!(result, Ok('간'));
1045
1046        let block_no_final = HangulBlock {
1047            initial: Jamo::from_compatibility_jamo('ㅂ').unwrap(),
1048            vowel: Jamo::from_compatibility_jamo('ㅗ').unwrap(),
1049            final_optional: None,
1050        };
1051        let result_no_final = block_no_final.to_char();
1052        assert_eq!(result_no_final, Ok('보'));
1053    }
1054
1055    #[test]
1056    fn test_hangul_blocks_vec_to_string() {
1057        let blocks = vec![
1058            HangulBlock {
1059                initial: Jamo::from_compatibility_jamo('ㅇ').unwrap(),
1060                vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1061                final_optional: Some(Jamo::from_compatibility_jamo('ㄴ').unwrap()),
1062            },
1063            HangulBlock {
1064                initial: Jamo::from_compatibility_jamo('ㄴ').unwrap(),
1065                vowel: Jamo::from_compatibility_jamo('ㅕ').unwrap(),
1066                final_optional: Some(Jamo::from_compatibility_jamo('ㅇ').unwrap()),
1067            },
1068            HangulBlock {
1069                initial: Jamo::from_compatibility_jamo('ㅎ').unwrap(),
1070                vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1071                final_optional: None,
1072            },
1073            HangulBlock {
1074                initial: Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1075                vowel: Jamo::from_compatibility_jamo('ㅔ').unwrap(),
1076                final_optional: None,
1077            },
1078            HangulBlock {
1079                initial: Jamo::from_compatibility_jamo('ㅇ').unwrap(),
1080                vowel: Jamo::from_compatibility_jamo('ㅛ').unwrap(),
1081                final_optional: None,
1082            },
1083        ];
1084        let result = hangul_blocks_vec_to_string(&blocks);
1085        assert_eq!(result, Ok("안녕하세요".to_string()));
1086    }
1087
1088    struct BlockComposerPushTestCase {
1089        input: Vec<Jamo>,
1090        expected_final_word_state: BlockPushResult,
1091        expected_final_block_state: BlockCompositionState,
1092    }
1093
1094    fn run_test_cases(cases: Vec<BlockComposerPushTestCase>) {
1095        for case in &cases {
1096            let mut composer = BlockComposer::new();
1097            let mut final_word_state = BlockPushResult::Success;
1098            for letter in &case.input {
1099                final_word_state = composer.push(letter);
1100            }
1101            assert_eq!(
1102                final_word_state, case.expected_final_word_state,
1103                "Final WORD state did not match expected. Composer: {:?}",
1104                composer
1105            );
1106            assert_eq!(
1107                composer.state, case.expected_final_block_state,
1108                "Final BLOCK state did not match expected. Composer: {:?}",
1109                composer
1110            );
1111        }
1112    }
1113
1114    #[test]
1115    fn single_block_composition_valid() {
1116        let test_cases: Vec<BlockComposerPushTestCase> = vec![
1117            BlockComposerPushTestCase {
1118                input: vec![Jamo::from_compatibility_jamo('ㄱ').unwrap()],
1119                expected_final_word_state: BlockPushResult::Success,
1120                expected_final_block_state: BlockCompositionState::ExpectingDoubleInitialOrVowel,
1121            },
1122            BlockComposerPushTestCase {
1123                input: vec![
1124                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1125                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1126                ],
1127                expected_final_word_state: BlockPushResult::Success,
1128                expected_final_block_state: BlockCompositionState::ExpectingVowel,
1129            },
1130            BlockComposerPushTestCase {
1131                input: vec![
1132                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1133                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1134                    Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1135                ],
1136                expected_final_word_state: BlockPushResult::Success,
1137                expected_final_block_state: BlockCompositionState::ExpectingCompositeVowelOrFinal,
1138            },
1139            BlockComposerPushTestCase {
1140                input: vec![
1141                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1142                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1143                    Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1144                    Jamo::from_compatibility_jamo('ㅓ').unwrap(),
1145                ],
1146                expected_final_word_state: BlockPushResult::Success,
1147                expected_final_block_state: BlockCompositionState::ExpectingFinal,
1148            },
1149            BlockComposerPushTestCase {
1150                input: vec![
1151                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1152                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1153                    Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1154                    Jamo::from_compatibility_jamo('ㅓ').unwrap(),
1155                    Jamo::from_compatibility_jamo('ㄹ').unwrap(),
1156                ],
1157                expected_final_word_state: BlockPushResult::Success,
1158                expected_final_block_state: BlockCompositionState::ExpectingCompositeFinal,
1159            },
1160            BlockComposerPushTestCase {
1161                input: vec![
1162                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1163                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1164                    Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1165                    Jamo::from_compatibility_jamo('ㅓ').unwrap(),
1166                    Jamo::from_compatibility_jamo('ㄹ').unwrap(),
1167                    Jamo::from_compatibility_jamo('ㅎ').unwrap(),
1168                ],
1169                expected_final_word_state: BlockPushResult::Success,
1170                expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1171            },
1172            BlockComposerPushTestCase {
1173                input: vec![
1174                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1175                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1176                    Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1177                    Jamo::from_compatibility_jamo('ㅓ').unwrap(),
1178                    Jamo::from_compatibility_jamo('ㄹ').unwrap(),
1179                    Jamo::from_compatibility_jamo('ㅎ').unwrap(),
1180                    Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1181                ],
1182                expected_final_word_state: BlockPushResult::PopAndStartNewBlock,
1183                expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1184            },
1185            BlockComposerPushTestCase {
1186                input: vec![
1187                    Jamo::from_compatibility_jamo('ㅃ').unwrap(),
1188                    Jamo::from_compatibility_jamo('ㅣ').unwrap(),
1189                    Jamo::from_compatibility_jamo('ㄳ').unwrap(),
1190                ],
1191                expected_final_word_state: BlockPushResult::Success,
1192                expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1193            },
1194            BlockComposerPushTestCase {
1195                input: vec![
1196                    Jamo::from_compatibility_jamo('ㅈ').unwrap(),
1197                    Jamo::from_compatibility_jamo('ㅚ').unwrap(),
1198                ],
1199                expected_final_word_state: BlockPushResult::Success,
1200                expected_final_block_state: BlockCompositionState::ExpectingFinal,
1201            },
1202            BlockComposerPushTestCase {
1203                input: vec![
1204                    Jamo::from_compatibility_jamo('ㅉ').unwrap(),
1205                    Jamo::from_compatibility_jamo('ㅢ').unwrap(),
1206                    Jamo::from_compatibility_jamo('ㅃ').unwrap(),
1207                ],
1208                expected_final_word_state: BlockPushResult::StartNewBlockNoPop,
1209                expected_final_block_state: BlockCompositionState::ExpectingFinal,
1210            },
1211            BlockComposerPushTestCase {
1212                input: vec![
1213                    Jamo::from_compatibility_jamo('ㅇ').unwrap(),
1214                    Jamo::from_compatibility_jamo('ㅣ').unwrap(),
1215                    Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1216                    Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1217                ],
1218                expected_final_word_state: BlockPushResult::Success,
1219                expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1220            },
1221            BlockComposerPushTestCase {
1222                input: vec![
1223                    Jamo::from_compatibility_jamo('ㅇ').unwrap(),
1224                    Jamo::from_compatibility_jamo('ㅣ').unwrap(),
1225                    Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1226                    Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1227                    Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1228                ],
1229                expected_final_word_state: BlockPushResult::StartNewBlockNoPop,
1230                expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1231            },
1232        ];
1233
1234        run_test_cases(test_cases);
1235    }
1236
1237    #[test]
1238    fn single_block_composition_invalid() {
1239        let test_cases: Vec<BlockComposerPushTestCase> = vec![
1240            BlockComposerPushTestCase {
1241                input: vec![
1242                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1243                    Jamo::from_compatibility_jamo('ㄹ').unwrap(),
1244                ],
1245                expected_final_word_state: BlockPushResult::InvalidHangul,
1246                expected_final_block_state: BlockCompositionState::ExpectingDoubleInitialOrVowel,
1247            },
1248            BlockComposerPushTestCase {
1249                input: vec![
1250                    Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1251                    Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1252                    Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1253                ],
1254                expected_final_word_state: BlockPushResult::InvalidHangul,
1255                expected_final_block_state: BlockCompositionState::ExpectingCompositeVowelOrFinal,
1256            },
1257        ];
1258        run_test_cases(test_cases);
1259    }
1260
1261    #[derive(Debug)]
1262    struct BlockE2ETestCase((char, char, char, char));
1263
1264    fn run_e2e_test_cases(case: BlockE2ETestCase) {
1265        // let mut composer = BlockComposer::new();
1266        // assert_eq!(
1267        //     composer.push(&Jamo::from_compatibility_jamo(case.0.0).unwrap()),
1268        //     BlockPushResult::Success,
1269        //     "Failed at initial consonant for case {:?}",
1270        //     case
1271        // );
1272        // assert_eq!(
1273        //     composer.push(&Jamo::from_compatibility_jamo(case.0.1).unwrap()),
1274        //     BlockPushResult::Success,
1275        //     "Failed at vowel for case {:?}",
1276        //     case
1277        // );
1278        // if case.0.2 != '\0' {
1279        //     assert_eq!(
1280        //         composer.push(&Jamo::from_compatibility_jamo(case.0.2).unwrap()),
1281        //         BlockPushResult::Success,
1282        //         "Failed at final consonant for case {:?}",
1283        //         case
1284        //     );
1285        // }
1286
1287        // let block_char = composer.block_as_string().unwrap();
1288        // assert_eq!(
1289        //     block_char,
1290        //     Some(case.0.3),
1291        //     "Final composed character did not match expected for case {:?}",
1292        //     case
1293        // );
1294
1295        let from_block_char = HangulBlock::from_char(case.0.3).unwrap();
1296        assert_eq!(
1297            from_block_char.initial,
1298            Jamo::from_compatibility_jamo(case.0.0).unwrap(),
1299            "Initial consonant did not match expected for case {:?}",
1300            case
1301        );
1302        assert_eq!(
1303            from_block_char.vowel,
1304            Jamo::from_compatibility_jamo(case.0.1).unwrap(),
1305            "Vowel did not match expected for case {:?}",
1306            case
1307        );
1308        if case.0.2 != '\0' {
1309            assert_eq!(
1310                from_block_char.final_optional.unwrap(),
1311                Jamo::from_compatibility_jamo(case.0.2).unwrap(),
1312                "Final consonant did not match expected for case {:?}",
1313                case
1314            );
1315        } else {
1316            assert!(
1317                from_block_char.final_optional.is_none(),
1318                "Final consonant was expected to be None for case {:?}",
1319                case
1320            );
1321        }
1322    }
1323
1324    #[test]
1325    fn test_valid_blocks_e2e() {
1326        let case_tuples: Vec<(char, char, char, char)> = vec![
1327            // no final consonant
1328            ('ㅂ', 'ㅛ', '\0', '뵤'),
1329            ('ㅈ', 'ㅕ', '\0', '져'),
1330            ('ㄷ', 'ㅑ', '\0', '댜'),
1331            ('ㄱ', 'ㅐ', '\0', '개'),
1332            ('ㅅ', 'ㅔ', '\0', '세'),
1333            ('ㅁ', 'ㅗ', '\0', '모'),
1334            ('ㄴ', 'ㅓ', '\0', '너'),
1335            ('ㅇ', 'ㅏ', '\0', '아'),
1336            ('ㅎ', 'ㅣ', '\0', '히'),
1337            ('ㅋ', 'ㅠ', '\0', '큐'),
1338            ('ㅌ', 'ㅜ', '\0', '투'),
1339            ('ㅊ', 'ㅡ', '\0', '츠'),
1340            ('ㄹ', 'ㅒ', '\0', '럐'),
1341            ('ㅍ', 'ㅖ', '\0', '폐'),
1342            ('ㅃ', 'ㅛ', '\0', '뾰'),
1343            ('ㅉ', 'ㅕ', '\0', '쪄'),
1344            ('ㄸ', 'ㅑ', '\0', '땨'),
1345            ('ㄲ', 'ㅐ', '\0', '깨'),
1346            ('ㅆ', 'ㅔ', '\0', '쎄'),
1347            ('ㅂ', 'ㅘ', '\0', '봐'),
1348            ('ㅈ', 'ㅙ', '\0', '좨'),
1349            ('ㄷ', 'ㅚ', '\0', '되'),
1350            ('ㄱ', 'ㅝ', '\0', '궈'),
1351            ('ㅅ', 'ㅞ', '\0', '쉐'),
1352            ('ㅁ', 'ㅟ', '\0', '뮈'),
1353            ('ㄴ', 'ㅢ', '\0', '늬'),
1354            // with final consonant
1355            ('ㅂ', 'ㅛ', 'ㅆ', '뵸'),
1356            ('ㅈ', 'ㅕ', 'ㄲ', '젺'),
1357            ('ㄷ', 'ㅑ', 'ㄳ', '댟'),
1358            ('ㄱ', 'ㅐ', 'ㄵ', '갡'),
1359            ('ㅅ', 'ㅔ', 'ㄶ', '섾'),
1360            ('ㅁ', 'ㅗ', 'ㄺ', '몱'),
1361            ('ㄴ', 'ㅓ', 'ㄻ', '넒'),
1362            ('ㅇ', 'ㅏ', 'ㄼ', '앏'),
1363            ('ㅎ', 'ㅣ', 'ㄽ', '힔'),
1364            ('ㅋ', 'ㅠ', 'ㄾ', '큝'),
1365            ('ㅌ', 'ㅜ', 'ㄿ', '툺'),
1366            ('ㅊ', 'ㅡ', 'ㅀ', '츯'),
1367            ('ㄹ', 'ㅒ', 'ㅄ', '럢'),
1368            ('ㅍ', 'ㅖ', 'ㅂ', '폡'),
1369            ('ㅃ', 'ㅛ', 'ㅈ', '뿆'),
1370            ('ㅉ', 'ㅕ', 'ㄷ', '쪋'),
1371            ('ㄸ', 'ㅑ', 'ㄱ', '땩'),
1372            ('ㄲ', 'ㅐ', 'ㅅ', '깻'),
1373            ('ㅆ', 'ㅔ', 'ㅁ', '쎔'),
1374            ('ㅂ', 'ㅘ', 'ㄴ', '봔'),
1375            ('ㅈ', 'ㅙ', 'ㅇ', '좽'),
1376            ('ㄷ', 'ㅚ', 'ㄹ', '될'),
1377            ('ㄱ', 'ㅝ', 'ㅋ', '궠'),
1378            ('ㅅ', 'ㅞ', 'ㅌ', '쉩'),
1379            ('ㅁ', 'ㅟ', 'ㅊ', '뮟'),
1380            ('ㄴ', 'ㅢ', 'ㅍ', '닆'),
1381        ];
1382
1383        for tuple in case_tuples {
1384            run_e2e_test_cases(BlockE2ETestCase(tuple));
1385        }
1386    }
1387
1388    #[test]
1389    fn test_decompose_vec_decompose_composites() {
1390        let block = HangulBlock::from_char('값').unwrap();
1391        let options = HangulBlockDecompositionOptions {
1392            decompose_composites: true,
1393            jamo_era: JamoUnicodeType::Modern,
1394        };
1395
1396        let decomposed = block.decomposed_vec(&options).unwrap();
1397        let expected = vec!['ᄀ', 'ᅡ', 'ᆸ', 'ᆺ'];
1398        assert_eq!(decomposed, expected);
1399    }
1400
1401    #[test]
1402    fn test_decompose_vec_no_decompose_composites() {
1403        let block = HangulBlock::from_char('값').unwrap();
1404        let options = HangulBlockDecompositionOptions {
1405            decompose_composites: false,
1406            jamo_era: JamoUnicodeType::Compatibility,
1407        };
1408
1409        let decomposed = block.decomposed_vec(&options).unwrap();
1410        let expected = vec!['ㄱ', 'ㅏ', 'ㅄ'];
1411        assert_eq!(decomposed, expected);
1412    }
1413}