use anyhow::Result;
use clap::Subcommand;
#[derive(Subcommand)]
pub enum BaconianAction {
#[command(about = "Encode text using Baconian cipher")]
Encode {
#[arg(help = "Input text")]
input: String,
},
#[command(about = "Decode Baconian cipher")]
Decode {
#[arg(help = "Baconian encoded text (A/B patterns, space-separated)")]
input: String,
},
}
pub fn run(action: BaconianAction) -> Result<()> {
match action {
BaconianAction::Encode { input } => {
println!("{}", encode(&input)?);
}
BaconianAction::Decode { input } => {
println!("{}", decode(&input)?);
}
}
Ok(())
}
fn char_to_bacon_index(c: char) -> Option<u8> {
match c.to_ascii_uppercase() {
'A' => Some(0),
'B' => Some(1),
'C' => Some(2),
'D' => Some(3),
'E' => Some(4),
'F' => Some(5),
'G' => Some(6),
'H' => Some(7),
'I' | 'J' => Some(8),
'K' => Some(9),
'L' => Some(10),
'M' => Some(11),
'N' => Some(12),
'O' => Some(13),
'P' => Some(14),
'Q' => Some(15),
'R' => Some(16),
'S' => Some(17),
'T' => Some(18),
'U' | 'V' => Some(19),
'W' => Some(20),
'X' => Some(21),
'Y' => Some(22),
'Z' => Some(23),
_ => None,
}
}
fn bacon_index_to_char(idx: u8) -> Option<char> {
match idx {
0 => Some('A'),
1 => Some('B'),
2 => Some('C'),
3 => Some('D'),
4 => Some('E'),
5 => Some('F'),
6 => Some('G'),
7 => Some('H'),
8 => Some('I'),
9 => Some('K'),
10 => Some('L'),
11 => Some('M'),
12 => Some('N'),
13 => Some('O'),
14 => Some('P'),
15 => Some('Q'),
16 => Some('R'),
17 => Some('S'),
18 => Some('T'),
19 => Some('U'),
20 => Some('W'),
21 => Some('X'),
22 => Some('Y'),
23 => Some('Z'),
_ => None,
}
}
fn index_to_pattern(idx: u8) -> String {
(0..5)
.rev()
.map(|bit| if (idx >> bit) & 1 == 0 { 'A' } else { 'B' })
.collect()
}
fn pattern_to_index(pattern: &str) -> Option<u8> {
if pattern.len() != 5 {
return None;
}
let mut idx: u8 = 0;
for c in pattern.chars() {
idx <<= 1;
match c.to_ascii_uppercase() {
'A' => {}
'B' => idx |= 1,
_ => return None,
}
}
if idx > 23 {
return None;
}
Some(idx)
}
pub fn encode(input: &str) -> Result<String> {
let patterns: Vec<String> = input
.chars()
.filter_map(|c| char_to_bacon_index(c).map(index_to_pattern))
.collect();
if patterns.is_empty() && input.chars().any(|c| c.is_ascii_alphabetic()) {
anyhow::bail!("Failed to encode input");
}
Ok(patterns.join(" "))
}
pub fn decode(input: &str) -> Result<String> {
if input.is_empty() {
return Ok(String::new());
}
let mut result = String::new();
for token in input.split_whitespace() {
let idx = pattern_to_index(token)
.ok_or_else(|| anyhow::anyhow!("Invalid Baconian pattern: {}", token))?;
let c = bacon_index_to_char(idx)
.ok_or_else(|| anyhow::anyhow!("Invalid Baconian index: {}", idx))?;
result.push(c);
}
Ok(result)
}