use crate::char_struct::CharType;
use crate::number;
use crate::rules::RuleMeta;
use crate::rules::context::RuleContext;
use crate::rules::korean::rule_69::parse_numeric_ascii_unit_prefix;
use crate::rules::traits::{BrailleRule, Phase, RuleResult};
pub static META_40: RuleMeta = RuleMeta {
section: "40",
subsection: None,
name: "number_prefix",
standard_ref: "2024 Korean Braille Standard, Ch.5 Sec.11 Art.40",
description: "Number indicator ⠼ (60) before first digit in number sequence",
};
pub const NUMBER_INDICATOR: u8 = 60;
#[cfg(test)]
fn encode_digit(ch: char) -> Result<u8, String> {
number::encode_number(ch)
}
pub struct Rule40;
impl BrailleRule for Rule40 {
fn meta(&self) -> &'static RuleMeta {
&META_40
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn matches(&self, ctx: &RuleContext) -> bool {
matches!(ctx.char_type, CharType::Number(_))
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
let CharType::Number(c) = ctx.char_type else {
return Ok(RuleResult::Skip);
};
if ctx.index == 0
&& let Some((numeric, unit, consumed)) = parse_numeric_ascii_unit_prefix(ctx.word_chars)
{
let mut encoded = crate::encode(&numeric)?;
encoded.extend(unit);
ctx.emit_slice(&encoded);
ctx.state.is_number = false;
*ctx.skip_count = consumed.saturating_sub(1);
return Ok(RuleResult::Consumed);
}
if !ctx.state.is_number {
let needs_prefix = ctx
.prev_char()
.is_none_or(|prev| !is_number_continuation(prev));
if needs_prefix {
ctx.emit(NUMBER_INDICATOR);
if ctx
.prev_char()
.is_some_and(|prev| prev == '\'' || prev == '\u{2019}')
{
ctx.emit(4);
}
}
ctx.state.is_number = true;
}
let digit = number::encode_number(*c)?;
ctx.emit(digit);
Ok(RuleResult::Consumed)
}
}
pub fn is_number_continuation(prev: char) -> bool {
prev == '.' || prev == ','
}
#[cfg(test)]
mod tests {
use super::*;
use crate::unicode::decode_unicode;
#[test]
fn encodes_digits() {
assert_eq!(encode_digit('1').unwrap(), decode_unicode('⠁'));
assert_eq!(encode_digit('0').unwrap(), decode_unicode('⠚'));
assert_eq!(encode_digit('9').unwrap(), decode_unicode('⠊'));
}
#[test]
fn invalid_digit() {
assert!(encode_digit('a').is_err());
}
#[test]
fn continuation_chars() {
assert!(is_number_continuation('.'));
assert!(is_number_continuation(','));
assert!(!is_number_continuation(' '));
assert!(!is_number_continuation('-'));
}
#[test]
fn golden_test_alignment() {
let cases = vec![("1", "⠼⠁"), ("10", "⠼⠁⠚"), ("0.48", "⠼⠚⠲⠙⠓")];
for (input, expected) in cases {
let result = crate::encode_to_unicode(input).unwrap();
assert_eq!(
result, expected,
"Rule 40 golden test failed for: {}",
input
);
}
}
}