use crate::base::{
clean, set_low_numwords, set_mid_numwords, splitnum, Cards, KwVal, Kwargs, Lang, N2WError,
Node, Result,
};
use crate::currency::{parse_currency_parts, prefix_currency, CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::cmp::Ordering;
use std::collections::HashMap;
const ERA_START: &[(i64, &str, &str)] = &[
(645, "大化", "たいか"),
(650, "白雉", "はくち"),
(686, "朱鳥", "しゅちょう"),
(701, "大宝", "たいほう"),
(704, "慶雲", "けいうん"),
(708, "和銅", "わどう"),
(715, "霊亀", "れいき"),
(717, "養老", "ようろう"),
(724, "神亀", "じんき"),
(729, "天平", "てんぴょう"),
(749, "天平感宝", "てんぴょうかんぽう"),
(749, "天平勝宝", "てんぴょうしょうほう"),
(757, "天平宝字", "てんぴょうじょうじ"),
(765, "天平神護", "てんぴょうじんご"),
(767, "神護景雲", "じんごけいうん"),
(770, "宝亀", "ほうき"),
(781, "天応", "てんおう"),
(782, "延暦", "えんりゃく"),
(806, "大同", "だいどう"),
(810, "弘仁", "こうにん"),
(823, "天長", "てんちょう"),
(834, "承和", "じょうわ"),
(848, "嘉祥", "かしょう"),
(851, "仁寿", "にんじゅ"),
(855, "斉衡", "さいこう"),
(857, "天安", "てんあん"),
(859, "貞観", "じょうがん"),
(877, "元慶", "がんぎょう"),
(885, "仁和", "にんな"),
(889, "寛平", "かんぴょう"),
(898, "昌泰", "しょうたい"),
(901, "延喜", "えんぎ"),
(923, "延長", "えんちょう"),
(931, "承平", "じょうへい"),
(938, "天慶", "てんぎょう"),
(947, "天暦", "てんりゃく"),
(957, "天徳", "てんとく"),
(961, "応和", "おうわ"),
(964, "康保", "こうほう"),
(968, "安和", "あんな"),
(970, "天禄", "てんろく"),
(974, "天延", "てんえん"),
(976, "貞元", "じょうげん"),
(979, "天元", "てんげん"),
(983, "永観", "えいかん"),
(985, "寛和", "かんな"),
(987, "永延", "えいえん"),
(989, "永祚", "えいそ"),
(990, "正暦", "しょうりゃく"),
(995, "長徳", "ちょうとく"),
(999, "長保", "ちょうほう"),
(1004, "寛弘", "かんこう"),
(1013, "長和", "ちょうわ"),
(1017, "寛仁", "かんにん"),
(1021, "治安", "じあん"),
(1024, "万寿", "まんじゅ"),
(1028, "長元", "ちょうげん"),
(1037, "長暦", "ちょうりゃく"),
(1040, "長久", "ちょうきゅう"),
(1045, "寛徳", "かんとく"),
(1046, "永承", "えいしょう"),
(1053, "天喜", "てんぎ"),
(1058, "康平", "こうへい"),
(1065, "治暦", "じりゃく"),
(1069, "延久", "えんきゅう"),
(1074, "承保", "じょうほう"),
(1078, "承暦", "じょうりゃく"),
(1081, "永保", "えいほう"),
(1084, "応徳", "おうとく"),
(1087, "寛治", "かんじ"),
(1095, "嘉保", "かほう"),
(1097, "永長", "えいちょう"),
(1098, "承徳", "じょうとく"),
(1099, "康和", "こうわ"),
(1104, "長治", "ちょうじ"),
(1106, "嘉承", "かじょう"),
(1108, "天仁", "てんにん"),
(1110, "天永", "てんねい"),
(1113, "永久", "えいきゅう"),
(1118, "元永", "げんえい"),
(1120, "保安", "ほうあん"),
(1124, "天治", "てんじ"),
(1126, "大治", "だいじ"),
(1131, "天承", "てんしょう"),
(1132, "長承", "ちょうしょう"),
(1135, "保延", "ほうえん"),
(1141, "永治", "えいじ"),
(1142, "康治", "こうじ"),
(1144, "天養", "てんよう"),
(1145, "久安", "きゅうあん"),
(1151, "仁平", "にんぺい"),
(1154, "久寿", "きゅうじゅ"),
(1156, "保元", "ほうげん"),
(1159, "平治", "へいじ"),
(1160, "永暦", "えいりゃく"),
(1161, "応保", "おうほう"),
(1163, "長寛", "ちょうかん"),
(1165, "永万", "えいまん"),
(1166, "仁安", "にんあん"),
(1169, "嘉応", "かおう"),
(1171, "承安", "しょうあん"),
(1175, "安元", "あんげん"),
(1177, "治承", "じしょう"),
(1181, "養和", "ようわ"),
(1182, "寿永", "じゅえい"),
(1184, "元暦", "げんりゃく"),
(1185, "文治", "ぶんじ"),
(1190, "建久", "けんきゅう"),
(1199, "正治", "しょうじ"),
(1201, "建仁", "けんにん"),
(1204, "元久", "げんきゅう"),
(1206, "建永", "けんえい"),
(1207, "承元", "じょうげん"),
(1211, "建暦", "けんりゃく"),
(1214, "建保", "けんぽう"),
(1219, "承久", "じょうきゅう"),
(1222, "貞応", "じょうおう"),
(1225, "元仁", "げんにん"),
(1225, "嘉禄", "かろく"),
(1228, "安貞", "あんてい"),
(1229, "寛喜", "かんき"),
(1232, "貞永", "じょうえい"),
(1233, "天福", "てんぷく"),
(1235, "文暦", "ぶんりゃく"),
(1235, "嘉禎", "かてい"),
(1239, "暦仁", "りゃくにん"),
(1239, "延応", "えんおう"),
(1240, "仁治", "にんじ"),
(1243, "寛元", "かんげん"),
(1247, "宝治", "ほうじ"),
(1249, "建長", "けんちょう"),
(1256, "康元", "こうげん"),
(1257, "正嘉", "しょうか"),
(1259, "正元", "しょうげん"),
(1260, "文応", "ぶんおう"),
(1261, "弘長", "こうちょう"),
(1264, "文永", "ぶんえい"),
(1275, "健治", "けんじ"),
(1278, "弘安", "こうあん"),
(1288, "正応", "しょうおう"),
(1293, "永仁", "えいにん"),
(1299, "正安", "しょうあん"),
(1303, "乾元", "けんげん"),
(1303, "嘉元", "かげん"),
(1307, "徳治", "とくじ"),
(1308, "延慶", "えんきょう"),
(1311, "応長", "おうちょう"),
(1312, "正和", "しょうわ"),
(1317, "文保", "ぶんぽう"),
(1319, "元応", "げんおう"),
(1321, "元亨", "げんこう"),
(1325, "正中", "しょうちゅ"),
(1326, "嘉暦", "かりゃく"),
(1329, "元徳", "げんとく"),
(1331, "元弘", "げんこう"),
(1332, "正慶", "しょうけい"),
(1334, "建武", "けんむ"),
(1336, "延元", "えいげん"),
(1338, "暦応", "りゃくおう"),
(1340, "興国", "こうこく"),
(1342, "康永", "こうえい"),
(1345, "貞和", "じょうわ"),
(1347, "正平", "しょうへい"),
(1350, "観応", "かんおう"),
(1352, "文和", "ぶんな"),
(1356, "延文", "えんぶん"),
(1361, "康安", "こうあん"),
(1362, "貞治", "じょうじ"),
(1368, "応安", "おうあん"),
(1370, "建徳", "けんとく"),
(1372, "文中", "ぶんちゅう"),
(1375, "永和", "えいわ"),
(1375, "天授", "てんじゅ"),
(1379, "康暦", "こうりゃく"),
(1381, "永徳", "えいとく"),
(1381, "弘和", "こうわ"),
(1384, "至徳", "しとく"),
(1384, "元中", "げんちゅう"),
(1387, "嘉慶", "かけい"),
(1389, "康応", "こうおう"),
(1390, "明徳", "めいとく"),
(1394, "応永", "おうえい"),
(1428, "正長", "しょうちょう"),
(1429, "永享", "えいきょう"),
(1441, "嘉吉", "かきつ"),
(1444, "文安", "ぶんあん"),
(1449, "宝徳", "ほうとく"),
(1452, "享徳", "きょうとく"),
(1455, "康正", "こうしょう"),
(1457, "長禄", "ちょうろく"),
(1461, "寛正", "かんしょう"),
(1466, "文正", "ぶんしょう"),
(1467, "応仁", "おうにん"),
(1469, "文明", "ぶんめい"),
(1487, "長享", "ちょうきょう"),
(1489, "延徳", "えんとく"),
(1492, "明応", "めいおう"),
(1501, "文亀", "ぶんき"),
(1504, "永正", "えいしょう"),
(1521, "大永", "だいえい"),
(1528, "享禄", "きょうろく"),
(1532, "天文", "てんぶん"),
(1555, "弘治", "こうじ"),
(1558, "永禄", "えいろく"),
(1570, "元亀", "げんき"),
(1573, "天正", "てんしょう"),
(1593, "文禄", "ぶんろく"),
(1596, "慶長", "けいちょう"),
(1615, "元和", "げんな"),
(1624, "寛永", "かんえい"),
(1645, "正保", "しょうほう"),
(1648, "慶安", "けいあん"),
(1652, "承応", "じょうおう"),
(1655, "明暦", "めいれき"),
(1658, "万治", "まんじ"),
(1661, "寛文", "かんぶん"),
(1673, "延宝", "えんぽう"),
(1681, "天和", "てんな"),
(1684, "貞享", "じょうきょう"),
(1688, "元禄", "げんろく"),
(1704, "宝永", "ほうえい"),
(1711, "正徳", "しょうとく"),
(1716, "享保", "きょうほう"),
(1736, "元文", "げんぶん"),
(1741, "寛保", "かんぽう"),
(1744, "延享", "えんきょう"),
(1748, "寛延", "かんえん"),
(1751, "宝暦", "ほうれき"),
(1764, "明和", "めいわ"),
(1773, "安永", "あんえい"),
(1781, "天明", "てんめい"),
(1801, "寛政", "かんせい"),
(1802, "享和", "きょうわ"),
(1804, "文化", "ぶんか"),
(1818, "文政", "ぶんせい"),
(1831, "天保", "てんぽう"),
(1845, "弘化", "こうか"),
(1848, "嘉永", "かえい"),
(1855, "安政", "あんせい"),
(1860, "万延", "まんえい"),
(1861, "文久", "ぶんきゅう"),
(1864, "元治", "げんじ"),
(1865, "慶応", "けいおう"),
(1868, "明治", "めいじ"),
(1912, "大正", "たいしょう"),
(1926, "昭和", "しょうわ"),
(1989, "平成", "へいせい"),
(2019, "令和", "れいわ"),
];
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("JPY", CurrencyForms::new(&["円", "えん"], &["銭", "銭"]));
m.insert(
"EUR",
CurrencyForms::new(&["ユーロ", "ユーロ"], &["セント", "セント"]),
);
m.insert(
"USD",
CurrencyForms::new(&["ドル", "ドル"], &["セント", "セント"]),
);
m.insert(
"GBP",
CurrencyForms::new(&["ポンド", "ポンド"], &["ペンス", "ペンス"]),
);
m.insert("CNY", CurrencyForms::new(&["元", "元"], &["分", "分"]));
m
}
fn float_repr_precision(f: f64) -> u32 {
let s = format!("{}", f);
match s.split_once('.') {
Some((_, frac)) if !frac.contains('e') => frac.len() as u32,
_ => 0,
}
}
struct KwCard {
num: BigInt,
kanji: Vec<&'static str>,
kana: Vec<&'static str>,
}
fn select_text(card: &KwCard, reading: bool, prefer: &[String]) -> &'static str {
let alts = if reading { &card.kana } else { &card.kanji };
if alts.len() > 1 {
let common: Vec<&'static str> = alts
.iter()
.copied()
.filter(|a| prefer.iter().any(|p| p == a))
.collect();
if common.len() == 1 {
return common[0];
}
}
alts[0]
}
#[derive(Clone)]
enum JaNode {
Pair(String, BigInt),
List(Vec<JaNode>),
}
fn rendaku_merge_pairs(l: (String, BigInt), r: (String, BigInt)) -> (String, BigInt) {
let (mut lt, ln) = l;
let (mut rt, rn) = r;
if rt == "ひゃく" && rn == BigInt::from(100) {
if lt == "さん" && ln == BigInt::from(3) {
rt = "びゃく".to_string();
} else if lt == "ろく" && ln == BigInt::from(6) {
lt = "ろっ".to_string();
rt = "ぴゃく".to_string();
} else if lt == "はち" && ln == BigInt::from(8) {
lt = "はっ".to_string();
rt = "ぴゃく".to_string();
}
} else if rt == "せん" && rn == BigInt::from(1000) {
if lt == "さん" && ln == BigInt::from(3) {
rt = "ぜん".to_string();
} else if lt == "はち" && ln == BigInt::from(8) {
lt = "はっ".to_string();
}
} else if rt == "ちょう" && rn == BigInt::from(10u64).pow(12) {
if lt == "いち" && ln.is_one() {
lt = "いっ".to_string();
} else if lt == "はち" && ln == BigInt::from(8) {
lt = "はっ".to_string();
} else if lt == "じゅう" && ln == BigInt::from(10) {
lt = "じゅっ".to_string();
}
} else if rt == "けい" && rn == BigInt::from(10u64).pow(16) {
if lt == "いち" && ln.is_one() {
lt = "いっ".to_string();
} else if lt == "ろく" && ln == BigInt::from(6) {
lt = "ろっ".to_string();
} else if lt == "はち" && ln == BigInt::from(8) {
lt = "はっ".to_string();
} else if lt == "じゅう" && ln == BigInt::from(10) {
lt = "じゅっ".to_string();
} else if lt == "ひゃく" && ln == BigInt::from(100) {
lt = "ひゃっ".to_string();
}
}
(format!("{}{}", lt, rt), ln * rn)
}
fn era_index(cmp: &dyn Fn(i64) -> Ordering, prefer: &[String]) -> usize {
let last_era_idx = ERA_START.len() as i64 - 1;
let mut first: i64 = 0;
let mut last: i64 = last_era_idx;
let mut era_idx: Option<i64> = None;
while era_idx.is_none() {
let mid = (first + last).div_euclid(2);
let m = mid as usize;
if mid == last_era_idx
|| (cmp(ERA_START[m].0) != Ordering::Less && cmp(ERA_START[m + 1].0) == Ordering::Less)
{
era_idx = Some(mid);
if !prefer.is_empty() {
let mut i = mid - 1;
while i >= 0 && cmp(ERA_START[i as usize].0) == Ordering::Equal {
let e = ERA_START[i as usize];
if prefer.iter().any(|p| p == e.1 || p == e.2) {
era_idx = Some(i);
break;
}
i -= 1;
}
}
}
if cmp(ERA_START[m].0) == Ordering::Less {
last = mid - 1;
} else {
first = mid + 1;
}
}
era_idx.unwrap() as usize
}
#[derive(PartialEq)]
enum JaReading {
Off,
Kana,
Arabic,
}
impl JaReading {
fn is_on(&self) -> bool {
!matches!(self, JaReading::Off)
}
}
fn kw_truthy(v: &KwVal) -> bool {
match v {
KwVal::Bool(b) => *b,
KwVal::Int(i) => *i != 0,
KwVal::Str(s) => !s.is_empty(),
KwVal::List(l) => !l.is_empty(),
KwVal::None => false,
}
}
fn parse_reading(kw: &Kwargs) -> JaReading {
match kw.get("reading") {
Option::None | Some(KwVal::None) => JaReading::Off,
Some(KwVal::Str(s)) if s == "arabic" => JaReading::Arabic,
Some(v) => {
if kw_truthy(v) {
JaReading::Kana
} else {
JaReading::Off
}
}
}
}
fn parse_prefer(kw: &Kwargs) -> Result<Vec<String>> {
match kw.get("prefer") {
Option::None | Some(KwVal::None) => Ok(Vec::new()),
Some(KwVal::List(l)) => Ok(l.clone()),
Some(KwVal::Str(s)) => Ok(s.chars().map(String::from).collect()),
Some(KwVal::Bool(false)) | Some(KwVal::Int(0)) => Ok(Vec::new()),
Some(_) => Err(N2WError::Fallback("kwargs".into())),
}
}
fn chop_last3(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let keep = chars.len().saturating_sub(3);
chars[..keep].iter().collect()
}
fn py_value_str(value: &FloatValue) -> String {
match value {
FloatValue::Float { value: f, .. } => {
if f.abs() >= 1e16 {
let s = format!("{:e}", f);
match s.split_once('e') {
Some((m, e)) if !e.starts_with('-') => format!("{}e+{}", m, e),
_ => s,
}
} else if f.fract() == 0.0 {
format!("{:.1}", f) } else {
format!("{}", f)
}
}
FloatValue::Decimal { value: d, .. } => crate::strnum::python_decimal_str(d),
}
}
pub struct LangJa {
cards: Cards,
maxval: BigInt,
kw_cards: Vec<KwCard>,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangJa {
fn default() -> Self {
Self::new()
}
}
impl LangJa {
pub fn new() -> Self {
let mut cards = Cards::new();
let high = [
"極", "載", "正", "澗", "溝", "穣", "秭", "垓", "京", "兆", "億", "万", ];
let mut n: u32 = 4 * high.len() as u32;
for word in high.iter() {
cards.insert(BigInt::from(10u8).pow(n), *word);
n -= 4;
}
set_mid_numwords(&mut cards, &[(1000, "千"), (100, "百")]);
set_low_numwords(
&mut cards,
&[
"十", "九", "八", "七", "六", "五", "四", "三", "二", "一", "零", ],
);
let maxval = cards.highest().cloned().unwrap() * BigInt::from(1000);
let mut kw_cards: Vec<KwCard> = Vec::new();
let kw_high: [(u32, &str, &str); 12] = [
(48, "極", "ごく"),
(44, "載", "さい"),
(40, "正", "せい"),
(36, "澗", "かん"),
(32, "溝", "こう"),
(28, "穣", "じょう"),
(24, "秭", "し"),
(20, "垓", "がい"),
(16, "京", "けい"),
(12, "兆", "ちょう"),
(8, "億", "おく"),
(4, "万", "まん"),
];
for (exp, kanji, kana) in kw_high {
kw_cards.push(KwCard {
num: BigInt::from(10u8).pow(exp),
kanji: vec![kanji],
kana: vec![kana],
});
}
kw_cards.push(KwCard {
num: BigInt::from(1000),
kanji: vec!["千"],
kana: vec!["せん"],
});
kw_cards.push(KwCard {
num: BigInt::from(100),
kanji: vec!["百"],
kana: vec!["ひゃく"],
});
let kw_low: [(u32, Vec<&'static str>, Vec<&'static str>); 11] = [
(10, vec!["十"], vec!["じゅう"]),
(9, vec!["九"], vec!["きゅう"]),
(8, vec!["八"], vec!["はち"]),
(7, vec!["七"], vec!["なな", "しち"]),
(6, vec!["六"], vec!["ろく"]),
(5, vec!["五"], vec!["ご"]),
(4, vec!["四"], vec!["よん", "し"]),
(3, vec!["三"], vec!["さん"]),
(2, vec!["二"], vec!["に"]),
(1, vec!["一"], vec!["いち"]),
(0, vec!["零", "〇"], vec!["ゼロ", "れい"]),
];
for (n, kanji, kana) in kw_low {
kw_cards.push(KwCard {
num: BigInt::from(n),
kanji,
kana,
});
}
LangJa {
cards,
maxval,
kw_cards,
currency_forms: build_currency_forms(),
}
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
Ok(())
}
fn ordinal_suffix(&self) -> &'static str {
"番目"
}
fn cents_as_hundredths(&self, right: &BigInt) -> Result<String> {
let r = match right.to_u32() {
Some(r) if r <= 99 => r,
_ => unreachable!("JA cents out of the 0..=99 domain: {}", right),
};
let (precision, post) = if r % 10 == 0 {
(1usize, r / 10)
} else {
(2usize, r)
};
let mut out = self.to_cardinal(&BigInt::zero())?;
out.push_str(self.pointword());
for ch in format!("{:0>width$}", post, width = precision).chars() {
let digit = BigInt::from(ch.to_digit(10).unwrap());
out.push_str(&self.to_cardinal(&digit)?);
}
Ok(out)
}
fn kw_card_one(&self) -> &KwCard {
self.kw_cards.iter().find(|c| c.num.is_one()).unwrap()
}
fn splitnum_kw(&self, value: &BigInt, reading: bool, prefer: &[String]) -> Vec<JaNode> {
for card in &self.kw_cards {
if card.num > *value {
continue;
}
let (div, mod_) = if value.is_zero() {
(BigInt::one(), BigInt::zero())
} else {
(value / &card.num, value % &card.num)
};
let mut out: Vec<JaNode> = Vec::new();
if div.is_one() {
out.push(JaNode::Pair(
select_text(self.kw_card_one(), reading, prefer).to_string(),
BigInt::one(),
));
} else if &div == value {
let n = div.to_usize().expect("JA tally count");
return vec![JaNode::Pair(
select_text(card, reading, prefer).repeat(n),
div * &card.num,
)];
} else {
out.push(JaNode::List(self.splitnum_kw(&div, reading, prefer)));
}
out.push(JaNode::Pair(
select_text(card, reading, prefer).to_string(),
card.num.clone(),
));
if !mod_.is_zero() {
out.push(JaNode::List(self.splitnum_kw(&mod_, reading, prefer)));
}
return out;
}
unreachable!("JA splitnum_kw: no card <= {}", value)
}
fn merge_kw(&self, l: (String, BigInt), r: (String, BigInt)) -> (String, BigInt) {
let (lt, ln) = l;
let (rt, rn) = r;
if ln.is_one() && rn < BigInt::from(10000) {
return (rt, rn);
}
match ln.cmp(&rn) {
Ordering::Greater => (format!("{}{}", lt, rt), ln + rn),
Ordering::Less => rendaku_merge_pairs((lt, ln), (rt, rn)),
Ordering::Equal => unreachable!(
"JA merge_kw: lnum == rnum ({}) is unreachable; Python returns None here",
ln
),
}
}
fn clean_kw(&self, mut val: Vec<JaNode>) -> (String, BigInt) {
while val.len() != 1 {
let first_two_pairs =
matches!(&val[0], JaNode::Pair(..)) && matches!(&val[1], JaNode::Pair(..));
let mut out: Vec<JaNode> = Vec::new();
if first_two_pairs {
let mut it = val.into_iter();
let (a, b) = (it.next().unwrap(), it.next().unwrap());
if let (JaNode::Pair(lt, ln), JaNode::Pair(rt, rn)) = (a, b) {
let (t, n) = self.merge_kw((lt, ln), (rt, rn));
out.push(JaNode::Pair(t, n));
} else {
unreachable!()
}
let rest: Vec<JaNode> = it.collect();
if !rest.is_empty() {
out.push(JaNode::List(rest));
}
} else {
for elem in val {
match elem {
JaNode::List(l) => {
if l.len() == 1 {
out.push(l.into_iter().next().unwrap());
} else {
let (t, n) = self.clean_kw(l);
out.push(JaNode::Pair(t, n));
}
}
pair => out.push(pair),
}
}
}
val = out;
}
match val.into_iter().next().unwrap() {
JaNode::Pair(t, n) => (t, n),
JaNode::List(l) => self.clean_kw(l),
}
}
fn cardinal_kw_int(&self, value: &BigInt, reading: bool, prefer: &[String]) -> Result<String> {
let mut out = String::new();
let mut v = value.clone();
if v.is_negative() {
v = v.abs();
out = self.negword().to_string(); }
if &v >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
v,
self.maxval()
)));
}
let tree = self.splitnum_kw(&v, reading, prefer);
let (words, _) = self.clean_kw(tree);
Ok(format!("{}{}", out, words))
}
fn cardinal_float_impl(
&self,
value: &FloatValue,
reading: bool,
prefer: &[String],
) -> Result<String> {
let eff_prefer: Vec<String> = if prefer.is_empty() {
vec!["れい".to_string()]
} else {
prefer.to_vec()
};
let (f, precision) = match value {
FloatValue::Float { value, precision } => (*value, *precision),
FloatValue::Decimal { value, .. } => {
let f = value
.to_f64()
.ok_or_else(|| N2WError::Value(format!("cannot represent {} as f64", value)))?;
(f, float_repr_precision(f))
}
};
let (pre, post) = float2tuple(&FloatValue::Float {
value: f,
precision,
});
let post_str = post.to_string();
let post_str = format!(
"{}{}",
"0".repeat((precision as usize).saturating_sub(post_str.len())),
post_str
);
let mut out = vec![self.cardinal_kw_int(&pre, reading, &eff_prefer)?];
if value.is_negative() && pre.is_zero() {
out.insert(0, self.negword().to_string());
}
if precision > 0 {
out.push((if reading { "てん" } else { "点" }).to_string());
}
for ch in post_str.chars().take(precision as usize) {
let d = ch
.to_digit(10)
.ok_or_else(|| N2WError::Value(format!("non-digit {:?} in fractional part", ch)))?;
out.push(self.cardinal_kw_int(&BigInt::from(d), reading, &eff_prefer)?);
}
Ok(out.join(""))
}
fn cents_as_hundredths_kw(
&self,
right: &BigInt,
reading: bool,
prefer: &[String],
) -> Result<String> {
let r = match right.to_u32() {
Some(r) if r <= 99 => r,
_ => unreachable!("JA cents out of the 0..=99 domain: {}", right),
};
let (precision, post) = if r % 10 == 0 {
(1usize, r / 10)
} else {
(2usize, r)
};
let eff_prefer: Vec<String> = if prefer.is_empty() {
vec!["れい".to_string()]
} else {
prefer.to_vec()
};
let mut out = self.cardinal_kw_int(&BigInt::zero(), reading, &eff_prefer)?;
out.push_str(if reading { "てん" } else { "点" });
for ch in format!("{:0>width$}", post, width = precision).chars() {
let digit = BigInt::from(ch.to_digit(10).unwrap());
out.push_str(&self.cardinal_kw_int(&digit, reading, &eff_prefer)?);
}
Ok(out)
}
fn ordinal_suffix_kw(&self, reading: bool, counter: &str) -> Result<String> {
if reading {
if counter == "番" {
Ok("ばんめ".to_string())
} else {
Err(N2WError::Custom {
module: "builtins",
class: "NotImplementedError",
msg: format!("Reading not implemented for {}", counter),
})
}
} else {
Ok(format!("{}目", counter))
}
}
fn year_era_int(
&self,
year: &BigInt,
reading: &JaReading,
prefer: &[String],
) -> Result<String> {
let min_year = ERA_START[0].0; if *year < BigInt::from(min_year) {
return Err(N2WError::Value(format!(
"Can't convert years less than {} to era",
min_year
)));
}
let idx = era_index(&|s| year.cmp(&BigInt::from(s)), prefer);
let era = ERA_START[idx];
let era_year = year - BigInt::from(era.0) + BigInt::one();
match reading {
JaReading::Arabic => Ok(format!("{}{}年", era.1, era_year)),
JaReading::Kana => {
let mut words = if era_year.is_one() {
"がん".to_string()
} else {
self.cardinal_kw_int(&era_year, true, prefer)?
};
if &era_year % BigInt::from(10) == BigInt::from(9) {
words = format!("{}く", chop_last3(&words));
}
Ok(format!("{}{}ねん", era.2, words))
}
JaReading::Off => {
let words = if era_year.is_one() {
"元".to_string()
} else {
self.to_cardinal(&era_year)?
};
Ok(format!("{}{}年", era.1, words))
}
}
}
}
impl Lang for LangJa {
fn default_currency(&self) -> &str {
"JPY"
}
fn default_separator(&self) -> &str {
""
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
"マイナス"
}
fn pointword(&self) -> &str {
"点"
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
let (ltext, lnum) = l;
let (rtext, rnum) = r;
let myriad = BigInt::from(10000);
if lnum.is_one() && rnum < &myriad {
(rtext.to_string(), rnum.clone())
} else if lnum > rnum {
(format!("{}{}", ltext, rtext), lnum + rnum)
} else if lnum < rnum {
(format!("{}{}", ltext, rtext), lnum * rnum)
} else {
unreachable!(
"JA merge: lnum == rnum ({}) is unreachable; Python returns None here",
lnum
)
}
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let mut out = String::new();
let mut v = value.clone();
if v.is_negative() {
v = v.abs();
out = self.negword().to_string();
}
if &v >= self.maxval() {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
v,
self.maxval()
)));
}
let tree = splitnum(self, &v).ok_or_else(|| {
N2WError::Overflow(format!("abs({}) must be less than {}.", v, self.maxval()))
})?;
let words = match clean(self, tree) {
Node::Leaf(t, _) => t,
Node::List(_) => return Err(N2WError::Type("clean did not reduce".into())),
};
Ok(self.title(&format!("{}{}", out, words)))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
let base = self.to_cardinal(value)?;
Ok(format!("{}{}", base, self.ordinal_suffix()))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", value, self.ordinal_suffix()))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.year_era_int(value, &JaReading::Off, &[])
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
match value.as_whole_int() {
Option::None => Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
py_value_str(value)
))),
Some(i) => {
if i.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
py_value_str(value)
)));
}
Ok(format!(
"{}{}",
self.to_cardinal(&i)?,
self.ordinal_suffix()
))
}
}
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}{}", repr_str, self.ordinal_suffix()))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
let min_year = ERA_START[0].0; let too_early =
|| N2WError::Value(format!("Can't convert years less than {} to era", min_year));
match value {
FloatValue::Float { value: f, .. } => {
let f = *f;
if f < min_year as f64 {
return Err(too_early());
}
let idx = era_index(&|s| f.partial_cmp(&(s as f64)).expect("finite year"), &[]);
let era = ERA_START[idx];
let era_year = f - era.0 as f64 + 1.0;
let words = if era_year == 1.0 {
"元".to_string()
} else {
let fv = FloatValue::Float {
value: era_year,
precision: float_repr_precision(era_year),
};
match fv.as_whole_int() {
Some(i) => self.to_cardinal(&i)?,
Option::None => self.to_cardinal_float(&fv, None)?,
}
};
Ok(format!("{}{}年", era.1, words))
}
FloatValue::Decimal { value: d, .. } => {
if *d < BigDecimal::from(min_year) {
return Err(too_early());
}
let idx = era_index(&|s| d.cmp(&BigDecimal::from(s)), &[]);
let era = ERA_START[idx];
let era_year = d - BigDecimal::from(era.0) + BigDecimal::from(1);
let words = if era_year == BigDecimal::from(1) {
"元".to_string()
} else {
let fv = FloatValue::Decimal {
value: era_year,
precision: 0,
};
match fv.as_whole_int() {
Some(i) => self.to_cardinal(&i)?,
Option::None => self.to_cardinal_float(&fv, None)?,
}
};
Ok(format!("{}{}年", era.1, words))
}
}
}
fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if kw.is_empty() {
return self.to_cardinal(value);
}
if !kw.only(&["reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let reading = parse_reading(kw);
let prefer = parse_prefer(kw)?;
self.cardinal_kw_int(value, reading.is_on(), &prefer)
}
fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if kw.is_empty() {
return self.to_ordinal(value);
}
if !kw.only(&["reading", "prefer", "counter"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let reading = parse_reading(kw);
let prefer = parse_prefer(kw)?;
let counter = match kw.get("counter") {
Option::None => "番",
Some(KwVal::Str(s)) => s.as_str(),
Some(_) => return Err(N2WError::Fallback("kwargs".into())),
};
self.verify_ordinal(value)?;
let base = self.cardinal_kw_int(value, reading.is_on(), &prefer)?;
let suffix = self.ordinal_suffix_kw(reading.is_on(), counter)?;
Ok(format!("{}{}", base, suffix))
}
fn to_ordinal_num_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if kw.is_empty() {
return self.to_ordinal_num(value);
}
if !kw.only(&["reading", "counter"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let reading = parse_reading(kw);
let counter = match kw.get("counter") {
Option::None => "番",
Some(KwVal::Str(s)) => s.as_str(),
Some(_) => return Err(N2WError::Fallback("kwargs".into())),
};
let suffix = self.ordinal_suffix_kw(reading.is_on(), counter)?;
Ok(format!("{}{}", value, suffix))
}
fn to_year_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if kw.is_empty() {
return self.to_year(value);
}
if !kw.only(&["suffix", "longval", "reading", "prefer", "era"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let reading = parse_reading(kw);
let prefer = parse_prefer(kw)?;
let era = match kw.get("era") {
Option::None => true,
Some(v) => kw_truthy(v),
};
if !era {
let mut year = value.clone();
let mut prefix = "";
if year.is_negative() {
year = -year;
prefix = if reading.is_on() {
"きげんぜん"
} else {
"紀元前"
};
}
let mut words = self.cardinal_kw_int(&year, reading.is_on(), &prefer)?;
if reading.is_on() && &year % BigInt::from(10) == BigInt::from(9) {
words = format!("{}く", chop_last3(&words));
}
return Ok(format!(
"{}{}{}",
prefix,
words,
if reading.is_on() { "ねん" } else { "年" }
));
}
self.year_era_int(value, &reading, &prefer)
}
fn to_cardinal_float_kw(
&self,
value: &FloatValue,
precision_override: Option<u32>,
kw: &Kwargs,
) -> Result<String> {
if kw.is_empty() {
return self.cardinal_float_entry(value, precision_override);
}
if !kw.only(&["reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let reading = parse_reading(kw);
let prefer = parse_prefer(kw)?;
if let Some(i) = value.as_whole_int() {
return self.cardinal_kw_int(&i, reading.is_on(), &prefer);
}
self.cardinal_float_impl(value, reading.is_on(), &prefer)
}
fn to_currency_kw(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
kw: &Kwargs,
) -> Result<String> {
if kw.is_empty() {
return self.to_currency(val, currency, cents, separator, adjective);
}
if !kw.only(&["reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let reading = parse_reading(kw).is_on();
let prefer = parse_prefer(kw)?;
let ri = if reading { 1 } else { 0 };
let decimal_val = match val {
CurrencyValue::Int(i) => BigDecimal::from(i.clone()),
CurrencyValue::Decimal { value, .. } => value.clone(),
};
let scaled = &decimal_val * BigDecimal::from(100);
let has_fractional_cents = &scaled - scaled.with_scale(0) != BigDecimal::zero();
let as_float = CurrencyValue::Decimal {
value: decimal_val,
has_decimal: false,
is_float: true,
};
let (left, right, is_negative) = parse_currency_parts(&as_float, cents, false, 100);
let right = right.as_bigint_and_exponent().0;
let forms = self.currency_forms.get(currency).ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let mut cr1 = forms.unit.clone();
let cr2 = &forms.subunit;
if adjective {
if let Some(adj) = self.currency_adjective(currency) {
cr1 = prefix_currency(adj, &cr1);
}
}
let minus_str = if is_negative { self.negword() } else { "" };
let (right_str, cr2_str) = if has_fractional_cents && !cr2.is_empty() {
(
self.cents_as_hundredths_kw(&right, reading, &prefer)?,
cr2[ri].as_str(),
)
} else if !cr2.is_empty() && !right.is_zero() {
(
self.cardinal_kw_int(&right, reading, &prefer)?,
cr2[ri].as_str(),
)
} else {
(String::new(), "")
};
Ok(format!(
"{}{}{}{}{}",
minus_str,
self.cardinal_kw_int(&left, reading, &prefer)?,
cr1[ri],
right_str,
cr2_str,
))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let (f, precision) = match value {
FloatValue::Float { value, precision } => (*value, *precision),
FloatValue::Decimal { value, .. } => {
let f = value
.to_f64()
.ok_or_else(|| N2WError::Value(format!("cannot represent {} as f64", value)))?;
(f, float_repr_precision(f))
}
};
let (pre, post) = float2tuple(&FloatValue::Float {
value: f,
precision,
});
let post_str = post.to_string();
let post_str = format!(
"{}{}",
"0".repeat((precision as usize).saturating_sub(post_str.len())),
post_str
);
let mut out = vec![self.to_cardinal(&pre)?];
if value.is_negative() && pre.is_zero() {
out.insert(0, self.negword().to_string());
}
if precision > 0 {
out.push(self.title(self.pointword()));
}
for ch in post_str.chars().take(precision as usize) {
let d = ch
.to_digit(10)
.ok_or_else(|| N2WError::Value(format!("non-digit {:?} in fractional part", ch)))?;
out.push(self.to_cardinal(&BigInt::from(d))?);
}
Ok(out.join(""))
}
fn lang_name(&self) -> &str {
"Num2Word_JA"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
_separator: Option<&str>,
adjective: bool,
) -> Result<String> {
let decimal_val = match val {
CurrencyValue::Int(i) => BigDecimal::from(i.clone()),
CurrencyValue::Decimal { value, .. } => value.clone(),
};
let scaled = &decimal_val * BigDecimal::from(100);
let has_fractional_cents = &scaled - scaled.with_scale(0) != BigDecimal::zero();
let as_float = CurrencyValue::Decimal {
value: decimal_val,
has_decimal: false,
is_float: true,
};
let (left, right, is_negative) = parse_currency_parts(&as_float, cents, false, 100);
let right = right.as_bigint_and_exponent().0;
let forms = self.currency_forms.get(currency).ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let mut cr1 = forms.unit.clone();
let cr2 = &forms.subunit;
if adjective {
if let Some(adj) = self.currency_adjective(currency) {
cr1 = prefix_currency(adj, &cr1);
}
}
let minus_str = if is_negative { self.negword() } else { "" };
let (right_str, cr2_str) = if has_fractional_cents && !cr2.is_empty() {
(self.cents_as_hundredths(&right)?, cr2[0].as_str())
} else if !cr2.is_empty() && !right.is_zero() {
(self.to_cardinal(&right)?, cr2[0].as_str())
} else {
(String::new(), "")
};
Ok(format!(
"{}{}{}{}{}",
minus_str,
self.to_cardinal(&left)?,
cr1[0],
right_str,
cr2_str,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn run_currency(arg: &str, code: &str) -> Result<String> {
let is_int = !arg.contains('.') && !arg.to_lowercase().contains('e');
let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
LangJa::new().to_currency(&v, code, true, None, false)
}
fn err_name(e: &N2WError) -> &'static str {
match e {
N2WError::NotImplemented(_) => "NotImplementedError",
N2WError::Overflow(_) => "OverflowError",
N2WError::Type(_) => "TypeError",
N2WError::Value(_) => "ValueError",
N2WError::Index(_) => "IndexError",
N2WError::Key(_) => "KeyError",
_ => "OtherError",
}
}
#[test]
fn corpus_currency() {
let cases: &[(&str, &str, std::result::Result<&str, &str>)] = &[
("0", "EUR", Ok("零ユーロ")),
("1", "EUR", Ok("一ユーロ")),
("2", "EUR", Ok("二ユーロ")),
("100", "EUR", Ok("百ユーロ")),
("12.34", "EUR", Ok("十二ユーロ三十四セント")),
("0.01", "EUR", Ok("零ユーロ一セント")),
("1.0", "EUR", Ok("一ユーロ")),
("99.99", "EUR", Ok("九十九ユーロ九十九セント")),
("1234.56", "EUR", Ok("千二百三十四ユーロ五十六セント")),
("-12.34", "EUR", Ok("マイナス十二ユーロ三十四セント")),
("1000000", "EUR", Ok("百万ユーロ")),
("0.5", "EUR", Ok("零ユーロ五十セント")),
("0", "USD", Ok("零ドル")),
("1", "USD", Ok("一ドル")),
("2", "USD", Ok("二ドル")),
("100", "USD", Ok("百ドル")),
("12.34", "USD", Ok("十二ドル三十四セント")),
("0.01", "USD", Ok("零ドル一セント")),
("1.0", "USD", Ok("一ドル")),
("99.99", "USD", Ok("九十九ドル九十九セント")),
("1234.56", "USD", Ok("千二百三十四ドル五十六セント")),
("-12.34", "USD", Ok("マイナス十二ドル三十四セント")),
("1000000", "USD", Ok("百万ドル")),
("0.5", "USD", Ok("零ドル五十セント")),
("0", "GBP", Ok("零ポンド")),
("1", "GBP", Ok("一ポンド")),
("2", "GBP", Ok("二ポンド")),
("100", "GBP", Ok("百ポンド")),
("12.34", "GBP", Ok("十二ポンド三十四ペンス")),
("0.01", "GBP", Ok("零ポンド一ペンス")),
("1.0", "GBP", Ok("一ポンド")),
("99.99", "GBP", Ok("九十九ポンド九十九ペンス")),
("1234.56", "GBP", Ok("千二百三十四ポンド五十六ペンス")),
("-12.34", "GBP", Ok("マイナス十二ポンド三十四ペンス")),
("1000000", "GBP", Ok("百万ポンド")),
("0.5", "GBP", Ok("零ポンド五十ペンス")),
("0", "JPY", Ok("零円")),
("1", "JPY", Ok("一円")),
("2", "JPY", Ok("二円")),
("100", "JPY", Ok("百円")),
("12.34", "JPY", Ok("十二円三十四銭")),
("0.01", "JPY", Ok("零円一銭")),
("1.0", "JPY", Ok("一円")),
("99.99", "JPY", Ok("九十九円九十九銭")),
("1234.56", "JPY", Ok("千二百三十四円五十六銭")),
("-12.34", "JPY", Ok("マイナス十二円三十四銭")),
("1000000", "JPY", Ok("百万円")),
("0.5", "JPY", Ok("零円五十銭")),
("0", "KWD", Err("NotImplementedError")),
("1", "KWD", Err("NotImplementedError")),
("2", "KWD", Err("NotImplementedError")),
("100", "KWD", Err("NotImplementedError")),
("12.34", "KWD", Err("NotImplementedError")),
("0.01", "KWD", Err("NotImplementedError")),
("1.0", "KWD", Err("NotImplementedError")),
("99.99", "KWD", Err("NotImplementedError")),
("1234.56", "KWD", Err("NotImplementedError")),
("-12.34", "KWD", Err("NotImplementedError")),
("1000000", "KWD", Err("NotImplementedError")),
("0.5", "KWD", Err("NotImplementedError")),
("0", "BHD", Err("NotImplementedError")),
("1", "BHD", Err("NotImplementedError")),
("2", "BHD", Err("NotImplementedError")),
("100", "BHD", Err("NotImplementedError")),
("12.34", "BHD", Err("NotImplementedError")),
("0.01", "BHD", Err("NotImplementedError")),
("1.0", "BHD", Err("NotImplementedError")),
("99.99", "BHD", Err("NotImplementedError")),
("1234.56", "BHD", Err("NotImplementedError")),
("-12.34", "BHD", Err("NotImplementedError")),
("1000000", "BHD", Err("NotImplementedError")),
("0.5", "BHD", Err("NotImplementedError")),
("0", "INR", Err("NotImplementedError")),
("1", "INR", Err("NotImplementedError")),
("2", "INR", Err("NotImplementedError")),
("100", "INR", Err("NotImplementedError")),
("12.34", "INR", Err("NotImplementedError")),
("0.01", "INR", Err("NotImplementedError")),
("1.0", "INR", Err("NotImplementedError")),
("99.99", "INR", Err("NotImplementedError")),
("1234.56", "INR", Err("NotImplementedError")),
("-12.34", "INR", Err("NotImplementedError")),
("1000000", "INR", Err("NotImplementedError")),
("0.5", "INR", Err("NotImplementedError")),
("0", "CNY", Ok("零元")),
("1", "CNY", Ok("一元")),
("2", "CNY", Ok("二元")),
("100", "CNY", Ok("百元")),
("12.34", "CNY", Ok("十二元三十四分")),
("0.01", "CNY", Ok("零元一分")),
("1.0", "CNY", Ok("一元")),
("99.99", "CNY", Ok("九十九元九十九分")),
("1234.56", "CNY", Ok("千二百三十四元五十六分")),
("-12.34", "CNY", Ok("マイナス十二元三十四分")),
("1000000", "CNY", Ok("百万元")),
("0.5", "CNY", Ok("零元五十分")),
("0", "CHF", Err("NotImplementedError")),
("1", "CHF", Err("NotImplementedError")),
("2", "CHF", Err("NotImplementedError")),
("100", "CHF", Err("NotImplementedError")),
("12.34", "CHF", Err("NotImplementedError")),
("0.01", "CHF", Err("NotImplementedError")),
("1.0", "CHF", Err("NotImplementedError")),
("99.99", "CHF", Err("NotImplementedError")),
("1234.56", "CHF", Err("NotImplementedError")),
("-12.34", "CHF", Err("NotImplementedError")),
("1000000", "CHF", Err("NotImplementedError")),
("0.5", "CHF", Err("NotImplementedError")),
];
for (arg, code, want) in cases {
let got = run_currency(arg, code);
match (want, &got) {
(Ok(w), Ok(g)) => assert_eq!(g, w, "currency:{} arg={}", code, arg),
(Err(w), Err(g)) => assert_eq!(&err_name(g), w, "currency:{} arg={}", code, arg),
_ => panic!(
"currency:{} arg={}: want {:?}, got {:?}",
code, arg, want, got
),
}
}
}
#[test]
fn corpus_cheque() {
let cases: &[(&str, &str, std::result::Result<&str, &str>)] = &[
("1234.56", "EUR", Ok("千二百三十四 AND 56/100 ユーロ")),
("1234.56", "USD", Ok("千二百三十四 AND 56/100 ドル")),
("1234.56", "GBP", Ok("千二百三十四 AND 56/100 ポンド")),
("1234.56", "JPY", Ok("千二百三十四 AND 56/100 えん")),
("1234.56", "KWD", Err("NotImplementedError")),
("1234.56", "BHD", Err("NotImplementedError")),
("1234.56", "INR", Err("NotImplementedError")),
("1234.56", "CNY", Ok("千二百三十四 AND 56/100 元")),
("1234.56", "CHF", Err("NotImplementedError")),
];
for (arg, code, want) in cases {
let d = BigDecimal::from_str(arg).unwrap();
let got = LangJa::new().to_cheque(&d, code);
match (want, &got) {
(Ok(w), Ok(g)) => assert_eq!(g, w, "cheque:{} arg={}", code, arg),
(Err(w), Err(g)) => assert_eq!(&err_name(g), w, "cheque:{} arg={}", code, arg),
_ => panic!(
"cheque:{} arg={}: want {:?}, got {:?}",
code, arg, want, got
),
}
}
}
#[test]
fn missing_currency_message() {
match run_currency("1.0", "KWD") {
Err(N2WError::NotImplemented(m)) => {
assert_eq!(
m,
"Currency code \"KWD\" not implemented for \"Num2Word_JA\""
)
}
other => panic!("expected NotImplemented, got {:?}", other),
}
}
#[test]
fn inert_kwargs() {
let l = LangJa::new();
for arg in [
"0", "1", "2", "100", "1000000", "12.34", "0.01", "1.0", "99.99", "1234.56", "-12.34",
"0.5",
] {
let is_int = !arg.contains('.');
let v = CurrencyValue::parse(arg, is_int, !is_int, !is_int).unwrap();
for code in ["JPY", "EUR", "USD", "GBP", "CNY"] {
let base = l.to_currency(&v, code, false, None, false).unwrap();
assert_eq!(
l.to_currency(&v, code, true, None, false).unwrap(),
base,
"cents= changed {} {}",
code,
arg
);
assert_eq!(
l.to_currency(&v, code, false, Some("XX"), false).unwrap(),
base,
"separator= changed {} {}",
code,
arg
);
assert_eq!(
l.to_currency(&v, code, false, None, true).unwrap(),
base,
"adjective= changed {} {}",
code,
arg
);
}
}
}
fn float_val(f: f64) -> FloatValue {
FloatValue::Float {
value: f,
precision: float_repr_precision(f),
}
}
#[test]
fn corpus_cardinal_float() {
let cases: &[(f64, &str)] = &[
(0.5, "零点五"),
(1.5, "一点五"),
(2.25, "二点二五"),
(3.14, "三点一四"),
(0.01, "零点零一"),
(0.1, "零点一"),
(0.99, "零点九九"),
(1.01, "一点零一"),
(12.34, "十二点三四"),
(99.99, "九十九点九九"),
(100.5, "百点五"),
(1234.56, "千二百三十四点五六"),
(-0.5, "マイナス零点五"),
(-1.5, "マイナス一点五"),
(-12.34, "マイナス十二点三四"),
(1.005, "一点零零五"), (2.675, "二点六七五"), ];
for (f, want) in cases {
assert_eq!(
LangJa::new()
.to_cardinal_float(&float_val(*f), None)
.unwrap(),
*want,
"cardinal float arg={}",
f
);
}
}
#[test]
fn corpus_cardinal_dec() {
let cases: &[(&str, u32, &str)] = &[
("0.01", 2, "零点零一"),
("1.10", 2, "一点一"),
("12.345", 3, "十二点三四五"),
(
"98746251323029.99",
2,
"九十八兆七千四百六十二億五千百三十二万三千二十九点九八",
),
("0.001", 3, "零点零零一"),
];
for (s, prec, want) in cases {
let v = FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: *prec,
};
assert_eq!(
LangJa::new().to_cardinal_float(&v, None).unwrap(),
*want,
"cardinal_dec arg={}",
s
);
}
}
#[test]
fn interpreter_cross_check() {
let l = LangJa::new();
let floats: &[(f64, &str)] = &[
(0.001, "零点零零一"),
(1000000.5, "百万点五"),
(123456.789, "十二万三千四百五十六点七八九"),
(0.7071, "零点七零七一"),
(1.239999999, "一点二三九九九九九九九"),
(-0.01, "マイナス零点零一"),
(-0.999, "マイナス零点九九九"),
(0.0009, "零点零零零九"),
(1000000000.5, "十億点五"),
(0.1234567, "零点一二三四五六七"),
];
for (f, want) in floats {
assert_eq!(
l.to_cardinal_float(&float_val(*f), None).unwrap(),
*want,
"float {}",
f
);
}
let decs: &[(&str, &str)] = &[
("1.230000", "一点二三"),
("0.50", "零点五"),
("1000.00001", "千点零零零零一"),
("1.999999999999", "一点九九九九九九九九九九九九"),
];
for (s, want) in decs {
let v = FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
};
assert_eq!(l.to_cardinal_float(&v, None).unwrap(), *want, "dec {}", s);
}
}
#[test]
fn fractional_cents_arm() {
for (arg, want) in [
("1.005", "一ユーロ零点零一セント"),
("0.001", "零ユーロ零点零セント"),
("1.011", "一ユーロ零点零一セント"),
("0.005", "零ユーロ零点零一セント"),
("2.567", "二ユーロ零点五七セント"),
("0.999", "一ユーロ零点零セント"),
("12.345", "十二ユーロ零点三五セント"),
("-1.005", "マイナス一ユーロ零点零一セント"),
("0.7071", "零ユーロ零点七一セント"),
] {
assert_eq!(run_currency(arg, "EUR").unwrap(), want, "arg={}", arg);
}
}
}