use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
use crate::split;
pub static META: RuleMeta = RuleMeta {
section: "2",
subsection: None,
name: "double_choseong",
standard_ref: "2024 Korean Braille Standard, Ch.1 Sec.1 Art.2",
description: "Double consonants (ㄲ,ㄸ,ㅃ,ㅆ,ㅉ) as choseong: 된소리표 ⠠ + base consonant",
};
const DOUBLE_CONSONANT_INDICATOR: u8 = 32;
pub const DOUBLE_CHOSEONG: [char; 5] = ['ㄲ', 'ㄸ', 'ㅃ', 'ㅆ', 'ㅉ'];
pub fn is_double_choseong(cho: char) -> bool {
DOUBLE_CHOSEONG.contains(&cho)
}
pub fn decompose(cho: char) -> Option<(u8, char)> {
if !is_double_choseong(cho) {
return None;
}
let (base, _) = split::split_korean_jauem(cho).ok()?;
Some((DOUBLE_CONSONANT_INDICATOR, base))
}
pub struct Rule2;
impl BrailleRule for Rule2 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn priority(&self) -> u16 {
195 }
fn matches(&self, ctx: &RuleContext) -> bool {
ctx.as_korean().is_some_and(|k| is_double_choseong(k.cho))
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
let Some(korean) = ctx.as_korean() else {
return Ok(RuleResult::Skip);
};
if let Some((indicator, _base)) = decompose(korean.cho) {
ctx.emit(indicator);
}
Ok(RuleResult::Continue) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifies_all_double_consonants() {
assert!(is_double_choseong('ㄲ'));
assert!(is_double_choseong('ㄸ'));
assert!(is_double_choseong('ㅃ'));
assert!(is_double_choseong('ㅆ'));
assert!(is_double_choseong('ㅉ'));
}
#[test]
fn rejects_single_consonants() {
assert!(!is_double_choseong('ㄱ'));
assert!(!is_double_choseong('ㄷ'));
assert!(!is_double_choseong('ㅂ'));
assert!(!is_double_choseong('ㅅ'));
assert!(!is_double_choseong('ㅈ'));
}
#[test]
fn decomposes_correctly() {
assert_eq!(decompose('ㄲ'), Some((32, 'ㄱ')));
assert_eq!(decompose('ㄸ'), Some((32, 'ㄷ')));
assert_eq!(decompose('ㅃ'), Some((32, 'ㅂ')));
assert_eq!(decompose('ㅆ'), Some((32, 'ㅅ')));
assert_eq!(decompose('ㅉ'), Some((32, 'ㅈ')));
}
#[test]
fn decompose_returns_none_for_single() {
assert_eq!(decompose('ㄱ'), None);
assert_eq!(decompose('ㅎ'), None);
}
#[test]
fn golden_test_alignment() {
let cases = vec![("꾸러미", "⠠⠈⠍⠐⠎⠑⠕"), ("쓰기", "⠠⠠⠪⠈⠕")];
for (input, expected) in cases {
let result = crate::encode_to_unicode(input).unwrap();
assert_eq!(result, expected, "Rule 2 golden test failed for: {}", input);
}
}
}