use anyhow::Result;
use clap::Subcommand;
use std::collections::HashMap;
use std::sync::LazyLock;
#[derive(Subcommand)]
pub enum PhoneAction {
#[command(about = "Encode text to phone keypad multi-tap")]
Encode {
#[arg(help = "Input text")]
input: String,
},
#[command(about = "Decode phone keypad multi-tap to text")]
Decode {
#[arg(help = "Phone keypad encoded text")]
input: String,
},
}
pub fn run(action: PhoneAction) -> Result<()> {
match action {
PhoneAction::Encode { input } => {
println!("{}", encode(&input));
}
PhoneAction::Decode { input } => {
println!("{}", decode(&input)?);
}
}
Ok(())
}
const KEYPAD: &[(char, &[char])] = &[
('2', &['A', 'B', 'C']),
('3', &['D', 'E', 'F']),
('4', &['G', 'H', 'I']),
('5', &['J', 'K', 'L']),
('6', &['M', 'N', 'O']),
('7', &['P', 'Q', 'R', 'S']),
('8', &['T', 'U', 'V']),
('9', &['W', 'X', 'Y', 'Z']),
];
static CHAR_TO_KEY: LazyLock<HashMap<char, (char, usize)>> = LazyLock::new(|| {
let mut map = HashMap::new();
for &(digit, letters) in KEYPAD {
for (pos, &letter) in letters.iter().enumerate() {
map.insert(letter, (digit, pos));
}
}
map
});
static KEY_TO_CHAR: LazyLock<HashMap<(char, usize), char>> = LazyLock::new(|| {
let mut map = HashMap::new();
for &(digit, letters) in KEYPAD {
for (pos, &letter) in letters.iter().enumerate() {
map.insert((digit, pos + 1), letter);
}
}
map
});
fn key_for_char(c: char) -> Option<char> {
CHAR_TO_KEY.get(&c).map(|&(digit, _)| digit)
}
const PHONE_PRESS_TABLE: [&str; 26] = [
"2", "22", "222", "3", "33", "333", "4", "44", "444", "5", "55", "555", "6", "66", "666", "7", "77", "777", "7777", "8", "88", "888", "9", "99", "999", "9999", ];
pub fn encode(input: &str) -> String {
let upper = input.to_uppercase();
let mut result = String::with_capacity(upper.len() * 4);
let mut prev_key: Option<char> = None;
let mut group_has_content = false;
for c in upper.chars() {
if c == ' ' {
if group_has_content {
result.push(' ');
}
result.push('0');
group_has_content = true;
prev_key = None;
continue;
}
if !c.is_ascii_alphabetic() {
continue;
}
let idx = (c as u8 - b'A') as usize;
let presses = PHONE_PRESS_TABLE[idx];
let cur_key = key_for_char(c);
if prev_key.is_some() && prev_key == cur_key {
result.push('-');
} else {
if group_has_content {
result.push(' ');
}
group_has_content = true;
}
result.push_str(presses);
prev_key = cur_key;
}
result
}
fn presses_to_char(s: &str) -> Result<char> {
if s.is_empty() {
anyhow::bail!("Empty press sequence");
}
let digit = s.chars().next().unwrap();
if !s.chars().all(|c| c == digit) {
anyhow::bail!("Mixed digits in press sequence: {}", s);
}
let count = s.len();
if digit == '0' {
return Ok(' ');
}
KEY_TO_CHAR
.get(&(digit, count))
.copied()
.ok_or_else(|| anyhow::anyhow!("Invalid press count {} for key {}", count, digit))
}
pub fn decode(input: &str) -> Result<String> {
if input.trim().is_empty() {
return Ok(String::new());
}
let mut result = String::new();
for group in input.split_whitespace() {
if group == "0" {
result.push(' ');
continue;
}
for press_seq in group.split('-') {
result.push(presses_to_char(press_seq)?);
}
}
Ok(result)
}