use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
use crate::word_shortcut;
pub static META: RuleMeta = RuleMeta {
section: "18",
subsection: None,
name: "word_abbreviation",
standard_ref: "2024 Korean Braille Standard, Ch.2 Sec.7 Art.18",
description: "Word abbreviations: 그래서,그러나,그러면,그러므로,그런데,그리고,그리하여",
};
#[cfg(test)]
fn apply(text: &str) -> Option<(&'static str, &'static [u8], String)> {
word_shortcut::split_word_shortcut(text)
}
fn match_word_shortcut(word_chars: &[char]) -> Option<&'static [u8]> {
for (key, codes) in word_shortcut::SHORTCUT_MAP.entries() {
let mut key_chars = key.chars();
let mut matched = true;
let mut consumed = 0usize;
loop {
match key_chars.next() {
None => break,
Some(kc) => match word_chars.get(consumed) {
Some(wc) if *wc == kc => {
consumed += 1;
}
_ => {
matched = false;
break;
}
},
}
}
if matched {
return Some(*codes);
}
}
None
}
pub struct Rule18;
impl BrailleRule for Rule18 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::WordShortcut
}
fn matches(&self, ctx: &RuleContext) -> bool {
if ctx.index != 0 {
return false;
}
match_word_shortcut(ctx.word_chars).is_some()
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
if let Some(codes) = match_word_shortcut(ctx.word_chars) {
ctx.emit_slice(codes);
Ok(RuleResult::Consumed)
} else {
Ok(RuleResult::Skip)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[rstest::rstest]
#[case("그래서")]
#[case("그러나")]
#[case("그러면")]
#[case("그러므로")]
#[case("그런데")]
#[case("그리고")]
#[case("그리하여")]
fn matches_all_word_abbreviations(#[case] word: &str) {
let result = apply(word);
assert!(result.is_some(), "Expected abbreviation for: {word}");
let (matched, codes, rest) = result.unwrap();
assert_eq!(matched, word);
assert!(!codes.is_empty());
assert!(rest.is_empty());
}
#[test]
fn matches_with_suffix() {
let result = apply("그래서인지").unwrap();
assert_eq!(result.0, "그래서");
assert_eq!(result.2, "인지");
}
#[rstest::rstest]
#[case::korean_unknown("안녕하세요")]
#[case::english("hello")]
fn no_match_for_non_abbreviation(#[case] input: &str) {
assert!(apply(input).is_none());
}
#[rstest::rstest]
#[case::geuraeseo("그래서", "⠁⠎")]
#[case::geureona("그러나", "⠁⠉")]
#[case::geurigo("그리고", "⠁⠥")]
fn golden_test_alignment(#[case] input: &str, #[case] expected: &str) {
let result = crate::encode_to_unicode(input).unwrap();
assert_eq!(result, expected, "Rule 18 golden test failed for: {input}");
}
#[rstest::rstest]
#[case("그래서")]
#[case("그러나")]
#[case("그러면")]
#[case("그러므로")]
#[case("그런데")]
#[case("그리고")]
#[case("그리하여")]
fn match_word_shortcut_finds_each_abbreviation(#[case] word: &str) {
let chars: Vec<char> = word.chars().collect();
let result = match_word_shortcut(&chars);
assert!(result.is_some(), "should match {word}");
assert!(!result.unwrap().is_empty());
}
#[test]
fn match_word_shortcut_returns_none_for_unknown() {
let chars: Vec<char> = "안녕".chars().collect();
assert!(match_word_shortcut(&chars).is_none());
}
#[test]
fn match_word_shortcut_matches_prefix_only() {
let chars: Vec<char> = "그래서인지".chars().collect();
let result = match_word_shortcut(&chars);
assert!(result.is_some());
}
#[test]
fn match_word_shortcut_short_word_no_match() {
let chars: Vec<char> = "가".chars().collect();
assert!(match_word_shortcut(&chars).is_none());
}
#[test]
fn rule18_meta_and_phase() {
let rule = Rule18;
let meta = rule.meta();
assert_eq!(meta.section, "18");
assert!(matches!(rule.phase(), Phase::WordShortcut));
}
fn make_ctx<'a>(
word_chars: &'a [char],
index: usize,
char_type: &'a crate::char_struct::CharType,
skip_count: &'a mut usize,
state: &'a mut crate::rules::context::EncoderState,
result: &'a mut Vec<u8>,
) -> RuleContext<'a> {
RuleContext {
word_chars,
index,
char_type,
prev_word: "",
remaining_words: &[],
has_korean_char: true,
is_all_uppercase: false,
ascii_starts_at_beginning: false,
skip_count,
state,
result,
}
}
#[test]
fn rule18_matches_at_word_start_only() {
use crate::char_struct::CharType;
use crate::rules::context::EncoderState;
let word_chars: Vec<char> = "그래서".chars().collect();
let ct0 = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = EncoderState::new(false);
let mut result = Vec::new();
let ctx_start = make_ctx(&word_chars, 0, &ct0, &mut skip, &mut state, &mut result);
assert!(Rule18.matches(&ctx_start));
let ct1 = CharType::new(word_chars[1]).unwrap();
let mut skip2 = 0usize;
let mut state2 = EncoderState::new(false);
let mut result2 = Vec::new();
let ctx_mid = make_ctx(&word_chars, 1, &ct1, &mut skip2, &mut state2, &mut result2);
assert!(!Rule18.matches(&ctx_mid));
}
#[test]
fn rule18_apply_emits_codes_on_match() {
use crate::char_struct::CharType;
use crate::rules::context::EncoderState;
let word_chars: Vec<char> = "그래서".chars().collect();
let ct = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = EncoderState::new(false);
let mut result = Vec::new();
let mut ctx = make_ctx(&word_chars, 0, &ct, &mut skip, &mut state, &mut result);
let outcome = Rule18.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Consumed));
assert!(!result.is_empty());
}
#[test]
fn rule18_apply_skips_on_no_match() {
use crate::char_struct::CharType;
use crate::rules::context::EncoderState;
let word_chars: Vec<char> = "안녕".chars().collect();
let ct = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = EncoderState::new(false);
let mut result = Vec::new();
let mut ctx = make_ctx(&word_chars, 0, &ct, &mut skip, &mut state, &mut result);
let outcome = Rule18.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Skip));
assert!(result.is_empty());
}
}