pub const SBASE: u32 = 0xAC00;
pub const LBASE: u32 = 0x1100;
pub const VBASE: u32 = 0x1161;
pub const TBASE: u32 = 0x11A7;
pub const LCOUNT: u32 = 19;
pub const VCOUNT: u32 = 21;
pub const TCOUNT: u32 = 28;
pub const SCOUNT: u32 = LCOUNT * VCOUNT * TCOUNT;
pub const CHO: [u32; 19] = [
0x1100, 0x1101, 0x1102, 0x1103, 0x1104, 0x1105, 0x1106, 0x1107, 0x1108, 0x1109, 0x110A, 0x110B,
0x110C, 0x110D, 0x110E, 0x110F, 0x1110, 0x1111, 0x1112,
];
pub const JUNG: [u32; 21] = [
0x1161, 0x1162, 0x1163, 0x1164, 0x1165, 0x1166, 0x1167, 0x1168, 0x1169, 0x116A, 0x116B, 0x116C,
0x116D, 0x116E, 0x116F, 0x1170, 0x1171, 0x1172, 0x1173, 0x1174, 0x1175,
];
pub const JONG: [u32; 28] = [
0x0000, 0x11A8, 0x11A9, 0x11AA, 0x11AB, 0x11AC, 0x11AD, 0x11AE, 0x11AF, 0x11B0, 0x11B1, 0x11B2, 0x11B3,
0x11B4, 0x11B5, 0x11B6, 0x11B7, 0x11B8, 0x11B9, 0x11BA, 0x11BB, 0x11BC, 0x11BD, 0x11BE, 0x11BF,
0x11C0, 0x11C1, 0x11C2,
];
pub fn cho_index(cp: u32) -> Option<u8> {
(LBASE..=0x1112).contains(&cp).then(|| (cp - LBASE) as u8)
}
pub fn jung_index(cp: u32) -> Option<u8> {
(VBASE..=0x1175).contains(&cp).then(|| (cp - VBASE) as u8)
}
pub fn jong_index(cp: u32) -> Option<u8> {
(0x11A8..=0x11C2).contains(&cp).then(|| (cp - TBASE) as u8)
}
pub fn compose_indices(cho: u8, jung: u8, jong: u8) -> Option<char> {
if (cho as u32) >= LCOUNT || (jung as u32) >= VCOUNT || (jong as u32) >= TCOUNT {
return None;
}
let s = SBASE + ((cho as u32 * VCOUNT) + jung as u32) * TCOUNT + jong as u32;
char::from_u32(s)
}
pub fn compose(cho_cp: u32, jung_cp: u32, jong_cp: Option<u32>) -> Option<char> {
let cho = cho_index(cho_cp)?;
let jung = jung_index(jung_cp)?;
let jong = match jong_cp {
None | Some(0) => 0,
Some(cp) => jong_index(cp)?,
};
compose_indices(cho, jung, jong)
}
pub fn decompose(syllable: char) -> Option<(u32, u32, Option<u32>)> {
let s = syllable as u32;
if !(SBASE..SBASE + SCOUNT).contains(&s) {
return None;
}
let idx = s - SBASE;
let cho = idx / (VCOUNT * TCOUNT);
let jung = (idx % (VCOUNT * TCOUNT)) / TCOUNT;
let jong = idx % TCOUNT;
Some((
CHO[cho as usize],
JUNG[jung as usize],
(jong != 0).then(|| JONG[jong as usize]),
))
}
pub fn is_conjoining_jamo(cp: u32) -> bool {
(0x1100..=0x11FF).contains(&cp) || (0xA960..=0xA97F).contains(&cp) || (0xD7B0..=0xD7FF).contains(&cp) }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn precompose_basic() {
assert_eq!(compose(0x1100, 0x1161, None), Some('가'));
assert_eq!(compose(0x1100, 0x1161, Some(0x11A8)), Some('각'));
assert_eq!(compose(0x1112, 0x1161, Some(0x11AB)), Some('한'));
assert_eq!(compose(0x1100, 0x116A, None), Some('과'));
assert_eq!(compose(0x1101, 0x1161, None), Some('까'));
}
#[test]
fn precompose_indices_match_formula() {
assert_eq!(compose_indices(0, 0, 0), Some('가'));
assert_eq!(compose_indices(18, 20, 27), Some('힣'));
assert_eq!(SBASE + SCOUNT - 1, '힣' as u32);
}
#[test]
fn roundtrip_decompose() {
for ch in ['가', '각', '한', '뷁', '힣', '꿈', '워', '의'] {
let (c, j, t) = decompose(ch).unwrap();
assert_eq!(compose(c, j, t), Some(ch), "roundtrip {ch}");
}
}
#[test]
fn index_lookups() {
assert_eq!(cho_index(0x1100), Some(0));
assert_eq!(cho_index(0x1112), Some(18));
assert_eq!(cho_index(0x1113), None);
assert_eq!(jung_index(0x1161), Some(0));
assert_eq!(jung_index(0x1175), Some(20));
assert_eq!(jong_index(0x11A8), Some(1));
assert_eq!(jong_index(0x11C2), Some(27));
assert_eq!(jong_index(0x1100), None);
}
#[test]
fn non_modern_has_no_precompose() {
assert_eq!(compose(0x114C, 0x1161, None), None);
}
}