use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
pub static META: RuleMeta = RuleMeta {
section: "53",
subsection: None,
name: "ellipsis_normalization",
standard_ref: "2024 Korean Braille Standard, Ch.6 Sec.13 Art.53",
description: "Normalize ellipsis: 6 dots→3, double middle dot→single",
};
#[cfg(test)]
fn normalize(word: &str) -> String {
word.replace("......", "...").replace("……", "…")
}
pub struct Rule53;
impl BrailleRule for Rule53 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::Preprocessing
}
fn matches(&self, ctx: &RuleContext) -> bool {
if ctx.index != 0 {
return false;
}
let mut dot_run = 0u8;
let mut prev_ellipsis = false;
for &ch in ctx.word_chars {
if ch == '.' {
dot_run += 1;
if dot_run >= 6 {
return true;
}
prev_ellipsis = false;
} else if ch == '…' {
if prev_ellipsis {
return true;
}
prev_ellipsis = true;
dot_run = 0;
} else {
dot_run = 0;
prev_ellipsis = false;
}
}
false
}
fn apply(&self, _ctx: &mut RuleContext) -> Result<RuleResult, String> {
Ok(RuleResult::Continue)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[rstest::rstest]
#[case::six_periods_to_three("hello......world", "hello...world")]
#[case::double_middle_dot_to_single("hello……world", "hello…world")]
#[case::three_periods_unchanged("hello...world", "hello...world")]
#[case::single_middle_dot_unchanged("hello…world", "hello…world")]
#[case::normal_korean_unchanged("안녕하세요", "안녕하세요")]
#[case::empty_string("", "")]
fn normalize_paths(#[case] input: &str, #[case] expected: &str) {
assert_eq!(normalize(input), expected);
}
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: false,
is_all_uppercase: false,
ascii_starts_at_beginning: false,
skip_count,
state,
result,
}
}
#[test]
fn rule53_meta_and_phase() {
let r = Rule53;
assert_eq!(r.meta().section, "53");
assert!(matches!(r.phase(), Phase::Preprocessing));
}
#[test]
fn rule53_matches_six_periods_run() {
use crate::char_struct::CharType;
let word_chars: Vec<char> = "......".chars().collect();
let ct = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = crate::rules::context::EncoderState::new(false);
let mut out = Vec::new();
let ctx = make_ctx(&word_chars, 0, &ct, &mut skip, &mut state, &mut out);
assert!(Rule53.matches(&ctx));
}
#[test]
fn rule53_matches_double_ellipsis() {
use crate::char_struct::CharType;
let word_chars: Vec<char> = "……".chars().collect();
let ct = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = crate::rules::context::EncoderState::new(false);
let mut out = Vec::new();
let ctx = make_ctx(&word_chars, 0, &ct, &mut skip, &mut state, &mut out);
assert!(Rule53.matches(&ctx));
}
#[test]
fn rule53_does_not_match_three_periods() {
use crate::char_struct::CharType;
let word_chars: Vec<char> = "...".chars().collect();
let ct = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = crate::rules::context::EncoderState::new(false);
let mut out = Vec::new();
let ctx = make_ctx(&word_chars, 0, &ct, &mut skip, &mut state, &mut out);
assert!(!Rule53.matches(&ctx));
}
#[test]
fn rule53_match_resets_on_other_char() {
use crate::char_struct::CharType;
let word_chars: Vec<char> = "...a...".chars().collect();
let ct = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = crate::rules::context::EncoderState::new(false);
let mut out = Vec::new();
let ctx = make_ctx(&word_chars, 0, &ct, &mut skip, &mut state, &mut out);
assert!(!Rule53.matches(&ctx));
}
#[test]
fn rule53_match_false_when_not_at_word_start() {
use crate::char_struct::CharType;
let word_chars: Vec<char> = "......".chars().collect();
let ct = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = crate::rules::context::EncoderState::new(false);
let mut out = Vec::new();
let ctx = make_ctx(&word_chars, 1, &ct, &mut skip, &mut state, &mut out);
assert!(!Rule53.matches(&ctx));
}
#[test]
fn rule53_apply_just_continues() {
use crate::char_struct::CharType;
let word_chars: Vec<char> = "......".chars().collect();
let ct = CharType::new(word_chars[0]).unwrap();
let mut skip = 0usize;
let mut state = crate::rules::context::EncoderState::new(false);
let mut out = Vec::new();
let mut ctx = make_ctx(&word_chars, 0, &ct, &mut skip, &mut state, &mut out);
let res = Rule53.apply(&mut ctx).unwrap();
assert!(matches!(res, RuleResult::Continue));
assert!(out.is_empty());
}
}