use crate::jauem::choseong;
use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
pub static META: RuleMeta = RuleMeta {
section: "1",
subsection: None,
name: "basic_choseong",
standard_ref: "2024 Korean Braille Standard, Ch.1 Sec.1 Art.1",
description: "Encode 13 basic initial consonants (choseong) to braille",
};
#[cfg(test)]
fn apply(cho: char) -> Result<u8, String> {
choseong::encode_choseong(cho)
}
pub fn is_silent_ieung(cho: char) -> bool {
cho == 'ㅇ'
}
pub struct Rule1;
impl BrailleRule for Rule1 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn priority(&self) -> u16 {
200 }
fn matches(&self, ctx: &RuleContext) -> bool {
ctx.as_korean().is_some()
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
let Some(korean) = ctx.as_korean() else {
return Ok(RuleResult::Skip);
};
if !is_silent_ieung(korean.cho) {
let code = choseong::encode_choseong(korean.cho)?;
ctx.emit(code);
}
Ok(RuleResult::Continue) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::unicode::decode_unicode;
#[rstest::rstest]
#[case::giyeok('ㄱ', '⠈')]
#[case::nieun('ㄴ', '⠉')]
#[case::digeut('ㄷ', '⠊')]
#[case::rieul('ㄹ', '⠐')]
#[case::mieum('ㅁ', '⠑')]
#[case::bieup('ㅂ', '⠘')]
#[case::siot('ㅅ', '⠠')]
#[case::jieut('ㅈ', '⠨')]
#[case::chieut('ㅊ', '⠰')]
#[case::kieuk('ㅋ', '⠋')]
#[case::tieut('ㅌ', '⠓')]
#[case::pieup('ㅍ', '⠙')]
#[case::hieut('ㅎ', '⠚')]
fn encodes_all_13_basic_consonants(#[case] cho: char, #[case] expected_braille: char) {
let result = apply(cho).unwrap();
assert_eq!(
result,
decode_unicode(expected_braille),
"Failed for choseong: {cho}"
);
}
#[test]
fn ieung_is_not_in_choseong_map() {
assert!(apply('ㅇ').is_err());
}
#[test]
fn silent_ieung_detected() {
assert!(is_silent_ieung('ㅇ'));
assert!(!is_silent_ieung('ㄱ'));
}
#[test]
fn invalid_char_returns_error() {
assert!(apply('A').is_err());
assert!(apply('가').is_err());
}
#[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 1 golden test failed for: {}", input);
}
}
use rstest::rstest;
#[rstest]
#[case("가", true)]
#[case("힣", true)]
#[case("A", false)]
#[case("1", false)]
#[case("ㄱ", false)] fn rule1_matches_only_for_korean_syllables(#[case] input: &str, #[case] expected: bool) {
let mut owned = crate::test_helpers::CtxOwned::for_text(input, false);
let ctx = owned.ctx_at(0);
assert_eq!(Rule1.matches(&ctx), expected, "input={input}");
}
#[rstest]
#[case("가", false)] #[case("나", false)] #[case("아", true)] #[case("어", true)] fn rule1_apply_handles_silent_ieung(#[case] input: &str, #[case] silent: bool) {
let mut owned = crate::test_helpers::CtxOwned::for_text(input, false);
let mut ctx = owned.ctx_at(0);
let outcome = Rule1.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Continue));
if silent {
assert!(
owned.result.is_empty(),
"ㅇ should emit nothing for {input}"
);
} else {
assert!(!owned.result.is_empty(), "non-ㅇ should emit for {input}");
}
}
#[test]
fn rule1_apply_skips_non_korean() {
let mut owned = crate::test_helpers::CtxOwned::for_text("A", false);
let mut ctx = owned.ctx_at(0);
let outcome = Rule1.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Skip));
}
#[test]
fn rule1_meta_phase_priority() {
assert_eq!(Rule1.meta().section, "1");
assert!(matches!(Rule1.phase(), Phase::CoreEncoding));
assert_eq!(Rule1.priority(), 200);
}
}