use std::collections::HashMap;
use std::sync::OnceLock;
const UNIT: i64 = 1;
const TEEN: i64 = 10;
const TEN: i64 = 10;
const HUNDRED: i64 = 100;
const THOUSAND: i64 = 1000;
const MILLION: i64 = 1_000_000;
fn number_words() -> &'static HashMap<&'static str, (i64, i64)> {
static TABLE: OnceLock<HashMap<&'static str, (i64, i64)>> = OnceLock::new();
TABLE.get_or_init(|| {
let mut m: HashMap<&'static str, (i64, i64)> = HashMap::new();
let mut add = |value: i64, scale: i64, forms: &[&'static str]| {
for &form in forms {
m.insert(form, (value, scale));
}
};
add(0, UNIT, &["ноль", "ноля", "нолю", "нолем", "нолём", "ноле"]);
add(
1,
UNIT,
&[
"один",
"одна",
"одно",
"одного",
"одной",
"одному",
"одном",
"одним",
],
);
add(2, UNIT, &["два", "две", "двух", "двум", "двумя"]);
add(3, UNIT, &["три", "трех", "трёх", "трем", "трём", "тремя"]);
add(
4,
UNIT,
&[
"четыре",
"четырёх",
"четырех",
"четырём",
"четырем",
"четырьмя",
],
);
add(5, UNIT, &["пять", "пяти", "пятью"]);
add(6, UNIT, &["шесть", "шести", "шестью"]);
add(7, UNIT, &["семь", "семи", "семью"]);
add(8, UNIT, &["восемь", "восьми", "восьмью"]);
add(9, UNIT, &["девять", "девяти", "девятью"]);
add(10, TEEN, &["десять", "десяти", "десятью"]);
add(11, TEEN, &["одиннадцать", "одиннадцати"]);
add(12, TEEN, &["двенадцать", "двенадцати"]);
add(13, TEEN, &["тринадцать", "тринадцати"]);
add(14, TEEN, &["четырнадцать", "четырнадцати"]);
add(15, TEEN, &["пятнадцать", "пятнадцати"]);
add(16, TEEN, &["шестнадцать", "шестнадцати"]);
add(17, TEEN, &["семнадцать", "семнадцати"]);
add(18, TEEN, &["восемнадцать", "восемнадцати"]);
add(19, TEEN, &["девятнадцать", "девятнадцати"]);
add(20, TEN, &["двадцать", "двадцати"]);
add(30, TEN, &["тридцать", "тридцати"]);
add(40, TEN, &["сорок", "сорока"]);
add(50, TEN, &["пятьдесят", "пятидесяти"]);
add(60, TEN, &["шестьдесят", "шестидесяти"]);
add(70, TEN, &["семьдесят", "семидесяти"]);
add(80, TEN, &["восемьдесят", "восьмидесяти"]);
add(90, TEN, &["девяносто", "девяноста"]);
add(100, HUNDRED, &["сто", "ста"]);
add(200, HUNDRED, &["двести", "двухсот"]);
add(300, HUNDRED, &["триста", "трехсот", "трёхсот"]);
add(400, HUNDRED, &["четыреста", "четырёхсот"]);
add(500, HUNDRED, &["пятьсот", "пятисот"]);
add(600, HUNDRED, &["шестьсот", "шестисот"]);
add(700, HUNDRED, &["семьсот", "семисот"]);
add(800, HUNDRED, &["восемьсот", "восьмисот"]);
add(900, HUNDRED, &["девятьсот", "девятисот"]);
add(
1000,
THOUSAND,
&[
"тысяча",
"тысячи",
"тысяч",
"тысяче",
"тысячу",
"тысячей",
"тысячам",
"тысячами",
"тысячах",
],
);
add(
1_000_000,
MILLION,
&[
"миллион",
"миллиона",
"миллионов",
"миллиону",
"миллионе",
"миллионам",
"миллионами",
"миллионах",
],
);
m
})
}
fn words_to_numbers(tokens: &[String]) -> Vec<String> {
let table = number_words();
let mut result: Vec<String> = Vec::with_capacity(tokens.len());
let mut current: i64 = 0;
let mut running_total: i64 = 0;
let mut prev_scale: i64 = 0;
let mut in_number = false;
for token in tokens {
let key = token.to_lowercase();
match table.get(key.as_str()) {
Some(&(value, scale)) => {
in_number = true;
if scale == THOUSAND || scale == MILLION {
if current == 0 {
current = 1;
}
running_total += current * scale;
current = 0;
prev_scale = scale;
} else if current > 0 && scale >= prev_scale {
result.push((running_total + current).to_string());
running_total = 0;
current = value;
in_number = true;
prev_scale = scale;
} else {
current += value;
prev_scale = scale;
}
}
None => {
let total = running_total + current;
if in_number {
result.push(total.to_string());
}
current = 0;
running_total = 0;
prev_scale = 0;
in_number = false;
result.push(token.clone());
}
}
}
let total = running_total + current;
if in_number {
result.push(total.to_string());
}
result
}
fn is_digit_token(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
fn merge_digit_groups(tokens: &[String]) -> Vec<String> {
let mut result: Vec<String> = Vec::with_capacity(tokens.len());
let n = tokens.len();
let mut i = 0;
while i < n {
if is_digit_token(&tokens[i]) {
let mut j = i;
let mut group: Vec<&String> = Vec::new();
while j < n && is_digit_token(&tokens[j]) {
group.push(&tokens[j]);
j += 1;
}
if !group.is_empty() && group.iter().all(|t| t.chars().count() <= 3) {
result.push(group.iter().map(|t| t.as_str()).collect::<String>());
} else {
result.extend(group.iter().map(|t| (*t).clone()));
}
i = j;
} else {
result.push(tokens[i].clone());
i += 1;
}
}
result
}
pub fn apply_itn(text: &str) -> String {
let tokens: Vec<String> = text.split_whitespace().map(str::to_string).collect();
let tokens = words_to_numbers(&tokens);
let tokens = merge_digit_groups(&tokens);
tokens.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apply_itn_tens_plus_unit() {
assert_eq!(apply_itn("двадцать один"), "21");
}
#[test]
fn test_apply_itn_compound_thousand() {
assert_eq!(apply_itn("две тысячи двадцать"), "2020");
}
#[test]
fn test_apply_itn_sixty_thousand() {
assert_eq!(apply_itn("шестьдесят тысяч"), "60000");
}
#[test]
fn test_apply_itn_hundred() {
assert_eq!(apply_itn("сто"), "100");
}
#[test]
fn test_apply_itn_plain_words_unchanged() {
assert_eq!(apply_itn("привет как дела"), "привет как дела");
}
#[test]
fn test_apply_itn_mixed_words_and_number() {
assert_eq!(apply_itn("позвони на шестьдесят"), "позвони на 60");
}
#[test]
fn test_apply_itn_case_insensitive() {
assert_eq!(apply_itn("Двадцать один"), "21");
}
#[test]
fn test_apply_itn_digit_group_merge() {
assert_eq!(apply_itn("восемь девять пять"), "895");
}
#[test]
fn test_apply_itn_trailing_unit_folds_into_running_total() {
assert_eq!(apply_itn("шестьдесят тысяч пять"), "60005");
}
#[test]
fn test_apply_itn_long_digit_run_not_merged() {
assert_eq!(apply_itn("тысяча сто двести"), "1100 200");
}
#[test]
fn test_apply_itn_phrase_with_trailing_words() {
assert_eq!(
apply_itn("шестьдесят тысяч тенге сколько будет стоить"),
"60000 тенге сколько будет стоить"
);
}
#[test]
fn test_apply_itn_empty_string() {
assert_eq!(apply_itn(""), "");
}
#[test]
fn test_apply_itn_punctuation_passes_through() {
assert_eq!(apply_itn("привет, мир"), "привет, мир");
}
}