use thiserror::Error;
use crate::jamo::*;
use std::fmt::Debug;
#[derive(Error, Debug, PartialEq, Eq)]
pub enum BlockError {
#[error("Jamo error: {0:?}")]
JamoError(#[from] JamoError),
#[error("Could not convert unknown codepoint U+{0:04X} to Hangul syllable block")]
InvalidBlockRepresentation(u32),
#[error("Jamo '{0:?}' is not valid in position '{1:?}' for Unicode era '{2:?}'")]
InvalidJamoContext(Jamo, JamoPosition, JamoUnicodeType),
#[error("Could not convert codepoint U+{0:04X} to valid Unicode character")]
InvalidComponentRepresentation(u32),
#[error("Jamo '{0:?}' is in invalid position; expected '{1:?}'")]
JamoInInvalidPosition(Jamo, JamoPosition),
}
#[derive(Debug, PartialEq, Eq)]
pub struct HangulBlock {
pub initial: Jamo,
pub vowel: Jamo,
pub final_optional: Option<Jamo>,
}
impl HangulBlock {
pub fn to_char(&self) -> Result<char, BlockError> {
let initial = match self.initial.char_modern(JamoPosition::Initial) {
Some(c) => c,
None => {
return Err(BlockError::InvalidJamoContext(
self.initial.clone(),
JamoPosition::Initial,
JamoUnicodeType::Modern,
));
}
};
let vowel = match self.vowel.char_modern(JamoPosition::Vowel) {
Some(c) => c,
None => {
return Err(BlockError::InvalidJamoContext(
self.vowel.clone(),
JamoPosition::Vowel,
JamoUnicodeType::Modern,
));
}
};
let final_optional = match &self.final_optional {
Some(c) => c.char_modern(JamoPosition::Final),
None => None,
};
let initial_num = initial as u32;
let vowel_num = vowel as u32;
let final_num = match final_optional {
Some(c) => c as u32,
None => 0,
};
let l_index = initial_num - L_BASE;
let v_index = vowel_num - V_BASE;
let t_index = if final_num == 0 {
0
} else {
final_num - T_BASE
};
let s_index = (l_index * N_COUNT) + (v_index * T_COUNT) + t_index;
if let Some(c) = std::char::from_u32(S_BASE + s_index) {
Ok(c)
} else {
Err(BlockError::InvalidBlockRepresentation(S_BASE + s_index))
}
}
pub fn from_char(c: char) -> Result<Self, BlockError> {
let codepoint = c as u32;
if codepoint < S_BASE || codepoint > S_BASE + S_COUNT {
return Err(BlockError::InvalidBlockRepresentation(codepoint));
}
let s_index = codepoint - S_BASE;
let l_index = s_index / N_COUNT;
let v_index = (s_index % N_COUNT) / T_COUNT;
let t_index = s_index % T_COUNT;
let initial = Jamo::from_modern_jamo(
std::char::from_u32(L_BASE + l_index)
.ok_or(BlockError::InvalidComponentRepresentation(L_BASE + l_index))?,
)?;
let vowel = Jamo::from_modern_jamo(
std::char::from_u32(V_BASE + v_index)
.ok_or(BlockError::InvalidComponentRepresentation(V_BASE + v_index))?,
)?;
let final_optional = if t_index > 0 {
Some(Jamo::from_modern_jamo(
std::char::from_u32(T_BASE + t_index)
.ok_or(BlockError::InvalidComponentRepresentation(T_BASE + t_index))?,
)?)
} else {
None
};
Ok(HangulBlock {
initial,
vowel,
final_optional,
})
}
pub fn decomposed_tuple(
&self,
) -> Result<
(
Option<Jamo>,
Option<Jamo>,
Option<Jamo>,
Option<Jamo>,
Option<Jamo>,
Option<Jamo>,
),
BlockError,
> {
let (i1, i2) = match &self.initial {
Jamo::CompositeConsonant(c) => match c.decompose() {
(a, b) => (Some(a), Some(b)),
},
Jamo::Consonant(c) => (Some(Jamo::Consonant(c.clone())), None),
_ => (None, None),
};
let (v1, v2) = match &self.vowel {
Jamo::CompositeVowel(c) => match c.decompose() {
(a, b) => (Some(a), Some(b)),
},
Jamo::Vowel(c) => (Some(Jamo::Vowel(c.clone())), None),
_ => (None, None),
};
let (f1, f2) = match &self.final_optional {
Some(Jamo::CompositeConsonant(c)) => match c.decompose() {
(a, b) => (Some(a), Some(b)),
},
Some(Jamo::Consonant(c)) => (Some(Jamo::Consonant(c.clone())), None),
_ => (None, None),
};
Ok((i1, i2, v1, v2, f1, f2))
}
pub fn decomposed_vec(
&self,
options: &HangulBlockDecompositionOptions,
) -> Result<Vec<char>, BlockError> {
let mut result = Vec::new();
match (&self.initial, &options.jamo_era) {
(Jamo::CompositeConsonant(c), JamoUnicodeType::Modern) => {
if options.decompose_composites {
let (a, b) = c.decompose();
result.push(a.char_modern(JamoPosition::Initial).ok_or(
BlockError::InvalidJamoContext(
a,
JamoPosition::Initial,
JamoUnicodeType::Modern,
),
)?);
result.push(b.char_modern(JamoPosition::Initial).ok_or(
BlockError::InvalidJamoContext(
b,
JamoPosition::Initial,
JamoUnicodeType::Modern,
),
)?);
} else {
result.push(c.char_modern(JamoPosition::Initial).ok_or(
BlockError::InvalidJamoContext(
Jamo::CompositeConsonant(c.clone()),
JamoPosition::Initial,
JamoUnicodeType::Modern,
),
)?);
}
}
(Jamo::CompositeConsonant(c), JamoUnicodeType::Compatibility) => {
if options.decompose_composites {
match c.decompose() {
(a, b) => {
result.push(a.char_compatibility());
result.push(b.char_compatibility());
}
}
} else {
result.push(c.char_compatibility());
}
}
(Jamo::Consonant(c), JamoUnicodeType::Modern) => {
result.push(c.char_modern(JamoPosition::Initial).ok_or(
BlockError::InvalidJamoContext(
Jamo::Consonant(c.clone()),
JamoPosition::Initial,
JamoUnicodeType::Modern,
),
)?);
}
(Jamo::Consonant(c), JamoUnicodeType::Compatibility) => {
result.push(c.char_compatibility());
}
(j, _) => {
return Err(BlockError::JamoInInvalidPosition(
j.clone(),
JamoPosition::Initial,
));
}
}
match (&self.vowel, &options.jamo_era) {
(Jamo::CompositeVowel(c), JamoUnicodeType::Modern) => {
if options.decompose_composites {
let (a, b) = c.decompose();
result.push(a.char_modern(JamoPosition::Vowel).ok_or(
BlockError::InvalidJamoContext(
Jamo::CompositeVowel(c.clone()),
JamoPosition::Vowel,
JamoUnicodeType::Modern,
),
)?);
result.push(b.char_modern(JamoPosition::Vowel).ok_or(
BlockError::InvalidJamoContext(
Jamo::CompositeVowel(c.clone()),
JamoPosition::Vowel,
JamoUnicodeType::Modern,
),
)?);
} else {
result.push(c.char_modern());
}
}
(Jamo::CompositeVowel(c), JamoUnicodeType::Compatibility) => {
if options.decompose_composites {
match c.decompose() {
(a, b) => {
result.push(a.char_compatibility());
result.push(b.char_compatibility());
}
}
} else {
result.push(c.char_compatibility());
}
}
(Jamo::Vowel(c), JamoUnicodeType::Modern) => {
result.push(c.char_modern());
}
(Jamo::Vowel(c), JamoUnicodeType::Compatibility) => {
result.push(c.char_compatibility());
}
_ => {
return Err(BlockError::JamoInInvalidPosition(
self.vowel.clone(),
JamoPosition::Vowel,
));
}
}
if let Some(final_jamo) = &self.final_optional {
match (&final_jamo, &options.jamo_era) {
(Jamo::CompositeConsonant(c), JamoUnicodeType::Modern) => {
if options.decompose_composites {
let (a, b) = c.decompose();
result.push(a.char_modern(JamoPosition::Final).ok_or(
BlockError::InvalidJamoContext(
Jamo::CompositeConsonant(c.clone()),
JamoPosition::Final,
JamoUnicodeType::Modern,
),
)?);
result.push(b.char_modern(JamoPosition::Final).ok_or(
BlockError::InvalidJamoContext(
Jamo::CompositeConsonant(c.clone()),
JamoPosition::Final,
JamoUnicodeType::Modern,
),
)?);
} else {
result.push(c.char_modern(JamoPosition::Final).ok_or(
BlockError::InvalidJamoContext(
Jamo::CompositeConsonant(c.clone()),
JamoPosition::Final,
JamoUnicodeType::Modern,
),
)?);
}
}
(Jamo::CompositeConsonant(c), JamoUnicodeType::Compatibility) => {
if options.decompose_composites {
match c.decompose() {
(a, b) => {
result.push(a.char_compatibility());
result.push(b.char_compatibility());
}
}
} else {
result.push(c.char_compatibility());
}
}
(Jamo::Consonant(c), JamoUnicodeType::Modern) => {
result.push(c.char_modern(JamoPosition::Final).ok_or(
BlockError::InvalidJamoContext(
Jamo::Consonant(c.clone()),
JamoPosition::Final,
JamoUnicodeType::Modern,
),
)?);
}
(Jamo::Consonant(c), JamoUnicodeType::Compatibility) => {
result.push(c.char_compatibility());
}
_ => {
return Err(BlockError::JamoInInvalidPosition(
final_jamo.clone(),
JamoPosition::Final,
));
}
}
}
Ok(result)
}
}
pub struct HangulBlockDecompositionOptions {
pub decompose_composites: bool,
pub jamo_era: JamoUnicodeType,
}
#[derive(Debug, PartialEq, Eq)]
pub enum BlockPushResult {
Success,
StartNewBlockNoPop,
PopAndStartNewBlock,
InvalidHangul,
NonHangul,
}
#[derive(Debug, PartialEq, Eq)]
enum BlockCompositionState {
ExpectingInitial,
ExpectingDoubleInitialOrVowel,
ExpectingVowel,
ExpectingCompositeVowelOrFinal,
ExpectingFinal,
ExpectingCompositeFinal,
ExpectingNextBlock,
}
#[derive(Debug, PartialEq, Eq)]
pub struct BlockComposer {
state: BlockCompositionState,
initial_first: Option<Jamo>,
initial_second: Option<Jamo>,
vowel_first: Option<Jamo>,
vowel_second: Option<Jamo>,
final_first: Option<Jamo>,
final_second: Option<Jamo>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum BlockCompletionStatus {
Complete(HangulBlock),
Incomplete(Jamo),
Empty,
}
#[derive(Debug, PartialEq, Eq)]
pub enum BlockPopStatus {
PoppedAndNonEmpty(Jamo),
PoppedAndEmpty(Jamo),
None,
}
impl BlockComposer {
pub fn new() -> Self {
BlockComposer {
state: BlockCompositionState::ExpectingInitial,
initial_first: None,
initial_second: None,
vowel_first: None,
vowel_second: None,
final_first: None,
final_second: None,
}
}
pub fn push(&mut self, letter: &Jamo) -> BlockPushResult {
match self.state {
BlockCompositionState::ExpectingInitial => self.try_push_initial(letter),
BlockCompositionState::ExpectingDoubleInitialOrVowel => {
self.try_push_double_initial_or_vowel(letter)
}
BlockCompositionState::ExpectingVowel => self.try_push_vowel(letter),
BlockCompositionState::ExpectingCompositeVowelOrFinal => {
self.try_push_composite_vowel_or_final(letter)
}
BlockCompositionState::ExpectingFinal => self.try_push_final(letter),
BlockCompositionState::ExpectingCompositeFinal => self.try_push_composite_final(letter),
BlockCompositionState::ExpectingNextBlock => self.try_push_next_block(letter),
}
}
pub fn push_char(&mut self, c: char) -> Result<BlockPushResult, BlockError> {
match Character::from_char(c)?.jamo() {
Some(jamo) => Ok(self.push(&jamo)),
None => Ok(BlockPushResult::NonHangul),
}
}
pub fn pop(&mut self) -> BlockPopStatus {
if let Some(c) = self.final_second.take() {
self.state = BlockCompositionState::ExpectingCompositeFinal;
BlockPopStatus::PoppedAndNonEmpty(c)
} else if let Some(c) = self.final_first.take() {
self.state = match self.vowel_second {
Some(_) => BlockCompositionState::ExpectingFinal,
None => BlockCompositionState::ExpectingCompositeVowelOrFinal,
};
BlockPopStatus::PoppedAndNonEmpty(c)
} else if let Some(c) = self.vowel_second.take() {
self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
BlockPopStatus::PoppedAndNonEmpty(c)
} else if let Some(c) = self.vowel_first.take() {
self.state = match self.initial_second {
Some(_) => BlockCompositionState::ExpectingVowel,
None => BlockCompositionState::ExpectingDoubleInitialOrVowel,
};
BlockPopStatus::PoppedAndNonEmpty(c)
} else if let Some(c) = self.initial_second.take() {
self.state = BlockCompositionState::ExpectingVowel;
BlockPopStatus::PoppedAndNonEmpty(c)
} else if let Some(c) = self.initial_first.take() {
self.state = BlockCompositionState::ExpectingInitial;
BlockPopStatus::PoppedAndEmpty(c)
} else {
self.state = BlockCompositionState::ExpectingInitial;
BlockPopStatus::None
}
}
pub(crate) fn pop_end_consonant(&mut self) -> Option<Jamo> {
if let Some(c) = self.final_second.take() {
Some(c)
} else if let Some(c) = self.final_first.take() {
Some(c)
} else {
None
}
}
fn try_push_initial(&mut self, letter: &Jamo) -> BlockPushResult {
match letter {
Jamo::Consonant(_) => {
self.initial_first = Some(letter.clone());
self.state = BlockCompositionState::ExpectingDoubleInitialOrVowel;
BlockPushResult::Success
}
Jamo::CompositeConsonant(c) => {
if c.is_valid_initial() {
self.initial_first = Some(letter.clone());
self.state = BlockCompositionState::ExpectingVowel;
BlockPushResult::Success
} else {
BlockPushResult::InvalidHangul
}
}
_ => BlockPushResult::InvalidHangul,
}
}
fn try_push_double_initial_or_vowel(&mut self, letter: &Jamo) -> BlockPushResult {
match letter {
Jamo::Consonant(c) => match &self.initial_first {
Some(Jamo::Consonant(i1)) => {
if i1.combine_for_initial(c).is_some() {
self.initial_second = Some(letter.clone());
self.state = BlockCompositionState::ExpectingVowel;
BlockPushResult::Success
} else {
BlockPushResult::InvalidHangul
}
}
_ => BlockPushResult::InvalidHangul,
},
Jamo::Vowel(_) => {
self.vowel_first = Some(letter.clone());
self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
BlockPushResult::Success
}
Jamo::CompositeVowel(c) => match c.decompose() {
(v1, v2) => {
self.vowel_first = Some(v1);
self.vowel_second = Some(v2);
self.state = BlockCompositionState::ExpectingFinal;
BlockPushResult::Success
}
},
Jamo::CompositeConsonant(_) => BlockPushResult::InvalidHangul,
}
}
fn try_push_vowel(&mut self, letter: &Jamo) -> BlockPushResult {
match letter {
Jamo::Vowel(_) => {
self.vowel_first = Some(letter.clone());
self.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
BlockPushResult::Success
}
Jamo::CompositeVowel(c) => match c.decompose() {
(v1, v2) => {
self.vowel_first = Some(v1);
self.vowel_second = Some(v2);
self.state = BlockCompositionState::ExpectingFinal;
BlockPushResult::Success
}
},
_ => BlockPushResult::InvalidHangul,
}
}
fn try_push_composite_vowel_or_final(&mut self, letter: &Jamo) -> BlockPushResult {
match letter {
Jamo::Vowel(c) => match &self.vowel_first {
Some(Jamo::Vowel(v1)) => {
if v1.combine(c).is_some() {
self.vowel_second = Some(letter.clone());
self.state = BlockCompositionState::ExpectingFinal;
BlockPushResult::Success
} else {
BlockPushResult::InvalidHangul
}
}
_ => BlockPushResult::InvalidHangul,
},
Jamo::Consonant(_) => {
self.final_first = Some(letter.clone());
self.state = BlockCompositionState::ExpectingCompositeFinal;
BlockPushResult::Success
}
Jamo::CompositeConsonant(c) => {
if c.is_valid_final() {
match c.decompose() {
(f1, f2) => {
self.final_first = Some(f1);
self.final_second = Some(f2);
self.state = BlockCompositionState::ExpectingNextBlock;
BlockPushResult::Success
}
}
} else if c.is_valid_initial() {
BlockPushResult::StartNewBlockNoPop
} else {
BlockPushResult::InvalidHangul
}
}
_ => BlockPushResult::InvalidHangul,
}
}
fn try_push_final(&mut self, letter: &Jamo) -> BlockPushResult {
match letter {
Jamo::Consonant(_) => {
self.final_first = Some(letter.clone());
self.state = BlockCompositionState::ExpectingCompositeFinal;
BlockPushResult::Success
}
Jamo::CompositeConsonant(c) => {
if c.is_valid_final() {
match c.decompose() {
(f1, f2) => {
self.final_first = Some(f1);
self.final_second = Some(f2);
self.state = BlockCompositionState::ExpectingNextBlock;
BlockPushResult::Success
}
}
} else if c.is_valid_initial() {
BlockPushResult::StartNewBlockNoPop
} else {
BlockPushResult::InvalidHangul
}
}
_ => BlockPushResult::InvalidHangul,
}
}
fn try_push_composite_final(&mut self, letter: &Jamo) -> BlockPushResult {
match letter {
Jamo::Consonant(c) => match &self.final_first {
Some(Jamo::Consonant(f1)) => {
if f1.combine_for_final(c).is_some() {
self.final_second = Some(letter.clone());
self.state = BlockCompositionState::ExpectingNextBlock;
BlockPushResult::Success
} else {
BlockPushResult::StartNewBlockNoPop
}
}
_ => BlockPushResult::InvalidHangul,
},
Jamo::CompositeConsonant(c) => {
if c.is_valid_initial() {
BlockPushResult::StartNewBlockNoPop
} else {
BlockPushResult::InvalidHangul
}
}
_ => BlockPushResult::PopAndStartNewBlock,
}
}
fn try_push_next_block(&mut self, letter: &Jamo) -> BlockPushResult {
match letter {
Jamo::Consonant(_) | Jamo::CompositeConsonant(_) => BlockPushResult::StartNewBlockNoPop,
Jamo::Vowel(_) | Jamo::CompositeVowel(_) => BlockPushResult::PopAndStartNewBlock,
}
}
pub fn try_as_complete_block(&self) -> Result<BlockCompletionStatus, BlockError> {
let initial_optional = match (&self.initial_first, &self.initial_second) {
(Some(Jamo::Consonant(i1)), Some(Jamo::Consonant(i2))) => {
match i1.combine_for_initial(&i2) {
Some(composite) => Some(Jamo::CompositeConsonant(composite)),
None => {
return Err(BlockError::JamoInInvalidPosition(
Jamo::Consonant(i2.clone()),
JamoPosition::Initial,
));
}
}
}
(Some(i1), None) => Some(i1.clone()),
_ => None,
};
let vowel_optional = match (&self.vowel_first, &self.vowel_second) {
(Some(Jamo::Vowel(v1)), Some(Jamo::Vowel(v2))) => match v1.combine(&v2) {
Some(composite) => Some(Jamo::CompositeVowel(composite)),
None => {
return Err(BlockError::JamoInInvalidPosition(
Jamo::Vowel(v2.clone()),
JamoPosition::Vowel,
));
}
},
(Some(v1), None) => Some(v1.clone()),
_ => None,
};
let final_optional = match (&self.final_first, &self.final_second) {
(Some(Jamo::Consonant(f1)), Some(Jamo::Consonant(f2))) => {
match f1.combine_for_final(&f2) {
Some(composite) => Some(Jamo::CompositeConsonant(composite)),
None => {
return Err(BlockError::JamoInInvalidPosition(
Jamo::Consonant(f2.clone()),
JamoPosition::Final,
));
}
}
}
(Some(f1), None) => Some(f1.clone()),
_ => None,
};
match (initial_optional, vowel_optional) {
(Some(initial), Some(vowel)) => Ok(BlockCompletionStatus::Complete(HangulBlock {
initial,
vowel,
final_optional,
})),
(Some(initial), None) => Ok(BlockCompletionStatus::Incomplete(initial)),
(None, Some(vowel)) => Ok(BlockCompletionStatus::Incomplete(vowel)),
(None, None) => match final_optional {
Some(f) => Ok(BlockCompletionStatus::Incomplete(f)),
None => Ok(BlockCompletionStatus::Empty),
},
}
}
pub fn block_as_string(&self) -> Result<Option<char>, BlockError> {
match self.try_as_complete_block()? {
BlockCompletionStatus::Complete(block) => match block.to_char()? {
c => Ok(Some(c)),
},
BlockCompletionStatus::Incomplete(c) => Ok(c.char_modern(match c {
Jamo::Consonant(_) | Jamo::CompositeConsonant(_) => JamoPosition::Initial,
Jamo::Vowel(_) | Jamo::CompositeVowel(_) => JamoPosition::Vowel,
})),
BlockCompletionStatus::Empty => Ok(None),
}
}
pub fn from_composed_block(block: &HangulBlock) -> Result<Self, BlockError> {
let mut result = BlockComposer::new();
let (i1, i2, v1, v2, f1, f2) = block.decomposed_tuple()?;
if f2.is_some() {
result.state = BlockCompositionState::ExpectingNextBlock;
} else if f1.is_some() {
result.state = BlockCompositionState::ExpectingCompositeFinal;
} else if v2.is_some() {
result.state = BlockCompositionState::ExpectingFinal;
} else if v1.is_some() {
result.state = BlockCompositionState::ExpectingCompositeVowelOrFinal;
}
else if i2.is_some() {
result.state = BlockCompositionState::ExpectingVowel;
} else if i1.is_some() {
result.state = BlockCompositionState::ExpectingDoubleInitialOrVowel;
} else {
result.state = BlockCompositionState::ExpectingInitial;
}
result.initial_first = i1;
result.initial_second = i2;
result.vowel_first = v1;
result.vowel_second = v2;
result.final_first = f1;
result.final_second = f2;
Ok(result)
}
}
pub fn hangul_blocks_vec_to_string(blocks: &Vec<HangulBlock>) -> Result<String, BlockError> {
let mut result = String::new();
for block in blocks {
result.push(block.to_char()?);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hangul_block_to_char() {
let block = HangulBlock {
initial: Jamo::from_compatibility_jamo('ㄱ').unwrap(),
vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
final_optional: Some(Jamo::from_compatibility_jamo('ㄴ').unwrap()),
};
let result = block.to_char();
assert_eq!(result, Ok('간'));
let block_no_final = HangulBlock {
initial: Jamo::from_compatibility_jamo('ㅂ').unwrap(),
vowel: Jamo::from_compatibility_jamo('ㅗ').unwrap(),
final_optional: None,
};
let result_no_final = block_no_final.to_char();
assert_eq!(result_no_final, Ok('보'));
}
#[test]
fn test_hangul_blocks_vec_to_string() {
let blocks = vec![
HangulBlock {
initial: Jamo::from_compatibility_jamo('ㅇ').unwrap(),
vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
final_optional: Some(Jamo::from_compatibility_jamo('ㄴ').unwrap()),
},
HangulBlock {
initial: Jamo::from_compatibility_jamo('ㄴ').unwrap(),
vowel: Jamo::from_compatibility_jamo('ㅕ').unwrap(),
final_optional: Some(Jamo::from_compatibility_jamo('ㅇ').unwrap()),
},
HangulBlock {
initial: Jamo::from_compatibility_jamo('ㅎ').unwrap(),
vowel: Jamo::from_compatibility_jamo('ㅏ').unwrap(),
final_optional: None,
},
HangulBlock {
initial: Jamo::from_compatibility_jamo('ㅅ').unwrap(),
vowel: Jamo::from_compatibility_jamo('ㅔ').unwrap(),
final_optional: None,
},
HangulBlock {
initial: Jamo::from_compatibility_jamo('ㅇ').unwrap(),
vowel: Jamo::from_compatibility_jamo('ㅛ').unwrap(),
final_optional: None,
},
];
let result = hangul_blocks_vec_to_string(&blocks);
assert_eq!(result, Ok("안녕하세요".to_string()));
}
struct BlockComposerPushTestCase {
input: Vec<Jamo>,
expected_final_word_state: BlockPushResult,
expected_final_block_state: BlockCompositionState,
}
fn run_test_cases(cases: Vec<BlockComposerPushTestCase>) {
for case in &cases {
let mut composer = BlockComposer::new();
let mut final_word_state = BlockPushResult::Success;
for letter in &case.input {
final_word_state = composer.push(letter);
}
assert_eq!(
final_word_state, case.expected_final_word_state,
"Final WORD state did not match expected. Composer: {:?}",
composer
);
assert_eq!(
composer.state, case.expected_final_block_state,
"Final BLOCK state did not match expected. Composer: {:?}",
composer
);
}
}
#[test]
fn single_block_composition_valid() {
let test_cases: Vec<BlockComposerPushTestCase> = vec![
BlockComposerPushTestCase {
input: vec![Jamo::from_compatibility_jamo('ㄱ').unwrap()],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingDoubleInitialOrVowel,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingVowel,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㅜ').unwrap(),
],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingCompositeVowelOrFinal,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㅜ').unwrap(),
Jamo::from_compatibility_jamo('ㅓ').unwrap(),
],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingFinal,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㅜ').unwrap(),
Jamo::from_compatibility_jamo('ㅓ').unwrap(),
Jamo::from_compatibility_jamo('ㄹ').unwrap(),
],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingCompositeFinal,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㅜ').unwrap(),
Jamo::from_compatibility_jamo('ㅓ').unwrap(),
Jamo::from_compatibility_jamo('ㄹ').unwrap(),
Jamo::from_compatibility_jamo('ㅎ').unwrap(),
],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㅜ').unwrap(),
Jamo::from_compatibility_jamo('ㅓ').unwrap(),
Jamo::from_compatibility_jamo('ㄹ').unwrap(),
Jamo::from_compatibility_jamo('ㅎ').unwrap(),
Jamo::from_compatibility_jamo('ㅏ').unwrap(),
],
expected_final_word_state: BlockPushResult::PopAndStartNewBlock,
expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㅃ').unwrap(),
Jamo::from_compatibility_jamo('ㅣ').unwrap(),
Jamo::from_compatibility_jamo('ㄳ').unwrap(),
],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㅈ').unwrap(),
Jamo::from_compatibility_jamo('ㅚ').unwrap(),
],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingFinal,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㅉ').unwrap(),
Jamo::from_compatibility_jamo('ㅢ').unwrap(),
Jamo::from_compatibility_jamo('ㅃ').unwrap(),
],
expected_final_word_state: BlockPushResult::StartNewBlockNoPop,
expected_final_block_state: BlockCompositionState::ExpectingFinal,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㅇ').unwrap(),
Jamo::from_compatibility_jamo('ㅣ').unwrap(),
Jamo::from_compatibility_jamo('ㅅ').unwrap(),
Jamo::from_compatibility_jamo('ㅅ').unwrap(),
],
expected_final_word_state: BlockPushResult::Success,
expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㅇ').unwrap(),
Jamo::from_compatibility_jamo('ㅣ').unwrap(),
Jamo::from_compatibility_jamo('ㅅ').unwrap(),
Jamo::from_compatibility_jamo('ㅅ').unwrap(),
Jamo::from_compatibility_jamo('ㅅ').unwrap(),
],
expected_final_word_state: BlockPushResult::StartNewBlockNoPop,
expected_final_block_state: BlockCompositionState::ExpectingNextBlock,
},
];
run_test_cases(test_cases);
}
#[test]
fn single_block_composition_invalid() {
let test_cases: Vec<BlockComposerPushTestCase> = vec![
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㄹ').unwrap(),
],
expected_final_word_state: BlockPushResult::InvalidHangul,
expected_final_block_state: BlockCompositionState::ExpectingDoubleInitialOrVowel,
},
BlockComposerPushTestCase {
input: vec![
Jamo::from_compatibility_jamo('ㄱ').unwrap(),
Jamo::from_compatibility_jamo('ㅏ').unwrap(),
Jamo::from_compatibility_jamo('ㅏ').unwrap(),
],
expected_final_word_state: BlockPushResult::InvalidHangul,
expected_final_block_state: BlockCompositionState::ExpectingCompositeVowelOrFinal,
},
];
run_test_cases(test_cases);
}
#[derive(Debug)]
struct BlockE2ETestCase((char, char, char, char));
fn run_e2e_test_cases(case: BlockE2ETestCase) {
let from_block_char = HangulBlock::from_char(case.0.3).unwrap();
assert_eq!(
from_block_char.initial,
Jamo::from_compatibility_jamo(case.0.0).unwrap(),
"Initial consonant did not match expected for case {:?}",
case
);
assert_eq!(
from_block_char.vowel,
Jamo::from_compatibility_jamo(case.0.1).unwrap(),
"Vowel did not match expected for case {:?}",
case
);
if case.0.2 != '\0' {
assert_eq!(
from_block_char.final_optional.unwrap(),
Jamo::from_compatibility_jamo(case.0.2).unwrap(),
"Final consonant did not match expected for case {:?}",
case
);
} else {
assert!(
from_block_char.final_optional.is_none(),
"Final consonant was expected to be None for case {:?}",
case
);
}
}
#[test]
fn test_valid_blocks_e2e() {
let case_tuples: Vec<(char, char, char, char)> = vec![
('ㅂ', 'ㅛ', '\0', '뵤'),
('ㅈ', 'ㅕ', '\0', '져'),
('ㄷ', 'ㅑ', '\0', '댜'),
('ㄱ', 'ㅐ', '\0', '개'),
('ㅅ', 'ㅔ', '\0', '세'),
('ㅁ', 'ㅗ', '\0', '모'),
('ㄴ', 'ㅓ', '\0', '너'),
('ㅇ', 'ㅏ', '\0', '아'),
('ㅎ', 'ㅣ', '\0', '히'),
('ㅋ', 'ㅠ', '\0', '큐'),
('ㅌ', 'ㅜ', '\0', '투'),
('ㅊ', 'ㅡ', '\0', '츠'),
('ㄹ', 'ㅒ', '\0', '럐'),
('ㅍ', 'ㅖ', '\0', '폐'),
('ㅃ', 'ㅛ', '\0', '뾰'),
('ㅉ', 'ㅕ', '\0', '쪄'),
('ㄸ', 'ㅑ', '\0', '땨'),
('ㄲ', 'ㅐ', '\0', '깨'),
('ㅆ', 'ㅔ', '\0', '쎄'),
('ㅂ', 'ㅘ', '\0', '봐'),
('ㅈ', 'ㅙ', '\0', '좨'),
('ㄷ', 'ㅚ', '\0', '되'),
('ㄱ', 'ㅝ', '\0', '궈'),
('ㅅ', 'ㅞ', '\0', '쉐'),
('ㅁ', 'ㅟ', '\0', '뮈'),
('ㄴ', 'ㅢ', '\0', '늬'),
('ㅂ', 'ㅛ', 'ㅆ', '뵸'),
('ㅈ', 'ㅕ', 'ㄲ', '젺'),
('ㄷ', 'ㅑ', 'ㄳ', '댟'),
('ㄱ', 'ㅐ', 'ㄵ', '갡'),
('ㅅ', 'ㅔ', 'ㄶ', '섾'),
('ㅁ', 'ㅗ', 'ㄺ', '몱'),
('ㄴ', 'ㅓ', 'ㄻ', '넒'),
('ㅇ', 'ㅏ', 'ㄼ', '앏'),
('ㅎ', 'ㅣ', 'ㄽ', '힔'),
('ㅋ', 'ㅠ', 'ㄾ', '큝'),
('ㅌ', 'ㅜ', 'ㄿ', '툺'),
('ㅊ', 'ㅡ', 'ㅀ', '츯'),
('ㄹ', 'ㅒ', 'ㅄ', '럢'),
('ㅍ', 'ㅖ', 'ㅂ', '폡'),
('ㅃ', 'ㅛ', 'ㅈ', '뿆'),
('ㅉ', 'ㅕ', 'ㄷ', '쪋'),
('ㄸ', 'ㅑ', 'ㄱ', '땩'),
('ㄲ', 'ㅐ', 'ㅅ', '깻'),
('ㅆ', 'ㅔ', 'ㅁ', '쎔'),
('ㅂ', 'ㅘ', 'ㄴ', '봔'),
('ㅈ', 'ㅙ', 'ㅇ', '좽'),
('ㄷ', 'ㅚ', 'ㄹ', '될'),
('ㄱ', 'ㅝ', 'ㅋ', '궠'),
('ㅅ', 'ㅞ', 'ㅌ', '쉩'),
('ㅁ', 'ㅟ', 'ㅊ', '뮟'),
('ㄴ', 'ㅢ', 'ㅍ', '닆'),
];
for tuple in case_tuples {
run_e2e_test_cases(BlockE2ETestCase(tuple));
}
}
#[test]
fn test_decompose_vec_decompose_composites() {
let block = HangulBlock::from_char('값').unwrap();
let options = HangulBlockDecompositionOptions {
decompose_composites: true,
jamo_era: JamoUnicodeType::Modern,
};
let decomposed = block.decomposed_vec(&options).unwrap();
let expected = vec!['ᄀ', 'ᅡ', 'ᆸ', 'ᆺ'];
assert_eq!(decomposed, expected);
}
#[test]
fn test_decompose_vec_no_decompose_composites() {
let block = HangulBlock::from_char('값').unwrap();
let options = HangulBlockDecompositionOptions {
decompose_composites: false,
jamo_era: JamoUnicodeType::Compatibility,
};
let decomposed = block.decomposed_vec(&options).unwrap();
let expected = vec!['ㄱ', 'ㅏ', 'ㅄ'];
assert_eq!(decomposed, expected);
}
}