use crate::char_struct::CharType;
use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
pub static META: RuleMeta = RuleMeta {
section: "44",
subsection: Some("b1"),
name: "number_korean_spacing",
standard_ref: "2024 Korean Braille Standard, Ch.5 Sec.11 Art.44 [다만]",
description: "Insert space between number and confusable Korean choseong",
};
const CONFUSABLE_CHOSEONG: [char; 7] = ['ㄴ', 'ㄷ', 'ㅁ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
pub(crate) fn is_number_confusable_korean_char(ch: char) -> bool {
matches!(
CharType::new(ch),
Ok(CharType::Korean(korean))
if CONFUSABLE_CHOSEONG.contains(&korean.cho) || ch == '운'
)
}
pub struct Rule44;
impl BrailleRule for Rule44 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn priority(&self) -> u16 {
50 }
fn matches(&self, ctx: &RuleContext) -> bool {
if !ctx.state.is_number {
return false;
}
let CharType::Korean(_) = ctx.char_type else {
return false;
};
is_number_confusable_korean_char(ctx.current_char())
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
let middle_dot_adjacent = ctx.prev_char() == Some('·');
if middle_dot_adjacent {
ctx.emit(8); } else {
ctx.emit(0); }
Ok(RuleResult::Continue) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identifies_confusable_choseong() {
for &cho in &CONFUSABLE_CHOSEONG {
assert!(
CONFUSABLE_CHOSEONG.contains(&cho),
"Missing confusable: {}",
cho
);
}
}
#[rstest::rstest]
#[case::nieun("는", true)]
#[case::digeut("당", true)]
#[case::mieum("명", true)]
#[case::kieuk("칸", true)]
#[case::tieut("톤", true)]
#[case::pieup("평", true)]
#[case::hieuh("항", true)]
#[case::un_abbreviation("운", true)]
#[case::vowel_initial("이다", false)]
#[case::non_korean("A", false)]
fn detects_number_confusable_following_korean(#[case] input: &str, #[case] expected: bool) {
assert_eq!(
is_number_confusable_korean_char(input.chars().next().unwrap()),
expected
);
}
#[test]
fn meta_is_correct() {
assert_eq!(META.section, "44");
assert_eq!(META.name, "number_korean_spacing");
}
#[test]
fn rule44_apply_emits_attached_separator_after_middle_dot() {
let word: Vec<char> = "·하".chars().collect();
let ct = CharType::new(word[1]).unwrap();
let mut skip = 0usize;
let mut state = crate::rules::context::EncoderState::new(false);
state.is_number = true;
let mut out = Vec::new();
let mut ctx = RuleContext {
word_chars: &word,
index: 1,
char_type: &ct,
prev_word: "",
remaining_words: &[],
has_korean_char: true,
is_all_uppercase: false,
ascii_starts_at_beginning: false,
roman_section_continues_from_previous_word: false,
skip_count: &mut skip,
state: &mut state,
result: &mut out,
};
let outcome = Rule44.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Continue));
assert_eq!(out, vec![8]); }
}