use crate::Result;
use crate::currency::vocab::TR as CURRENCY_VOCAB;
use crate::currency::{Currency, assert_minor_unit};
use crate::language::{Kind, Language, form};
use crate::max::Max;
use crate::number::{
Number, digits_to_biguint, parse_cardinal, parse_currency, parse_ordinal, split_groups,
};
use crate::options::options;
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
const ONES: [&str; 10] = [
"", "bir", "iki", "üç", "dört", "beş", "altı", "yedi", "sekiz", "dokuz",
];
const TEENS: [&str; 10] = [
"on", "on bir", "on iki", "on üç", "on dört", "on beş", "on altı", "on yedi", "on sekiz",
"on dokuz",
];
const TENS: [&str; 10] = [
"", "", "yirmi", "otuz", "kırk", "elli", "altmış", "yetmiş", "seksen", "doksan",
];
const HUNDRED: &str = "yüz";
const THOUSAND: &str = "bin";
const ZERO: &str = "sıfır";
const NEGATIVE: &str = "eksi";
const DECIMAL_SEP: &str = "virgül";
const SCALES: &[&str] = &["milyon", "milyar", "trilyon", "katrilyon", "kentilyon"];
pub const CARDINAL_MAX: Max = Max::western(SCALES.len() as u32 + 1);
pub const ORDINAL_MAX: Max = Max::western(SCALES.len() as u32 + 1);
pub const CURRENCY_MAX: Max = Max::western(SCALES.len() as u32 + 1);
const ORDINAL_SPECIAL: [&str; 11] = [
"",
"birinci",
"ikinci",
"üçüncü",
"dördüncü",
"beşinci",
"altıncı",
"yedinci",
"sekizinci",
"dokuzuncu",
"onuncu",
];
const fn separator(drop_spaces: bool) -> &'static str {
if drop_spaces { "" } else { " " }
}
fn build_segment(n: u32, sep: &str) -> String {
if n == 0 {
return String::new();
}
let ones = (n % 10) as usize;
let tens = ((n / 10) % 10) as usize;
let hundreds = (n / 100) as usize;
let mut parts: Vec<String> = Vec::new();
if hundreds > 0 {
parts.push(if hundreds == 1 {
HUNDRED.to_string()
} else {
format!("{}{sep}{HUNDRED}", ONES[hundreds])
});
}
let tens_ones = n % 100;
if tens_ones > 0 {
parts.push(if tens_ones < 10 {
ONES[ones].to_string()
} else if tens_ones < 20 {
TEENS[ones].replace(' ', sep)
} else if ones == 0 {
TENS[tens].to_string()
} else {
format!("{}{sep}{}", TENS[tens], ONES[ones])
});
}
parts.join(sep)
}
fn integer_to_words(n: &BigUint, drop_spaces: bool) -> String {
if n.is_zero() {
return ZERO.to_string();
}
let sep = separator(drop_spaces);
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_segment(small as u32, sep);
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
let mut result = if thousands == 1 {
THOUSAND.to_string()
} else {
format!("{}{sep}{THOUSAND}", build_segment(thousands, sep))
};
if remainder > 0 {
result.push_str(sep);
result.push_str(&build_segment(remainder, sep));
}
return result;
}
}
build_large_number_words(n, sep)
}
fn build_large_number_words(n: &BigUint, sep: &str) -> String {
let segments = split_groups(n, 1000);
let mut parts: Vec<String> = Vec::new();
for (i, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
let word = build_segment(segment, sep);
parts.push(match i {
0 => word,
1 if segment == 1 => THOUSAND.to_string(),
1 => format!("{word}{sep}{THOUSAND}"),
_ => format!("{word}{sep}{}", SCALES[i - 2]),
});
}
parts.join(sep)
}
fn decimal_part_to_words(decimal: &str, drop_spaces: bool) -> String {
let sep = separator(drop_spaces);
let mut result = String::new();
let zeros = decimal.bytes().take_while(|&b| b == b'0').count();
for _ in 0..zeros {
if !result.is_empty() {
result.push_str(sep);
}
result.push_str(ZERO);
}
let remainder = &decimal[zeros..];
if !remainder.is_empty() {
if !result.is_empty() {
result.push_str(sep);
}
result.push_str(&integer_to_words(
&digits_to_biguint(remainder),
drop_spaces,
));
}
result
}
options! {
pub struct CardinalOptions {
drop_spaces: bool = false;
}
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_cardinal_with(value, &CardinalOptions::default())
}
pub fn to_cardinal_with<'a>(
value: impl Into<Number<'a>>,
options: &CardinalOptions,
) -> Result<String> {
let number = parse_cardinal(&value.into())?;
CARDINAL_MAX.check(&number.integer, number.fraction.as_deref())?;
let sep = separator(options.drop_spaces);
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push_str(sep);
}
result.push_str(&integer_to_words(&number.integer, options.drop_spaces));
if let Some(fraction) = &number.fraction {
result.push_str(sep);
result.push_str(DECIMAL_SEP);
result.push_str(sep);
result.push_str(&decimal_part_to_words(fraction, options.drop_spaces));
}
Ok(result)
}
fn ordinal_suffix(word: &str) -> &'static str {
for c in word.chars().rev() {
match c {
'o' | 'u' => return "uncu",
'a' | 'ı' => return "ıncı",
'ö' | 'ü' => return "üncü",
'e' | 'i' => return "inci",
_ => {}
}
}
"inci"
}
fn integer_to_ordinal(n: &BigUint) -> String {
if let Some(small) = n.to_u64() {
if (1..=10).contains(&small) {
return ORDINAL_SPECIAL[small as usize].to_string();
}
}
let mut cardinal = integer_to_words(n, true);
cardinal.push_str(ordinal_suffix(&cardinal));
cardinal
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
Ok(integer_to_ordinal(&n))
}
options! {
pub struct CurrencyOptions {
currency: Currency = Currency::TRY, table = CURRENCY_VOCAB;
}
}
pub fn to_currency<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_currency_with(value, &CurrencyOptions::default())
}
pub fn to_currency_with<'a>(
value: impl Into<Number<'a>>,
options: &CurrencyOptions,
) -> Result<String> {
let words = CURRENCY_VOCAB.words(options.currency)?;
let money = parse_currency(&value.into(), options.currency.minor_unit_digits())?;
CURRENCY_MAX.check(&money.major, None)?;
assert_minor_unit(money.minor, options.currency)?;
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
if !money.major.is_zero() || money.minor == 0 {
result.push_str(&integer_to_words(&money.major, false));
result.push(' ');
result.push_str(words.major_at(0));
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push(' ');
}
result.push_str(&integer_to_words(&BigUint::from(money.minor), false));
result.push(' ');
result.push_str(words.minor_at(0));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "tr-TR",
name: "Turkish (Türkiye)",
kind: Kind::Full,
cardinal: Some(form!(with_options to_cardinal_with, CardinalOptions, CARDINAL_MAX)),
ordinal: Some(form!(plain to_ordinal, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};