Skip to main content

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