const HANGUL_BASE: u32 = 0xAC00;
const HANGUL_END: u32 = 0xD7A3;
const JUNGSEONG_COUNT: u32 = 21;
const JONGSEONG_COUNT: u32 = 28;
static CHOSEONG: &[&str] = &[
"g", "kk", "n", "d", "tt", "r", "m", "b", "pp", "s", "ss", "", "j", "jj", "ch", "k", "t", "p", "h", ];
static JUNGSEONG: &[&str] = &[
"a", "ae", "ya", "yae", "eo", "e", "yeo", "ye", "o", "wa", "wae", "oe", "yo", "u", "wo", "we", "wi", "yu", "eu", "ui", "i", ];
static JONGSEONG: &[&str] = &[
"", "g", "kk", "gs", "n", "nj", "nh", "d", "l", "lg", "lm", "lb", "ls", "lt", "lp", "lh", "m", "b", "bs", "s", "ss", "ng", "j", "ch", "k", "t", "p", "h", ];
const COMPAT_JAMO_BASE: u32 = 0x3131;
const COMPAT_JAMO_END: u32 = 0x3163;
static COMPAT_JAMO: &[&str] = &[
"g", "kk", "gs", "n", "nj", "nh", "d", "tt", "r", "lg", "lm", "lb", "ls", "lt", "lp", "lh", "m", "b", "pp", "bs", "s", "ss", "", "j", "jj", "ch", "k", "t", "p", "h", "a", "ae", "ya", "yae", "eo", "e", "yeo", "ye", "o", "wa", "wae", "oe", "yo", "u", "wo", "we", "wi", "yu", "eu", "ui", "i", ];
pub fn lookup_compat_jamo(ch: char) -> Option<&'static str> {
let cp = ch as u32;
if (COMPAT_JAMO_BASE..=COMPAT_JAMO_END).contains(&cp) {
Some(COMPAT_JAMO[(cp - COMPAT_JAMO_BASE) as usize])
} else {
None
}
}
pub fn romanize_hangul(ch: char) -> Option<String> {
let code = ch as u32;
if (HANGUL_BASE..=HANGUL_END).contains(&code) {
let index = code - HANGUL_BASE;
let cho = (index / (JUNGSEONG_COUNT * JONGSEONG_COUNT)) as usize;
let jung = ((index % (JUNGSEONG_COUNT * JONGSEONG_COUNT)) / JONGSEONG_COUNT) as usize;
let jong = (index % JONGSEONG_COUNT) as usize;
let mut result = String::with_capacity(8);
result.push_str(CHOSEONG[cho]);
result.push_str(JUNGSEONG[jung]);
result.push_str(JONGSEONG[jong]);
return Some(result);
}
lookup_compat_jamo(ch).map(std::string::ToString::to_string)
}
const _: () = assert!(JUNGSEONG_COUNT == 21);
const _: () = assert!(JONGSEONG_COUNT == 28);
const _: () = assert!(HANGUL_END - HANGUL_BASE + 1 == 19 * JUNGSEONG_COUNT * JONGSEONG_COUNT);
const _: () = assert!(COMPAT_JAMO_END - COMPAT_JAMO_BASE + 1 == 51);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jamo_array_lengths() {
assert_eq!(
CHOSEONG.len(),
19,
"CHOSEONG must have 19 initial consonants"
);
assert_eq!(JUNGSEONG.len(), 21, "JUNGSEONG must have 21 medial vowels");
assert_eq!(
JONGSEONG.len(),
28,
"JONGSEONG must have 28 final consonants"
);
assert_eq!(COMPAT_JAMO.len(), 51, "COMPAT_JAMO must have 51 entries");
}
#[test]
fn test_all_jamo_entries_are_ascii() {
for (i, entry) in CHOSEONG.iter().enumerate() {
assert!(entry.is_ascii(), "CHOSEONG[{i}] = {entry:?} is not ASCII");
}
for (i, entry) in JUNGSEONG.iter().enumerate() {
assert!(entry.is_ascii(), "JUNGSEONG[{i}] = {entry:?} is not ASCII");
}
for (i, entry) in JONGSEONG.iter().enumerate() {
assert!(entry.is_ascii(), "JONGSEONG[{i}] = {entry:?} is not ASCII");
}
for (i, entry) in COMPAT_JAMO.iter().enumerate() {
assert!(
entry.is_ascii(),
"COMPAT_JAMO[{i}] = {entry:?} is not ASCII"
);
}
}
#[test]
fn test_hangul_basic() {
assert_eq!(romanize_hangul('한'), Some("han".to_string()));
assert_eq!(romanize_hangul('글'), Some("geul".to_string()));
}
#[test]
fn test_hangul_no_final() {
assert_eq!(romanize_hangul('가'), Some("ga".to_string()));
}
#[test]
fn test_hangul_seoul() {
assert_eq!(romanize_hangul('서'), Some("seo".to_string()));
assert_eq!(romanize_hangul('울'), Some("ul".to_string()));
}
#[test]
fn test_non_hangul_returns_none() {
assert_eq!(romanize_hangul('A'), None);
assert_eq!(romanize_hangul('北'), None);
}
#[test]
fn test_compat_jamo() {
assert_eq!(romanize_hangul('ㄱ'), Some("g".to_string()));
assert_eq!(romanize_hangul('ㅏ'), Some("a".to_string()));
}
}