#[cfg(not(feature = "std"))]
use alloc::{
format,
string::{String, ToString},
};
use crate::convert::convert_int_to_words;
use crate::decline::get_declension;
use crate::parse::{parse_fractional_digits, split_decimal, strip_sign};
use crate::{Error, Gender};
const PERCENT_ONE: &str = "процент";
const PERCENT_TWO: &str = "процента";
const PERCENT_FIVE: &str = "процентов";
pub fn percent(n: i64) -> String {
let words = convert_int_to_words(n, Gender::Masculine);
let unit = get_declension(n, PERCENT_ONE, PERCENT_TWO, PERCENT_FIVE);
format!("{words} {unit}")
}
pub fn percent_decimal(s: &str) -> Result<String, Error> {
percent_decimal_precision(s, 2)
}
pub fn percent_decimal_precision(s: &str, precision: u32) -> Result<String, Error> {
if precision == 0 || precision > 9 {
return Err(Error::InvalidNumber(format!(
"precision must be 1-9, got {precision}"
)));
}
let (negative, rest) = strip_sign(s);
let (whole_str, frac_opt) = split_decimal(rest);
let whole: i64 = if negative {
format!("-{whole_str}").parse()
} else {
whole_str.parse()
}
.map_err(|_| Error::InvalidNumber(format!("invalid whole part: '{whole_str}'")))?;
let negative_zero = negative && whole == 0;
match frac_opt {
None => {
let result = percent(whole);
if negative_zero {
Ok(format!("минус {result}"))
} else {
Ok(result)
}
}
Some(frac_str) => {
let frac_value = parse_fractional_digits(frac_str, precision)?;
if frac_value == 0 {
let result = percent(whole);
return if negative_zero {
Ok(format!("минус {result}"))
} else {
Ok(result)
};
}
let decimal = crate::decimal::decimal_str_to_words_precision_for(
whole,
frac_value,
precision,
negative_zero,
);
Ok(format!("{decimal} {PERCENT_TWO}"))
}
}
}
pub fn percent_word(n: i64) -> String {
get_declension(n, PERCENT_ONE, PERCENT_TWO, PERCENT_FIVE).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_percent_int() {
let cases: &[(i64, &str)] = &[
(0, "ноль процентов"),
(1, "один процент"),
(2, "два процента"),
(3, "три процента"),
(4, "четыре процента"),
(5, "пять процентов"),
(11, "одиннадцать процентов"),
(21, "двадцать один процент"),
(22, "двадцать два процента"),
(100, "сто процентов"),
(101, "сто один процент"),
(-5, "минус пять процентов"),
];
for &(n, expected) in cases {
assert_eq!(percent(n), expected, "percent({n})");
}
}
#[test]
fn test_percent_decimal_basic() {
assert_eq!(
percent_decimal("1.5").unwrap(),
"одна целая пятьдесят сотых процента"
);
assert_eq!(
percent_decimal("42.25").unwrap(),
"сорок две целых двадцать пять сотых процента"
);
assert_eq!(
percent_decimal("0.01").unwrap(),
"ноль целых одна сотая процента"
);
}
#[test]
fn test_percent_decimal_precision() {
assert_eq!(
percent_decimal_precision("3.14159", 5).unwrap(),
"три целых четырнадцать тысяч сто пятьдесят девять стотысячных процента"
);
assert_eq!(
percent_decimal_precision("-0.5", 1).unwrap(),
"минус ноль целых пять десятых процента"
);
}
#[test]
fn test_percent_decimal_no_fraction() {
assert_eq!(percent_decimal("100").unwrap(), "сто процентов");
assert_eq!(percent_decimal("1").unwrap(), "один процент");
assert_eq!(percent_decimal("100.00").unwrap(), "сто процентов");
}
#[test]
fn test_percent_decimal_errors() {
assert!(percent_decimal("abc").is_err());
assert!(percent_decimal_precision("1.5", 0).is_err());
assert!(percent_decimal_precision("1.5", 10).is_err());
}
#[test]
fn test_percent_decimal_i64_min_boundary() {
assert!(percent_decimal_precision("-9223372036854775808.5", 1).is_ok());
}
#[test]
fn test_percent_word() {
assert_eq!(percent_word(1), "процент");
assert_eq!(percent_word(3), "процента");
assert_eq!(percent_word(5), "процентов");
assert_eq!(percent_word(21), "процент");
}
}