use anyhow::{Context, Result};
use clap::Subcommand;
use std::collections::HashMap;
use std::sync::LazyLock;
#[derive(Subcommand)]
pub enum BrailleAction {
#[command(about = "Encode text to Braille")]
Encode {
#[arg(help = "Input text")]
input: String,
},
#[command(about = "Decode Braille to text")]
Decode {
#[arg(help = "Braille encoded text")]
input: String,
},
}
pub fn run(action: BrailleAction) -> Result<()> {
match action {
BrailleAction::Encode { input } => {
println!("{}", encode(&input));
}
BrailleAction::Decode { input } => {
println!("{}", decode(&input)?);
}
}
Ok(())
}
const LETTER_TABLE: &[(char, char)] = &[
('A', '\u{2801}'),
('B', '\u{2803}'),
('C', '\u{2809}'),
('D', '\u{2819}'),
('E', '\u{2811}'),
('F', '\u{280B}'),
('G', '\u{281B}'),
('H', '\u{2813}'),
('I', '\u{280A}'),
('J', '\u{281A}'),
('K', '\u{2805}'),
('L', '\u{2807}'),
('M', '\u{280D}'),
('N', '\u{281D}'),
('O', '\u{2815}'),
('P', '\u{280F}'),
('Q', '\u{281F}'),
('R', '\u{2817}'),
('S', '\u{280E}'),
('T', '\u{281E}'),
('U', '\u{2825}'),
('V', '\u{2827}'),
('W', '\u{283A}'),
('X', '\u{282D}'),
('Y', '\u{283D}'),
('Z', '\u{2835}'),
];
const NUMBER_PREFIX: char = '\u{283C}';
const BRAILLE_SPACE: char = '\u{2800}';
static CHAR_TO_BRAILLE: LazyLock<HashMap<char, char>> =
LazyLock::new(|| LETTER_TABLE.iter().copied().collect());
static BRAILLE_TO_CHAR: LazyLock<HashMap<char, char>> =
LazyLock::new(|| LETTER_TABLE.iter().map(|&(c, b)| (b, c)).collect());
fn digit_to_braille(d: char) -> Option<char> {
let idx = match d {
'1' => 0, '2' => 1, '3' => 2, '4' => 3, '5' => 4, '6' => 5, '7' => 6, '8' => 7, '9' => 8, '0' => 9, _ => return None,
};
Some(LETTER_TABLE[idx].1)
}
const DIGIT_MAP: &[char] = &['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
fn braille_to_digit(b: char) -> Option<char> {
LETTER_TABLE
.iter()
.enumerate()
.find(|(idx, (_, br))| *br == b && *idx < 10)
.map(|(idx, _)| DIGIT_MAP[idx])
}
pub fn encode(input: &str) -> String {
let mut result = String::new();
for c in input.to_uppercase().chars() {
if c.is_ascii_alphabetic() {
if let Some(&braille) = CHAR_TO_BRAILLE.get(&c) {
result.push(braille);
}
} else if c.is_ascii_digit() {
result.push(NUMBER_PREFIX);
if let Some(braille) = digit_to_braille(c) {
result.push(braille);
}
} else if c == ' ' {
result.push(BRAILLE_SPACE);
}
}
result
}
pub fn decode(input: &str) -> Result<String> {
if input.is_empty() {
return Ok(String::new());
}
let mut result = String::new();
let mut in_number_mode = false;
for c in input.chars() {
if c == NUMBER_PREFIX {
in_number_mode = true;
continue;
}
if c == BRAILLE_SPACE {
result.push(' ');
in_number_mode = false;
continue;
}
if in_number_mode {
let digit = braille_to_digit(c)
.context(format!("Invalid Braille number character: {:?}", c))?;
result.push(digit);
in_number_mode = false;
} else {
let letter = BRAILLE_TO_CHAR
.get(&c)
.copied()
.context(format!("Unknown Braille character: {:?}", c))?;
result.push(letter);
}
}
Ok(result)
}