1use thiserror::Error;
2
3use crate::jamo::*;
4use std::fmt::Debug;
5
6#[derive(Error, Debug, PartialEq, Eq)]
8pub enum BlockError {
9 #[error("Jamo error: {0:?}")]
11 JamoError(#[from] JamoError),
12
13 #[error("Could not convert unknown codepoint U+{0:04X} to Hangul syllable block")]
18 InvalidBlockRepresentation(u32),
19
20 #[error("Jamo '{0:?}' is not valid in position '{1:?}' for Unicode era '{2:?}'")]
24 InvalidJamoContext(Jamo, JamoPosition, JamoUnicodeType),
25
26 #[error("Could not convert codepoint U+{0:04X} to valid Unicode character")]
29 InvalidComponentRepresentation(u32),
30
31 #[error("Jamo '{0:?}' is in invalid position; expected '{1:?}'")]
34 JamoInInvalidPosition(Jamo, JamoPosition),
35}
36
37#[derive(Debug, PartialEq, Eq)]
84pub struct HangulBlock {
85 pub initial: Jamo,
86 pub vowel: Jamo,
87 pub final_optional: Option<Jamo>,
88}
89
90pub 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 pub fn to_char(&self) -> Result<char, BlockError> {
111 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 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 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 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 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 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 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
413pub struct HangulBlockDecompositionOptions {
435 pub decompose_composites: bool,
437
438 pub jamo_era: JamoUnicodeType,
440}
441
442#[derive(Debug, PartialEq, Eq)]
444pub enum BlockPushResult {
445 Success,
447
448 StartNewBlockNoPop,
453
454 PopAndStartNewBlock,
459
460 InvalidHangul,
464
465 NonHangul,
467}
468
469#[derive(Debug, PartialEq, Eq)]
470#[allow(clippy::enum_variant_names)] enum BlockCompositionState {
472 ExpectingInitial,
474
475 ExpectingDoubleInitialOrVowel,
477
478 ExpectingVowel,
480
481 ExpectingCompositeVowelOrFinal,
483
484 ExpectingFinal,
486
487 ExpectingCompositeFinal,
489
490 ExpectingNextBlock,
492}
493
494#[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#[derive(Debug, PartialEq, Eq)]
547pub enum BlockCompletionStatus {
548 Complete(HangulBlock),
550
551 Incomplete(Jamo),
553
554 Empty,
556}
557
558#[derive(Debug, PartialEq, Eq)]
560pub enum BlockPopStatus {
561 PoppedAndNonEmpty(Jamo),
563
564 PoppedAndEmpty(Jamo),
566
567 None,
569}
570
571impl BlockComposer {
572 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 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 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 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 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 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 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 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
1016pub 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 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 ('ㅂ', 'ㅛ', '\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 ('ㅂ', 'ㅛ', 'ㅆ', '뵸'),
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}