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
90impl HangulBlock {
91 pub fn to_char(&self) -> Result<char, BlockError> {
94 let initial = match self.initial.char_modern(JamoPosition::Initial) {
97 Some(c) => c,
98 None => {
99 return Err(BlockError::InvalidJamoContext(
100 self.initial.clone(),
101 JamoPosition::Initial,
102 JamoUnicodeType::Modern,
103 ));
104 }
105 };
106 let vowel = match self.vowel.char_modern(JamoPosition::Vowel) {
107 Some(c) => c,
108 None => {
109 return Err(BlockError::InvalidJamoContext(
110 self.vowel.clone(),
111 JamoPosition::Vowel,
112 JamoUnicodeType::Modern,
113 ));
114 }
115 };
116 let final_optional = match &self.final_optional {
117 Some(c) => c.char_modern(JamoPosition::Final),
118 None => None,
119 };
120
121 let initial_num = initial as u32;
123 let vowel_num = vowel as u32;
124 let final_num = match final_optional {
125 Some(c) => c as u32,
126 None => 0,
127 };
128
129 let l_index = initial_num - L_BASE;
131 let v_index = vowel_num - V_BASE;
132 let t_index = if final_num == 0 {
133 0
134 } else {
135 final_num - T_BASE
136 };
137 let s_index = (l_index * N_COUNT) + (v_index * T_COUNT) + t_index;
138
139 if let Some(c) = std::char::from_u32(S_BASE + s_index) {
141 Ok(c)
142 } else {
143 Err(BlockError::InvalidBlockRepresentation(S_BASE + s_index))
144 }
145 }
146
147 pub fn from_char(c: char) -> Result<Self, BlockError> {
149 let codepoint = c as u32;
150 if codepoint < S_BASE || codepoint > S_BASE + S_COUNT {
151 return Err(BlockError::InvalidBlockRepresentation(codepoint));
152 }
153
154 let s_index = codepoint - S_BASE;
155 let l_index = s_index / N_COUNT;
156 let v_index = (s_index % N_COUNT) / T_COUNT;
157 let t_index = s_index % T_COUNT;
158
159 let initial = Jamo::from_modern_jamo(
160 std::char::from_u32(L_BASE + l_index)
161 .ok_or(BlockError::InvalidComponentRepresentation(L_BASE + l_index))?,
162 )?;
163 let vowel = Jamo::from_modern_jamo(
164 std::char::from_u32(V_BASE + v_index)
165 .ok_or(BlockError::InvalidComponentRepresentation(V_BASE + v_index))?,
166 )?;
167 let final_optional = if t_index > 0 {
168 Some(Jamo::from_modern_jamo(
169 std::char::from_u32(T_BASE + t_index)
170 .ok_or(BlockError::InvalidComponentRepresentation(T_BASE + t_index))?,
171 )?)
172 } else {
173 None
174 };
175
176 Ok(HangulBlock {
177 initial,
178 vowel,
179 final_optional,
180 })
181 }
182
183 pub fn decomposed_tuple(
193 &self,
194 ) -> Result<
195 (
196 Option<Jamo>,
197 Option<Jamo>,
198 Option<Jamo>,
199 Option<Jamo>,
200 Option<Jamo>,
201 Option<Jamo>,
202 ),
203 BlockError,
204 > {
205 let (i1, i2) = match &self.initial {
206 Jamo::CompositeConsonant(c) => match c.decompose() {
207 (a, b) => (Some(a), Some(b)),
208 },
209 Jamo::Consonant(c) => (Some(Jamo::Consonant(c.clone())), None),
210 _ => (None, None),
211 };
212
213 let (v1, v2) = match &self.vowel {
214 Jamo::CompositeVowel(c) => match c.decompose() {
215 (a, b) => (Some(a), Some(b)),
216 },
217 Jamo::Vowel(c) => (Some(Jamo::Vowel(c.clone())), None),
218 _ => (None, None),
219 };
220
221 let (f1, f2) = match &self.final_optional {
222 Some(Jamo::CompositeConsonant(c)) => match c.decompose() {
223 (a, b) => (Some(a), Some(b)),
224 },
225 Some(Jamo::Consonant(c)) => (Some(Jamo::Consonant(c.clone())), None),
226 _ => (None, None),
227 };
228
229 Ok((i1, i2, v1, v2, f1, f2))
230 }
231
232 pub fn decomposed_vec(
235 &self,
236 options: &HangulBlockDecompositionOptions,
237 ) -> Result<Vec<char>, BlockError> {
238 let mut result = Vec::new();
239
240 match (&self.initial, &options.jamo_era) {
241 (Jamo::CompositeConsonant(c), JamoUnicodeType::Modern) => {
242 if options.decompose_composites {
243 let (a, b) = c.decompose();
244 result.push(a.char_modern(JamoPosition::Initial).ok_or(
245 BlockError::InvalidJamoContext(
246 a,
247 JamoPosition::Initial,
248 JamoUnicodeType::Modern,
249 ),
250 )?);
251 result.push(b.char_modern(JamoPosition::Initial).ok_or(
252 BlockError::InvalidJamoContext(
253 b,
254 JamoPosition::Initial,
255 JamoUnicodeType::Modern,
256 ),
257 )?);
258 } else {
259 result.push(c.char_modern(JamoPosition::Initial).ok_or(
260 BlockError::InvalidJamoContext(
261 Jamo::CompositeConsonant(c.clone()),
262 JamoPosition::Initial,
263 JamoUnicodeType::Modern,
264 ),
265 )?);
266 }
267 }
268 (Jamo::CompositeConsonant(c), JamoUnicodeType::Compatibility) => {
269 if options.decompose_composites {
270 match c.decompose() {
271 (a, b) => {
272 result.push(a.char_compatibility());
273 result.push(b.char_compatibility());
274 }
275 }
276 } else {
277 result.push(c.char_compatibility());
278 }
279 }
280 (Jamo::Consonant(c), JamoUnicodeType::Modern) => {
281 result.push(c.char_modern(JamoPosition::Initial).ok_or(
282 BlockError::InvalidJamoContext(
283 Jamo::Consonant(c.clone()),
284 JamoPosition::Initial,
285 JamoUnicodeType::Modern,
286 ),
287 )?);
288 }
289 (Jamo::Consonant(c), JamoUnicodeType::Compatibility) => {
290 result.push(c.char_compatibility());
291 }
292 (j, _) => {
293 return Err(BlockError::JamoInInvalidPosition(
294 j.clone(),
295 JamoPosition::Initial,
296 ));
297 }
298 }
299
300 match (&self.vowel, &options.jamo_era) {
301 (Jamo::CompositeVowel(c), JamoUnicodeType::Modern) => {
302 if options.decompose_composites {
303 let (a, b) = c.decompose();
304 result.push(a.char_modern(JamoPosition::Vowel).ok_or(
305 BlockError::InvalidJamoContext(
306 Jamo::CompositeVowel(c.clone()),
307 JamoPosition::Vowel,
308 JamoUnicodeType::Modern,
309 ),
310 )?);
311 result.push(b.char_modern(JamoPosition::Vowel).ok_or(
312 BlockError::InvalidJamoContext(
313 Jamo::CompositeVowel(c.clone()),
314 JamoPosition::Vowel,
315 JamoUnicodeType::Modern,
316 ),
317 )?);
318 } else {
319 result.push(c.char_modern());
320 }
321 }
322 (Jamo::CompositeVowel(c), JamoUnicodeType::Compatibility) => {
323 if options.decompose_composites {
324 match c.decompose() {
325 (a, b) => {
326 result.push(a.char_compatibility());
327 result.push(b.char_compatibility());
328 }
329 }
330 } else {
331 result.push(c.char_compatibility());
332 }
333 }
334 (Jamo::Vowel(c), JamoUnicodeType::Modern) => {
335 result.push(c.char_modern());
336 }
337 (Jamo::Vowel(c), JamoUnicodeType::Compatibility) => {
338 result.push(c.char_compatibility());
339 }
340 _ => {
341 return Err(BlockError::JamoInInvalidPosition(
342 self.vowel.clone(),
343 JamoPosition::Vowel,
344 ));
345 }
346 }
347
348 if let Some(final_jamo) = &self.final_optional {
349 match (&final_jamo, &options.jamo_era) {
350 (Jamo::CompositeConsonant(c), JamoUnicodeType::Modern) => {
351 if options.decompose_composites {
352 let (a, b) = c.decompose();
353 result.push(a.char_modern(JamoPosition::Final).ok_or(
354 BlockError::InvalidJamoContext(
355 Jamo::CompositeConsonant(c.clone()),
356 JamoPosition::Final,
357 JamoUnicodeType::Modern,
358 ),
359 )?);
360 result.push(b.char_modern(JamoPosition::Final).ok_or(
361 BlockError::InvalidJamoContext(
362 Jamo::CompositeConsonant(c.clone()),
363 JamoPosition::Final,
364 JamoUnicodeType::Modern,
365 ),
366 )?);
367 } else {
368 result.push(c.char_modern(JamoPosition::Final).ok_or(
369 BlockError::InvalidJamoContext(
370 Jamo::CompositeConsonant(c.clone()),
371 JamoPosition::Final,
372 JamoUnicodeType::Modern,
373 ),
374 )?);
375 }
376 }
377 (Jamo::CompositeConsonant(c), JamoUnicodeType::Compatibility) => {
378 if options.decompose_composites {
379 match c.decompose() {
380 (a, b) => {
381 result.push(a.char_compatibility());
382 result.push(b.char_compatibility());
383 }
384 }
385 } else {
386 result.push(c.char_compatibility());
387 }
388 }
389 (Jamo::Consonant(c), JamoUnicodeType::Modern) => {
390 result.push(c.char_modern(JamoPosition::Final).ok_or(
391 BlockError::InvalidJamoContext(
392 Jamo::Consonant(c.clone()),
393 JamoPosition::Final,
394 JamoUnicodeType::Modern,
395 ),
396 )?);
397 }
398 (Jamo::Consonant(c), JamoUnicodeType::Compatibility) => {
399 result.push(c.char_compatibility());
400 }
401 _ => {
402 return Err(BlockError::JamoInInvalidPosition(
403 final_jamo.clone(),
404 JamoPosition::Final,
405 ));
406 }
407 }
408 }
409
410 Ok(result)
411 }
412}
413
414pub struct HangulBlockDecompositionOptions {
436 pub decompose_composites: bool,
438
439 pub jamo_era: JamoUnicodeType,
441}
442
443#[derive(Debug, PartialEq, Eq)]
445pub enum BlockPushResult {
446 Success,
448
449 StartNewBlockNoPop,
454
455 PopAndStartNewBlock,
460
461 InvalidHangul,
465
466 NonHangul,
468}
469
470#[derive(Debug, PartialEq, Eq)]
471enum 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
539#[derive(Debug, PartialEq, Eq)]
541pub enum BlockCompletionStatus {
542 Complete(HangulBlock),
544
545 Incomplete(Jamo),
547
548 Empty,
550}
551
552#[derive(Debug, PartialEq, Eq)]
554pub enum BlockPopStatus {
555 PoppedAndNonEmpty(Jamo),
557
558 PoppedAndEmpty(Jamo),
560
561 None,
563}
564
565impl BlockComposer {
566 pub fn new() -> Self {
568 BlockComposer {
569 state: BlockCompositionState::ExpectingInitial,
570 initial_first: None,
571 initial_second: None,
572 vowel_first: None,
573 vowel_second: None,
574 final_first: None,
575 final_second: None,
576 }
577 }
578
579 pub fn push(&mut self, letter: &Jamo) -> BlockPushResult {
584 match self.state {
585 BlockCompositionState::ExpectingInitial => self.try_push_initial(letter),
586 BlockCompositionState::ExpectingDoubleInitialOrVowel => {
587 self.try_push_double_initial_or_vowel(letter)
588 }
589 BlockCompositionState::ExpectingVowel => self.try_push_vowel(letter),
590 BlockCompositionState::ExpectingCompositeVowelOrFinal => {
591 self.try_push_composite_vowel_or_final(letter)
592 }
593 BlockCompositionState::ExpectingFinal => self.try_push_final(letter),
594 BlockCompositionState::ExpectingCompositeFinal => self.try_push_composite_final(letter),
595 BlockCompositionState::ExpectingNextBlock => self.try_push_next_block(letter),
596 }
597 }
598
599 pub fn push_char(&mut self, c: char) -> Result<BlockPushResult, BlockError> {
630 match Character::from_char(c)?.jamo() {
631 Some(jamo) => Ok(self.push(&jamo)),
632 None => Ok(BlockPushResult::NonHangul),
633 }
634 }
635
636 pub fn pop(&mut self) -> BlockPopStatus {
665 if let Some(c) = self.final_second.take() {
666 self.state = BlockCompositionState::ExpectingCompositeFinal;
667 BlockPopStatus::PoppedAndNonEmpty(c)
668 } else if let Some(c) = self.final_first.take() {
669 self.state = match self.vowel_second {
670 Some(_) => BlockCompositionState::ExpectingFinal,
671 None => BlockCompositionState::ExpectingCompositeVowelOrFinal,
672 };
673 BlockPopStatus::PoppedAndNonEmpty(c)
674 } else if let Some(c) = self.vowel_second.take() {
675 self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
676 BlockPopStatus::PoppedAndNonEmpty(c)
677 } else if let Some(c) = self.vowel_first.take() {
678 self.state = match self.initial_second {
679 Some(_) => BlockCompositionState::ExpectingVowel,
680 None => BlockCompositionState::ExpectingDoubleInitialOrVowel,
681 };
682 BlockPopStatus::PoppedAndNonEmpty(c)
683 } else if let Some(c) = self.initial_second.take() {
684 self.state = BlockCompositionState::ExpectingVowel;
685 BlockPopStatus::PoppedAndNonEmpty(c)
686 } else if let Some(c) = self.initial_first.take() {
687 self.state = BlockCompositionState::ExpectingInitial;
688 BlockPopStatus::PoppedAndEmpty(c)
689 } else {
690 self.state = BlockCompositionState::ExpectingInitial;
691 BlockPopStatus::None
692 }
693 }
694
695 pub(crate) fn pop_end_consonant(&mut self) -> Option<Jamo> {
696 if let Some(c) = self.final_second.take() {
697 Some(c)
698 } else if let Some(c) = self.final_first.take() {
699 Some(c)
700 } else {
701 None
702 }
703 }
704
705 fn try_push_initial(&mut self, letter: &Jamo) -> BlockPushResult {
706 match letter {
707 Jamo::Consonant(_) => {
708 self.initial_first = Some(letter.clone());
709 self.state = BlockCompositionState::ExpectingDoubleInitialOrVowel;
710 BlockPushResult::Success
711 }
712 Jamo::CompositeConsonant(c) => {
713 if c.is_valid_initial() {
714 self.initial_first = Some(letter.clone());
715 self.state = BlockCompositionState::ExpectingVowel;
716 BlockPushResult::Success
717 } else {
718 BlockPushResult::InvalidHangul
719 }
720 }
721 _ => BlockPushResult::InvalidHangul,
722 }
723 }
724
725 fn try_push_double_initial_or_vowel(&mut self, letter: &Jamo) -> BlockPushResult {
726 match letter {
727 Jamo::Consonant(c) => match &self.initial_first {
728 Some(Jamo::Consonant(i1)) => {
729 if i1.combine_for_initial(c).is_some() {
730 self.initial_second = Some(letter.clone());
731 self.state = BlockCompositionState::ExpectingVowel;
732 BlockPushResult::Success
733 } else {
734 BlockPushResult::InvalidHangul
735 }
736 }
737 _ => BlockPushResult::InvalidHangul,
738 },
739 Jamo::Vowel(_) => {
740 self.vowel_first = Some(letter.clone());
741 self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
742 BlockPushResult::Success
743 }
744 Jamo::CompositeVowel(c) => match c.decompose() {
745 (v1, v2) => {
746 self.vowel_first = Some(v1);
747 self.vowel_second = Some(v2);
748 self.state = BlockCompositionState::ExpectingFinal;
749 BlockPushResult::Success
750 }
751 },
752 Jamo::CompositeConsonant(_) => BlockPushResult::InvalidHangul,
753 }
754 }
755
756 fn try_push_vowel(&mut self, letter: &Jamo) -> BlockPushResult {
757 match letter {
758 Jamo::Vowel(_) => {
759 self.vowel_first = Some(letter.clone());
760 self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
761 BlockPushResult::Success
762 }
763 Jamo::CompositeVowel(c) => match c.decompose() {
764 (v1, v2) => {
765 self.vowel_first = Some(v1);
766 self.vowel_second = Some(v2);
767 self.state = BlockCompositionState::ExpectingFinal;
768 BlockPushResult::Success
769 }
770 },
771 _ => BlockPushResult::InvalidHangul,
772 }
773 }
774
775 fn try_push_composite_vowel_or_final(&mut self, letter: &Jamo) -> BlockPushResult {
776 match letter {
777 Jamo::Vowel(c) => match &self.vowel_first {
778 Some(Jamo::Vowel(v1)) => {
779 if v1.combine(c).is_some() {
780 self.vowel_second = Some(letter.clone());
781 self.state = BlockCompositionState::ExpectingFinal;
782 BlockPushResult::Success
783 } else {
784 BlockPushResult::InvalidHangul
785 }
786 }
787 _ => BlockPushResult::InvalidHangul,
788 },
789 Jamo::Consonant(_) => {
790 self.final_first = Some(letter.clone());
791 self.state = BlockCompositionState::ExpectingCompositeFinal;
792 BlockPushResult::Success
793 }
794 Jamo::CompositeConsonant(c) => {
795 if c.is_valid_final() {
796 match c.decompose() {
797 (f1, f2) => {
798 self.final_first = Some(f1);
799 self.final_second = Some(f2);
800 self.state = BlockCompositionState::ExpectingNextBlock;
801 BlockPushResult::Success
802 }
803 }
804 } else if c.is_valid_initial() {
805 BlockPushResult::StartNewBlockNoPop
806 } else {
807 BlockPushResult::InvalidHangul
808 }
809 }
810 _ => BlockPushResult::InvalidHangul,
811 }
812 }
813
814 fn try_push_final(&mut self, letter: &Jamo) -> BlockPushResult {
815 match letter {
816 Jamo::Consonant(_) => {
817 self.final_first = Some(letter.clone());
818 self.state = BlockCompositionState::ExpectingCompositeFinal;
819 BlockPushResult::Success
820 }
821 Jamo::CompositeConsonant(c) => {
822 if c.is_valid_final() {
823 match c.decompose() {
824 (f1, f2) => {
825 self.final_first = Some(f1);
826 self.final_second = Some(f2);
827 self.state = BlockCompositionState::ExpectingNextBlock;
828 BlockPushResult::Success
829 }
830 }
831 } else if c.is_valid_initial() {
832 BlockPushResult::StartNewBlockNoPop
833 } else {
834 BlockPushResult::InvalidHangul
835 }
836 }
837 _ => BlockPushResult::InvalidHangul,
838 }
839 }
840
841 fn try_push_composite_final(&mut self, letter: &Jamo) -> BlockPushResult {
842 match letter {
843 Jamo::Consonant(c) => match &self.final_first {
844 Some(Jamo::Consonant(f1)) => {
845 if f1.combine_for_final(c).is_some() {
846 self.final_second = Some(letter.clone());
847 self.state = BlockCompositionState::ExpectingNextBlock;
848 BlockPushResult::Success
849 } else {
850 BlockPushResult::StartNewBlockNoPop
851 }
852 }
853 _ => BlockPushResult::InvalidHangul,
854 },
855 Jamo::CompositeConsonant(c) => {
856 if c.is_valid_initial() {
857 BlockPushResult::StartNewBlockNoPop
858 } else {
859 BlockPushResult::InvalidHangul
860 }
861 }
862 _ => BlockPushResult::PopAndStartNewBlock,
863 }
864 }
865
866 fn try_push_next_block(&mut self, letter: &Jamo) -> BlockPushResult {
867 match letter {
868 Jamo::Consonant(_) | Jamo::CompositeConsonant(_) => BlockPushResult::StartNewBlockNoPop,
869 Jamo::Vowel(_) | Jamo::CompositeVowel(_) => BlockPushResult::PopAndStartNewBlock,
870 }
871 }
872
873 pub fn try_as_complete_block(&self) -> Result<BlockCompletionStatus, BlockError> {
906 let initial_optional = match (&self.initial_first, &self.initial_second) {
907 (Some(Jamo::Consonant(i1)), Some(Jamo::Consonant(i2))) => {
908 match i1.combine_for_initial(&i2) {
909 Some(composite) => Some(Jamo::CompositeConsonant(composite)),
910 None => {
911 return Err(BlockError::JamoInInvalidPosition(
912 Jamo::Consonant(i2.clone()),
913 JamoPosition::Initial,
914 ));
915 }
916 }
917 }
918 (Some(i1), None) => Some(i1.clone()),
919 _ => None,
920 };
921 let vowel_optional = match (&self.vowel_first, &self.vowel_second) {
922 (Some(Jamo::Vowel(v1)), Some(Jamo::Vowel(v2))) => match v1.combine(&v2) {
923 Some(composite) => Some(Jamo::CompositeVowel(composite)),
924 None => {
925 return Err(BlockError::JamoInInvalidPosition(
926 Jamo::Vowel(v2.clone()),
927 JamoPosition::Vowel,
928 ));
929 }
930 },
931 (Some(v1), None) => Some(v1.clone()),
932 _ => None,
933 };
934 let final_optional = match (&self.final_first, &self.final_second) {
935 (Some(Jamo::Consonant(f1)), Some(Jamo::Consonant(f2))) => {
936 match f1.combine_for_final(&f2) {
937 Some(composite) => Some(Jamo::CompositeConsonant(composite)),
938 None => {
939 return Err(BlockError::JamoInInvalidPosition(
940 Jamo::Consonant(f2.clone()),
941 JamoPosition::Final,
942 ));
943 }
944 }
945 }
946 (Some(f1), None) => Some(f1.clone()),
947 _ => None,
948 };
949
950 match (initial_optional, vowel_optional) {
951 (Some(initial), Some(vowel)) => Ok(BlockCompletionStatus::Complete(HangulBlock {
952 initial,
953 vowel,
954 final_optional,
955 })),
956 (Some(initial), None) => Ok(BlockCompletionStatus::Incomplete(initial)),
957 (None, Some(vowel)) => Ok(BlockCompletionStatus::Incomplete(vowel)),
958 (None, None) => match final_optional {
959 Some(f) => Ok(BlockCompletionStatus::Incomplete(f)),
960 None => Ok(BlockCompletionStatus::Empty),
961 },
962 }
963 }
964
965 pub fn block_as_string(&self) -> Result<Option<char>, BlockError> {
971 match self.try_as_complete_block()? {
972 BlockCompletionStatus::Complete(block) => match block.to_char()? {
973 c => Ok(Some(c)),
974 },
975 BlockCompletionStatus::Incomplete(c) => Ok(c.char_modern(match c {
976 Jamo::Consonant(_) | Jamo::CompositeConsonant(_) => JamoPosition::Initial,
977 Jamo::Vowel(_) | Jamo::CompositeVowel(_) => JamoPosition::Vowel,
978 })),
979 BlockCompletionStatus::Empty => Ok(None),
980 }
981 }
982
983 pub fn from_composed_block(block: &HangulBlock) -> Result<Self, BlockError> {
987 let mut result = BlockComposer::new();
988 let (i1, i2, v1, v2, f1, f2) = block.decomposed_tuple()?;
989
990 if f2.is_some() {
991 result.state = BlockCompositionState::ExpectingNextBlock;
992 } else if f1.is_some() {
993 result.state = BlockCompositionState::ExpectingCompositeFinal;
994 } else if v2.is_some() {
995 result.state = BlockCompositionState::ExpectingFinal;
996 } else if v1.is_some() {
997 result.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
998 }
999 else if i2.is_some() {
1004 result.state = BlockCompositionState::ExpectingVowel;
1005 } else if i1.is_some() {
1006 result.state = BlockCompositionState::ExpectingDoubleInitialOrVowel;
1007 } else {
1008 result.state = BlockCompositionState::ExpectingInitial;
1009 }
1010
1011 result.initial_first = i1;
1012 result.initial_second = i2;
1013 result.vowel_first = v1;
1014 result.vowel_second = v2;
1015 result.final_first = f1;
1016 result.final_second = f2;
1017
1018 Ok(result)
1019 }
1020}
1021
1022pub fn hangul_blocks_vec_to_string(blocks: &Vec<HangulBlock>) -> Result<String, BlockError> {
1025 let mut result = String::new();
1026 for block in blocks {
1027 result.push(block.to_char()?);
1028 }
1029 Ok(result)
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use super::*;
1035
1036 #[test]
1037 fn test_hangul_block_to_char() {
1038 let block = HangulBlock {
1039 initial: Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1040 vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1041 final_optional: Some(Jamo::from_compatibility_jamo('ㄴ').unwrap()),
1042 };
1043 let result = block.to_char();
1044 assert_eq!(result, Ok('간'));
1045
1046 let block_no_final = HangulBlock {
1047 initial: Jamo::from_compatibility_jamo('ㅂ').unwrap(),
1048 vowel: Jamo::from_compatibility_jamo('ㅗ').unwrap(),
1049 final_optional: None,
1050 };
1051 let result_no_final = block_no_final.to_char();
1052 assert_eq!(result_no_final, Ok('보'));
1053 }
1054
1055 #[test]
1056 fn test_hangul_blocks_vec_to_string() {
1057 let blocks = vec![
1058 HangulBlock {
1059 initial: Jamo::from_compatibility_jamo('ㅇ').unwrap(),
1060 vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1061 final_optional: Some(Jamo::from_compatibility_jamo('ㄴ').unwrap()),
1062 },
1063 HangulBlock {
1064 initial: Jamo::from_compatibility_jamo('ㄴ').unwrap(),
1065 vowel: Jamo::from_compatibility_jamo('ㅕ').unwrap(),
1066 final_optional: Some(Jamo::from_compatibility_jamo('ㅇ').unwrap()),
1067 },
1068 HangulBlock {
1069 initial: Jamo::from_compatibility_jamo('ㅎ').unwrap(),
1070 vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1071 final_optional: None,
1072 },
1073 HangulBlock {
1074 initial: Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1075 vowel: Jamo::from_compatibility_jamo('ㅔ').unwrap(),
1076 final_optional: None,
1077 },
1078 HangulBlock {
1079 initial: Jamo::from_compatibility_jamo('ㅇ').unwrap(),
1080 vowel: Jamo::from_compatibility_jamo('ㅛ').unwrap(),
1081 final_optional: None,
1082 },
1083 ];
1084 let result = hangul_blocks_vec_to_string(&blocks);
1085 assert_eq!(result, Ok("안녕하세요".to_string()));
1086 }
1087
1088 struct BlockComposerPushTestCase {
1089 input: Vec<Jamo>,
1090 expected_final_word_state: BlockPushResult,
1091 expected_final_block_state: BlockCompositionState,
1092 }
1093
1094 fn run_test_cases(cases: Vec<BlockComposerPushTestCase>) {
1095 for case in &cases {
1096 let mut composer = BlockComposer::new();
1097 let mut final_word_state = BlockPushResult::Success;
1098 for letter in &case.input {
1099 final_word_state = composer.push(letter);
1100 }
1101 assert_eq!(
1102 final_word_state, case.expected_final_word_state,
1103 "Final WORD state did not match expected. Composer: {:?}",
1104 composer
1105 );
1106 assert_eq!(
1107 composer.state, case.expected_final_block_state,
1108 "Final BLOCK state did not match expected. Composer: {:?}",
1109 composer
1110 );
1111 }
1112 }
1113
1114 #[test]
1115 fn single_block_composition_valid() {
1116 let test_cases: Vec<BlockComposerPushTestCase> = vec![
1117 BlockComposerPushTestCase {
1118 input: vec![Jamo::from_compatibility_jamo('ㄱ').unwrap()],
1119 expected_final_word_state: BlockPushResult::Success,
1120 expected_final_block_state: BlockCompositionState::ExpectingDoubleInitialOrVowel,
1121 },
1122 BlockComposerPushTestCase {
1123 input: vec![
1124 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1125 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1126 ],
1127 expected_final_word_state: BlockPushResult::Success,
1128 expected_final_block_state: BlockCompositionState::ExpectingVowel,
1129 },
1130 BlockComposerPushTestCase {
1131 input: vec![
1132 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1133 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1134 Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1135 ],
1136 expected_final_word_state: BlockPushResult::Success,
1137 expected_final_block_state: BlockCompositionState::ExpectingCompositeVowelOrFinal,
1138 },
1139 BlockComposerPushTestCase {
1140 input: vec![
1141 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1142 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1143 Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1144 Jamo::from_compatibility_jamo('ㅓ').unwrap(),
1145 ],
1146 expected_final_word_state: BlockPushResult::Success,
1147 expected_final_block_state: BlockCompositionState::ExpectingFinal,
1148 },
1149 BlockComposerPushTestCase {
1150 input: vec![
1151 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1152 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1153 Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1154 Jamo::from_compatibility_jamo('ㅓ').unwrap(),
1155 Jamo::from_compatibility_jamo('ㄹ').unwrap(),
1156 ],
1157 expected_final_word_state: BlockPushResult::Success,
1158 expected_final_block_state: BlockCompositionState::ExpectingCompositeFinal,
1159 },
1160 BlockComposerPushTestCase {
1161 input: vec![
1162 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1163 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1164 Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1165 Jamo::from_compatibility_jamo('ㅓ').unwrap(),
1166 Jamo::from_compatibility_jamo('ㄹ').unwrap(),
1167 Jamo::from_compatibility_jamo('ㅎ').unwrap(),
1168 ],
1169 expected_final_word_state: BlockPushResult::Success,
1170 expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1171 },
1172 BlockComposerPushTestCase {
1173 input: vec![
1174 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1175 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1176 Jamo::from_compatibility_jamo('ㅜ').unwrap(),
1177 Jamo::from_compatibility_jamo('ㅓ').unwrap(),
1178 Jamo::from_compatibility_jamo('ㄹ').unwrap(),
1179 Jamo::from_compatibility_jamo('ㅎ').unwrap(),
1180 Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1181 ],
1182 expected_final_word_state: BlockPushResult::PopAndStartNewBlock,
1183 expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1184 },
1185 BlockComposerPushTestCase {
1186 input: vec![
1187 Jamo::from_compatibility_jamo('ㅃ').unwrap(),
1188 Jamo::from_compatibility_jamo('ㅣ').unwrap(),
1189 Jamo::from_compatibility_jamo('ㄳ').unwrap(),
1190 ],
1191 expected_final_word_state: BlockPushResult::Success,
1192 expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1193 },
1194 BlockComposerPushTestCase {
1195 input: vec![
1196 Jamo::from_compatibility_jamo('ㅈ').unwrap(),
1197 Jamo::from_compatibility_jamo('ㅚ').unwrap(),
1198 ],
1199 expected_final_word_state: BlockPushResult::Success,
1200 expected_final_block_state: BlockCompositionState::ExpectingFinal,
1201 },
1202 BlockComposerPushTestCase {
1203 input: vec![
1204 Jamo::from_compatibility_jamo('ㅉ').unwrap(),
1205 Jamo::from_compatibility_jamo('ㅢ').unwrap(),
1206 Jamo::from_compatibility_jamo('ㅃ').unwrap(),
1207 ],
1208 expected_final_word_state: BlockPushResult::StartNewBlockNoPop,
1209 expected_final_block_state: BlockCompositionState::ExpectingFinal,
1210 },
1211 BlockComposerPushTestCase {
1212 input: vec![
1213 Jamo::from_compatibility_jamo('ㅇ').unwrap(),
1214 Jamo::from_compatibility_jamo('ㅣ').unwrap(),
1215 Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1216 Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1217 ],
1218 expected_final_word_state: BlockPushResult::Success,
1219 expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1220 },
1221 BlockComposerPushTestCase {
1222 input: vec![
1223 Jamo::from_compatibility_jamo('ㅇ').unwrap(),
1224 Jamo::from_compatibility_jamo('ㅣ').unwrap(),
1225 Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1226 Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1227 Jamo::from_compatibility_jamo('ㅅ').unwrap(),
1228 ],
1229 expected_final_word_state: BlockPushResult::StartNewBlockNoPop,
1230 expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
1231 },
1232 ];
1233
1234 run_test_cases(test_cases);
1235 }
1236
1237 #[test]
1238 fn single_block_composition_invalid() {
1239 let test_cases: Vec<BlockComposerPushTestCase> = vec![
1240 BlockComposerPushTestCase {
1241 input: vec![
1242 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1243 Jamo::from_compatibility_jamo('ㄹ').unwrap(),
1244 ],
1245 expected_final_word_state: BlockPushResult::InvalidHangul,
1246 expected_final_block_state: BlockCompositionState::ExpectingDoubleInitialOrVowel,
1247 },
1248 BlockComposerPushTestCase {
1249 input: vec![
1250 Jamo::from_compatibility_jamo('ㄱ').unwrap(),
1251 Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1252 Jamo::from_compatibility_jamo('ㅏ').unwrap(),
1253 ],
1254 expected_final_word_state: BlockPushResult::InvalidHangul,
1255 expected_final_block_state: BlockCompositionState::ExpectingCompositeVowelOrFinal,
1256 },
1257 ];
1258 run_test_cases(test_cases);
1259 }
1260
1261 #[derive(Debug)]
1262 struct BlockE2ETestCase((char, char, char, char));
1263
1264 fn run_e2e_test_cases(case: BlockE2ETestCase) {
1265 let from_block_char = HangulBlock::from_char(case.0.3).unwrap();
1296 assert_eq!(
1297 from_block_char.initial,
1298 Jamo::from_compatibility_jamo(case.0.0).unwrap(),
1299 "Initial consonant did not match expected for case {:?}",
1300 case
1301 );
1302 assert_eq!(
1303 from_block_char.vowel,
1304 Jamo::from_compatibility_jamo(case.0.1).unwrap(),
1305 "Vowel did not match expected for case {:?}",
1306 case
1307 );
1308 if case.0.2 != '\0' {
1309 assert_eq!(
1310 from_block_char.final_optional.unwrap(),
1311 Jamo::from_compatibility_jamo(case.0.2).unwrap(),
1312 "Final consonant did not match expected for case {:?}",
1313 case
1314 );
1315 } else {
1316 assert!(
1317 from_block_char.final_optional.is_none(),
1318 "Final consonant was expected to be None for case {:?}",
1319 case
1320 );
1321 }
1322 }
1323
1324 #[test]
1325 fn test_valid_blocks_e2e() {
1326 let case_tuples: Vec<(char, char, char, char)> = vec![
1327 ('ㅂ', 'ㅛ', '\0', '뵤'),
1329 ('ㅈ', 'ㅕ', '\0', '져'),
1330 ('ㄷ', 'ㅑ', '\0', '댜'),
1331 ('ㄱ', 'ㅐ', '\0', '개'),
1332 ('ㅅ', 'ㅔ', '\0', '세'),
1333 ('ㅁ', 'ㅗ', '\0', '모'),
1334 ('ㄴ', 'ㅓ', '\0', '너'),
1335 ('ㅇ', 'ㅏ', '\0', '아'),
1336 ('ㅎ', 'ㅣ', '\0', '히'),
1337 ('ㅋ', 'ㅠ', '\0', '큐'),
1338 ('ㅌ', 'ㅜ', '\0', '투'),
1339 ('ㅊ', 'ㅡ', '\0', '츠'),
1340 ('ㄹ', 'ㅒ', '\0', '럐'),
1341 ('ㅍ', 'ㅖ', '\0', '폐'),
1342 ('ㅃ', 'ㅛ', '\0', '뾰'),
1343 ('ㅉ', 'ㅕ', '\0', '쪄'),
1344 ('ㄸ', 'ㅑ', '\0', '땨'),
1345 ('ㄲ', 'ㅐ', '\0', '깨'),
1346 ('ㅆ', 'ㅔ', '\0', '쎄'),
1347 ('ㅂ', 'ㅘ', '\0', '봐'),
1348 ('ㅈ', 'ㅙ', '\0', '좨'),
1349 ('ㄷ', 'ㅚ', '\0', '되'),
1350 ('ㄱ', 'ㅝ', '\0', '궈'),
1351 ('ㅅ', 'ㅞ', '\0', '쉐'),
1352 ('ㅁ', 'ㅟ', '\0', '뮈'),
1353 ('ㄴ', 'ㅢ', '\0', '늬'),
1354 ('ㅂ', 'ㅛ', 'ㅆ', '뵸'),
1356 ('ㅈ', 'ㅕ', 'ㄲ', '젺'),
1357 ('ㄷ', 'ㅑ', 'ㄳ', '댟'),
1358 ('ㄱ', 'ㅐ', 'ㄵ', '갡'),
1359 ('ㅅ', 'ㅔ', 'ㄶ', '섾'),
1360 ('ㅁ', 'ㅗ', 'ㄺ', '몱'),
1361 ('ㄴ', 'ㅓ', 'ㄻ', '넒'),
1362 ('ㅇ', 'ㅏ', 'ㄼ', '앏'),
1363 ('ㅎ', 'ㅣ', 'ㄽ', '힔'),
1364 ('ㅋ', 'ㅠ', 'ㄾ', '큝'),
1365 ('ㅌ', 'ㅜ', 'ㄿ', '툺'),
1366 ('ㅊ', 'ㅡ', 'ㅀ', '츯'),
1367 ('ㄹ', 'ㅒ', 'ㅄ', '럢'),
1368 ('ㅍ', 'ㅖ', 'ㅂ', '폡'),
1369 ('ㅃ', 'ㅛ', 'ㅈ', '뿆'),
1370 ('ㅉ', 'ㅕ', 'ㄷ', '쪋'),
1371 ('ㄸ', 'ㅑ', 'ㄱ', '땩'),
1372 ('ㄲ', 'ㅐ', 'ㅅ', '깻'),
1373 ('ㅆ', 'ㅔ', 'ㅁ', '쎔'),
1374 ('ㅂ', 'ㅘ', 'ㄴ', '봔'),
1375 ('ㅈ', 'ㅙ', 'ㅇ', '좽'),
1376 ('ㄷ', 'ㅚ', 'ㄹ', '될'),
1377 ('ㄱ', 'ㅝ', 'ㅋ', '궠'),
1378 ('ㅅ', 'ㅞ', 'ㅌ', '쉩'),
1379 ('ㅁ', 'ㅟ', 'ㅊ', '뮟'),
1380 ('ㄴ', 'ㅢ', 'ㅍ', '닆'),
1381 ];
1382
1383 for tuple in case_tuples {
1384 run_e2e_test_cases(BlockE2ETestCase(tuple));
1385 }
1386 }
1387
1388 #[test]
1389 fn test_decompose_vec_decompose_composites() {
1390 let block = HangulBlock::from_char('값').unwrap();
1391 let options = HangulBlockDecompositionOptions {
1392 decompose_composites: true,
1393 jamo_era: JamoUnicodeType::Modern,
1394 };
1395
1396 let decomposed = block.decomposed_vec(&options).unwrap();
1397 let expected = vec!['ᄀ', 'ᅡ', 'ᆸ', 'ᆺ'];
1398 assert_eq!(decomposed, expected);
1399 }
1400
1401 #[test]
1402 fn test_decompose_vec_no_decompose_composites() {
1403 let block = HangulBlock::from_char('값').unwrap();
1404 let options = HangulBlockDecompositionOptions {
1405 decompose_composites: false,
1406 jamo_era: JamoUnicodeType::Compatibility,
1407 };
1408
1409 let decomposed = block.decomposed_vec(&options).unwrap();
1410 let expected = vec!['ㄱ', 'ㅏ', 'ㅄ'];
1411 assert_eq!(decomposed, expected);
1412 }
1413}