use crate::Result;
use crate::currency::vocab::SV 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::{One, ToPrimitive, Zero};
const ONES: [&str; 10] = [
"", "ett", "två", "tre", "fyra", "fem", "sex", "sju", "åtta", "nio",
];
const TEENS: [&str; 10] = [
"tio", "elva", "tolv", "tretton", "fjorton", "femton", "sexton", "sjutton", "arton", "nitton",
];
const TENS: [&str; 10] = [
"", "", "tjugo", "trettio", "fyrtio", "femtio", "sextio", "sjuttio", "åttio", "nittio",
];
const HUNDRED: &str = "hundra";
const ZERO: &str = "noll";
const NEGATIVE: &str = "minus";
const DECIMAL_SEP: &str = "komma";
const SCALES: &[&str] = &[
"tusen",
"miljon",
"miljard",
"biljon",
"biljard",
"triljon",
"triljard",
"kvadriljon",
];
pub const CARDINAL_MAX: Max = Max::western(SCALES.len() as u32);
pub const ORDINAL_MAX: Max = Max::western(SCALES.len() as u32);
pub const CURRENCY_MAX: Max = Max::western(SCALES.len() as u32);
const ORDINAL_ONES: [&str; 13] = [
"", "första", "andra", "tredje", "fjärde", "femte", "sjätte", "sjunde", "åttonde", "nionde",
"tionde", "elfte", "tolfte",
];
struct Segment {
word: String,
has_hundred: bool,
less_than_100: bool,
}
fn build_segment(n: u32) -> Segment {
if n == 0 {
return Segment {
word: String::new(),
has_hundred: false,
less_than_100: false,
};
}
let ones = (n % 10) as usize;
let tens = ((n / 10) % 10) as usize;
let hundreds = (n / 100) as usize;
let hundred_word = match hundreds {
0 => None,
1 => Some(HUNDRED.to_string()),
h => Some(format!("{} {HUNDRED}", ONES[h])),
};
let tens_ones_word = if tens == 1 {
TEENS[ones].to_string()
} else if tens >= 2 {
if ones > 0 {
format!("{}-{}", TENS[tens], ONES[ones])
} else {
TENS[tens].to_string()
}
} else {
ONES[ones].to_string()
};
match hundred_word {
Some(h) if !tens_ones_word.is_empty() => Segment {
word: format!("{h} och {tens_ones_word}"),
has_hundred: true,
less_than_100: false,
},
Some(h) => Segment {
word: h,
has_hundred: true,
less_than_100: false,
},
None => Segment {
word: tens_ones_word,
has_hundred: false,
less_than_100: true,
},
}
}
fn integer_to_words(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_segment(small as u32).word;
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
let mut result = if thousands == 1 {
SCALES[0].to_string()
} else {
format!("{} {}", build_segment(thousands).word, SCALES[0])
};
if remainder > 0 {
let remainder_result = build_segment(remainder);
if remainder_result.less_than_100 {
result.push_str(" och ");
} else {
result.push(' ');
}
result.push_str(&remainder_result.word);
}
return result;
}
}
build_large_number_words(n)
}
struct Part {
word: String,
has_hundred: bool,
is_scale: bool,
}
fn build_large_number_words(n: &BigUint) -> String {
let segments = split_groups(n, 1000);
let mut parts: Vec<Part> = Vec::new();
for (scale_index, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
if scale_index == 0 {
let segment_result = build_segment(segment);
parts.push(Part {
word: segment_result.word,
has_hundred: segment_result.has_hundred,
is_scale: false,
});
} else {
let scale_word = SCALES[scale_index - 1];
let segment_word = if segment == 1 {
if scale_index == 1 {
String::new() } else {
"en".to_string() }
} else {
build_segment(segment).word
};
if !segment_word.is_empty() {
parts.push(Part {
word: segment_word,
has_hundred: false,
is_scale: false,
});
}
parts.push(Part {
word: scale_word.to_string(),
has_hundred: false,
is_scale: true,
});
}
}
join_swedish_parts(&parts)
}
fn join_swedish_parts(parts: &[Part]) -> String {
match parts {
[] => return ZERO.to_string(),
[only] => return only.word.clone(),
_ => {}
}
let mut result: Vec<&str> = Vec::with_capacity(parts.len() + 1);
let last = parts.len() - 1;
for (i, part) in parts.iter().enumerate() {
if i == last {
let prev_part = &parts[i - 1];
if prev_part.is_scale && !part.has_hundred {
result.push("och");
}
}
result.push(&part.word);
}
result.join(" ")
}
fn decimal_part_to_words(decimal: &str) -> String {
let mut result = String::new();
let significant = decimal.trim_start_matches('0');
for _ in 0..decimal.len() - significant.len() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(ZERO);
}
if !significant.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(&integer_to_words(&digits_to_biguint(significant)));
}
result
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let number = parse_cardinal(&value.into())?;
CARDINAL_MAX.check(&number.integer, number.fraction.as_deref())?;
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&number.integer));
if let Some(fraction) = &number.fraction {
result.push(' ');
result.push_str(DECIMAL_SEP);
result.push(' ');
result.push_str(&decimal_part_to_words(fraction));
}
Ok(result)
}
fn integer_to_ordinal(n: &BigUint) -> String {
if let Some(small) = n.to_u64() {
if (1..=12).contains(&small) {
return ORDINAL_ONES[small as usize].to_string();
}
}
let mut result = integer_to_words(n);
result.push_str("de");
result
}
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::SEK, 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 {
if money.major.is_one() {
result.push_str("en ");
result.push_str(words.major_at(0));
} else {
result.push_str(&integer_to_words(&money.major));
result.push(' ');
result.push_str(words.major_at(1));
}
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push_str(" och ");
}
result.push_str(&integer_to_words(&BigUint::from(money.minor)));
result.push(' ');
result.push_str(words.minor_at(0));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "sv-SE",
name: "Swedish (Sweden)",
kind: Kind::Full,
cardinal: Some(form!(plain to_cardinal, CARDINAL_MAX)),
ordinal: Some(form!(plain to_ordinal, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};