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: "31",
subsection: None,
name: "greek_letters",
standard_ref: "2024 Korean Braille Standard, Ch.4 Art.31",
description: "Greek letters in Korean context use Roman indicators and Greek braille cells",
};
fn greek_braille(c: char) -> Option<&'static str> {
match c {
'Α' | 'α' => Some("⠨⠁"),
'Β' | 'β' => Some("⠨⠃"),
'Γ' | 'γ' => Some("⠨⠛"),
'Δ' | 'δ' => Some("⠨⠙"),
'Ε' | 'ε' => Some("⠨⠑"),
'Ζ' | 'ζ' => Some("⠨⠵"),
'Η' | 'η' => Some("⠨⠱"),
'Θ' | 'θ' => Some("⠨⠹"),
'Ι' | 'ι' => Some("⠨⠊"),
'Κ' | 'κ' => Some("⠨⠅"),
'Λ' | 'λ' => Some("⠨⠇"),
'Μ' | 'μ' | 'µ' => Some("⠨⠍"),
'Ν' | 'ν' => Some("⠨⠝"),
'Ξ' | 'ξ' => Some("⠨⠭"),
'Ο' | 'ο' => Some("⠨⠕"),
'Π' | 'π' => Some("⠨⠏"),
'Ρ' | 'ρ' => Some("⠨⠗"),
'Σ' | 'σ' | 'ς' => Some("⠨⠎"),
'Τ' | 'τ' => Some("⠨⠞"),
'Υ' | 'υ' => Some("⠨⠥"),
'Φ' | 'φ' => Some("⠨⠋"),
'Χ' | 'χ' => Some("⠨⠯"),
'Ψ' | 'ψ' => Some("⠨⠽"),
'Ω' | 'ω' => Some("⠨⠺"),
_ => None,
}
}
fn encode_unicode_cells(unicode: &str) -> Vec<u8> {
unicode
.chars()
.map(crate::unicode::decode_unicode)
.collect()
}
fn korean_context(ctx: &RuleContext) -> bool {
ctx.has_korean_char
|| ctx.prev_word.chars().any(crate::utils::is_korean_char)
|| ctx
.remaining_words
.first()
.is_some_and(|word| word.chars().any(crate::utils::is_korean_char))
}
pub fn is_greek_letter(c: char) -> bool {
greek_braille(c).is_some()
}
pub struct Rule31;
impl BrailleRule for Rule31 {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn phase(&self) -> Phase {
Phase::CoreEncoding
}
fn priority(&self) -> u16 {
145
}
fn matches(&self, ctx: &RuleContext) -> bool {
matches!(ctx.char_type, CharType::Symbol(c) if is_greek_letter(*c))
}
fn apply(&self, ctx: &mut RuleContext) -> Result<RuleResult, String> {
let run: Vec<char> = ctx.word_chars[ctx.index..]
.iter()
.copied()
.take_while(|ch| is_greek_letter(*ch))
.collect();
if run.is_empty() {
return Ok(RuleResult::Skip);
}
let korean_context = korean_context(ctx);
if korean_context && !ctx.state.is_english {
if ctx.state.needs_english_continuation {
ctx.emit(crate::rules::korean::rule_29::ENGLISH_CONTINUATION);
} else {
ctx.emit(crate::rules::korean::rule_29::ROMAN_INDICATOR);
}
}
if run.len() > 1 && run.iter().all(|c| c.is_uppercase()) {
ctx.emit(crate::unicode::decode_unicode('⠠'));
ctx.emit(crate::unicode::decode_unicode('⠠'));
} else if run.len() == 1 && run[0].is_uppercase() {
ctx.emit(crate::unicode::decode_unicode('⠠'));
}
for unicode in run.iter().filter_map(|ch| greek_braille(*ch)) {
ctx.emit_slice(&encode_unicode_cells(unicode));
}
if korean_context {
crate::rules::roman_mode::mark_section_open(ctx.state);
}
if run.len() > 1 {
*ctx.skip_count = run.len() - 1;
}
Ok(RuleResult::Consumed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_exercise() {
let mut owned = crate::test_helpers::CtxOwned::for_text("A", false);
let mut ctx = owned.ctx_at(0);
let _ = Rule31.apply(&mut ctx);
}
#[test]
fn matches_does_not_panic() {
let mut owned = crate::test_helpers::CtxOwned::for_text("A", false);
let ctx = owned.ctx_at(0);
let _ = Rule31.matches(&ctx);
}
#[test]
fn rule_metadata_reports_phase_and_priority() {
let rule = std::hint::black_box(Rule31);
assert!(matches!(rule.phase(), Phase::CoreEncoding));
assert_eq!(rule.priority(), 145);
}
#[test]
fn rule31_uppercase_single_greek_in_korean_context() {
let result = crate::encode_to_unicode("가 Δ").unwrap();
assert!(!result.is_empty());
}
#[test]
fn rule31_uppercase_run_in_korean_context() {
let result = crate::encode_to_unicode("가 ΔΕ").unwrap();
assert!(!result.is_empty());
}
#[test]
fn rule31_lowercase_greek_no_korean_context() {
let result = crate::encode_to_unicode("δ").unwrap();
assert!(!result.is_empty());
}
#[test]
fn rule31_uppercase_single_greek_no_korean_context() {
let result = crate::encode_to_unicode("Δ").unwrap();
assert!(!result.is_empty());
}
#[test]
fn rule31_uppercase_run_no_korean_context() {
let result = crate::encode_to_unicode("ΔΕ").unwrap();
assert!(!result.is_empty());
}
}
#[cfg(test)]
mod greek_run_coverage {
#[rstest::rstest]
#[case::single("알파 \u{03B1} 값")]
#[case::run("\u{03B1}\u{03B2}\u{03B3}")]
fn greek_letters_encode(#[case] input: &str) {
assert!(crate::encode_to_unicode(input).is_ok());
}
}
#[cfg(test)]
mod greek_continuation_coverage {
#[rstest::rstest]
#[case::single("알파 \u{03B1} 값")]
#[case::run("\u{03B1}\u{03B2}\u{03B3}")]
#[case::greek_then_roman("\u{03B1}x 값")]
#[case::greek_run_then_roman("\u{03B1}\u{03B2}t 값")]
fn a_greek_run_encodes(#[case] input: &str) {
assert!(crate::encode_to_unicode(input).is_ok());
}
}
#[cfg(test)]
mod greek_after_open_section_coverage {
#[rstest::rstest]
#[case::after_roman_hyphen("그는 IFN-\u{03B3} 를")]
#[case::after_comma("비타민A, \u{03B3}")]
#[case::after_number("\u{03B1} 1\u{03B2} 값")]
fn a_greek_letter_after_a_roman_item_encodes(#[case] input: &str) {
assert!(crate::encode_to_unicode(input).is_ok());
}
}
#[cfg(test)]
mod greek_continuation_indicator_coverage {
#[rstest::rstest]
#[case::after_comma("비타민A, \u{03B3}")]
#[case::after_comma_short("A, \u{03B3}")]
#[case::after_number_comma("비타민1, \u{03B3}")]
#[case::after_closing_paren("비타민(A), \u{03B3}")]
#[case::after_middle_dot("DT\u{00B7}\u{03B3}")]
#[case::after_hyphen("IFN-\u{03B3}")]
#[case::after_number("1\u{03B2} 값")]
fn a_greek_letter_after_an_open_section_encodes(#[case] input: &str) {
assert!(crate::encode_to_unicode(input).is_ok());
}
}
#[cfg(test)]
mod continuation_indicator_branch_coverage {
use super::*;
use crate::rules::traits::BrailleRule;
#[rstest::rstest]
#[case::continuation_pending(true, crate::rules::korean::rule_29::ENGLISH_CONTINUATION)]
#[case::fresh_section(false, crate::rules::korean::rule_29::ROMAN_INDICATOR)]
fn a_greek_run_opens_with_the_reserved_marker(
#[case] continuation_pending: bool,
#[case] expected_first_cell: u8,
) {
let mut owned = crate::test_helpers::CtxOwned::for_text("\u{03B1}", false);
owned.state.needs_english_continuation = continuation_pending;
owned.prev_word = "한글".to_string();
let mut ctx = owned.ctx_at(0);
let outcome = Rule31.apply(&mut ctx).unwrap();
assert!(matches!(outcome, RuleResult::Consumed));
assert_eq!(
owned.result.first().copied(),
Some(expected_first_cell),
"unexpected opening marker"
);
}
}