use crate::char_struct::CharType;
use crate::jauem::choseong::encode_choseong;
use crate::moeum::jungsong::encode_jungsong;
use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
use crate::split::split_korean_jauem;
use crate::utils::has_choseong_o;
pub static META: RuleMeta = RuleMeta {
section: "14",
subsection: None,
name: "no_abbrev_before_vowel",
standard_ref: "2024 Korean Braille Standard, Ch.2 Sec.6 Art.14",
description: "나,다,마,바,자,카,타,파,하 followed by vowel-initial syllable: no abbreviation",
};
pub const NO_ABBREV_SYLLABLES: [char; 9] = ['나', '다', '마', '바', '자', '카', '타', '파', '하'];
const NO_ABBREV_DOUBLE_BASES: [char; 3] = ['다', '바', '자'];
#[cfg(test)]
fn should_suppress_abbreviation(current: char, next_has_choseong_o: bool) -> bool {
is_no_abbrev_target(current) && next_has_choseong_o
}
pub fn is_no_abbrev_target(ch: char) -> bool {
if NO_ABBREV_SYLLABLES.contains(&ch) {
return true;
}
let code = ch as u32;
if !(0xAC00..=0xD7A3).contains(&code) {
return false;
}
let uni = code - 0xAC00;
let cho_idx = (uni / 588) as usize;
let jung_idx = ((uni - (cho_idx as u32 * 588)) / 28) as usize;
let jong_idx = (uni % 28) as usize;
if jong_idx != 0 {
return false;
}
if jung_idx != 0 {
return false;
}
const CHOSEONG: [char; 19] = [
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ',
'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ',
];
let cho = CHOSEONG[cho_idx];
if let Ok((cho0, Some(cho1))) = split_korean_jauem(cho)
&& cho0 == cho1
{
if let Some(simple_cho_idx) = CHOSEONG.iter().position(|c| *c == cho0) {
let simple_uni = (simple_cho_idx as u32) * 588;
let simple_char = char::from_u32(0xAC00 + simple_uni).unwrap_or('가');
return NO_ABBREV_DOUBLE_BASES.contains(&simple_char);
}
}
false
}
pub struct Rule14;
impl BrailleRule for Rule14 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn priority(&self) -> u16 {
80 }
fn matches(&self, ctx: &RuleContext) -> bool {
if !matches!(ctx.char_type, CharType::Korean(_)) {
return false;
}
if !is_no_abbrev_target(ctx.current_char()) {
return false;
}
ctx.index < ctx.word_chars.len() - 1 && has_choseong_o(ctx.word_chars[ctx.index + 1])
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
let CharType::Korean(korean) = ctx.char_type else {
return Ok(RuleResult::Skip);
};
let (cho0, cho1) = split_korean_jauem(korean.cho)?;
if cho1.is_some() {
ctx.emit(32);
}
let cho_code = encode_choseong(cho0)?;
ctx.emit(cho_code);
ctx.emit_slice(encode_jungsong(korean.jung)?);
Ok(RuleResult::Consumed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifies_all_target_syllables() {
for &ch in &NO_ABBREV_SYLLABLES {
assert!(is_no_abbrev_target(ch), "Expected {} to be target", ch);
}
}
#[test]
fn ga_is_not_target() {
assert!(!is_no_abbrev_target('가'));
}
#[rstest::rstest]
#[case('A')]
#[case('1')]
#[case(' ')]
fn is_no_abbrev_target_non_hangul_returns_false(#[case] ch: char) {
assert!(!is_no_abbrev_target(ch));
}
#[test]
fn suppresses_when_next_is_vowel_initial() {
assert!(should_suppress_abbreviation('나', true));
assert!(should_suppress_abbreviation('다', true));
assert!(should_suppress_abbreviation('하', true));
}
#[test]
fn does_not_suppress_when_next_is_consonant_initial() {
assert!(!should_suppress_abbreviation('나', false));
assert!(!should_suppress_abbreviation('하', false));
}
#[test]
fn does_not_suppress_for_non_target() {
assert!(!should_suppress_abbreviation('가', true));
assert!(!should_suppress_abbreviation('곤', true));
}
#[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 14 golden test failed for: {}",
input
);
}
}
use rstest::rstest;
#[rstest]
#[case("나아", true)] #[case("다어", true)]
#[case("자아", true)]
#[case("하이", true)]
#[case("가나", false)] #[case("나람", false)] #[case("A", false)] fn rule14_matches_target_then_o_initial(#[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!(Rule14.matches(&ctx), expected, "input={input}");
}
#[test]
fn rule14_apply_emits_for_target() {
let mut owned = crate::test_helpers::CtxOwned::for_text("나아", false);
let mut ctx = owned.ctx_at(0);
let _ = Rule14.apply(&mut ctx).unwrap();
assert!(!owned.result.is_empty());
}
#[test]
fn rule14_apply_skips_non_korean() {
let mut owned = crate::test_helpers::CtxOwned::for_text("A", false);
let mut ctx = owned.ctx_at(0);
let outcome = Rule14.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Skip));
}
}