1use crate::base::{Lang, N2WError, Result};
16use bigdecimal::BigDecimal;
17use num_bigint::BigInt;
18use num_traits::{Signed, Zero};
19use std::str::FromStr;
20
21#[derive(Debug, Clone)]
28pub struct CurrencyForms {
29 pub unit: Vec<String>,
30 pub subunit: Vec<String>,
31}
32
33impl CurrencyForms {
34 pub fn new(unit: &[&str], subunit: &[&str]) -> Self {
35 CurrencyForms {
36 unit: unit.iter().map(|s| s.to_string()).collect(),
37 subunit: subunit.iter().map(|s| s.to_string()).collect(),
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
48pub enum CurrencyValue {
49 Int(BigInt),
51 Decimal {
61 value: BigDecimal,
62 has_decimal: bool,
63 is_float: bool,
69 },
70}
71
72impl CurrencyValue {
73 pub fn parse(s: &str, is_int: bool, has_decimal: bool, is_float: bool) -> Result<Self> {
77 if is_int {
78 BigInt::from_str(s)
79 .map(CurrencyValue::Int)
80 .map_err(|e| N2WError::Value(e.to_string()))
81 } else {
82 BigDecimal::from_str(s)
83 .map(|value| CurrencyValue::Decimal { value, has_decimal, is_float })
84 .map_err(|e| N2WError::Value(e.to_string()))
85 }
86 }
87
88 pub fn is_negative(&self) -> bool {
89 match self {
90 CurrencyValue::Int(i) => i.is_negative(),
91 CurrencyValue::Decimal { value, .. } => value.is_negative(),
92 }
93 }
94}
95
96pub fn prefix_currency(prefix: &str, base: &[String]) -> Vec<String> {
98 base.iter().map(|i| format!("{} {}", prefix, i)).collect()
99}
100
101fn round_half_up(v: &BigDecimal, scale: i64) -> BigDecimal {
107 let neg = v.is_negative();
108 let abs = v.abs();
109 let shift = BigDecimal::from(BigInt::from(10).pow(scale.max(0) as u32));
110 let shifted = &abs * &shift;
111 let floor = shifted.with_scale(0);
113 let frac = &shifted - &floor;
114 let half = BigDecimal::from_str("0.5").unwrap();
115 let rounded = if frac >= half { floor + BigDecimal::from(1) } else { floor };
116 let out = rounded / shift;
117 if neg {
118 -out
119 } else {
120 out
121 }
122}
123
124pub fn parse_currency_parts(
130 value: &CurrencyValue,
131 is_int_with_cents: bool,
132 keep_precision: bool,
133 divisor: i64,
134) -> (BigInt, BigDecimal, bool) {
135 match value {
136 CurrencyValue::Int(v) => {
137 let negative = v.is_negative();
138 let v = v.abs();
139 if is_int_with_cents && divisor != 0 {
140 let d = BigInt::from(divisor);
141 (&v / &d, BigDecimal::from(&v % &d), negative)
142 } else {
143 (v, BigDecimal::zero(), negative)
144 }
145 }
146 CurrencyValue::Decimal { value: v, .. } => {
147 let mut v = v.clone();
148 if !keep_precision && divisor > 1 {
149 let scale = (divisor as f64).log10().round() as i64;
151 v = round_half_up(&v, scale);
152 }
153 let negative = v.is_negative();
154 let v = v.abs();
155 let integer = v.with_scale(0); let fraction = &v - &integer;
157 let cents = fraction * BigDecimal::from(divisor);
158 let cents = if keep_precision {
159 cents
160 } else {
161 cents.with_scale(0) };
163 (integer.as_bigint_and_exponent().0, cents, negative)
164 }
165 }
166}
167
168pub fn default_to_currency<L: Lang + ?Sized>(
173 lang: &L,
174 val: &CurrencyValue,
175 currency: &str,
176 cents: bool,
177 separator: &str,
178 adjective: bool,
179) -> Result<String> {
180 let divisor = lang.currency_precision(currency);
181
182 let val = if divisor == 1 {
185 match val {
186 CurrencyValue::Decimal { value, .. } => {
187 CurrencyValue::Int(round_half_up(value, 0).as_bigint_and_exponent().0)
188 }
189 other => other.clone(),
190 }
191 } else {
192 val.clone()
193 };
194
195 let forms = lang
196 .currency_forms(currency)
197 .ok_or_else(|| N2WError::NotImplemented(format!(
198 "Currency code \"{}\" not implemented for \"{}\"",
199 currency,
200 lang.lang_name()
201 )))?;
202
203 let mut cr1 = forms.unit.clone();
204 let cr2 = forms.subunit.clone();
205 if adjective {
206 if let Some(adj) = lang.currency_adjective(currency) {
207 cr1 = prefix_currency(adj, &cr1);
208 }
209 }
210
211 if let CurrencyValue::Int(v) = &val {
213 let minus = if v.is_negative() {
214 format!("{} ", lang.negword().trim())
215 } else {
216 String::new()
217 };
218 let abs = v.abs();
219 let money = lang.money_verbose(&abs, currency)?;
220 return Ok(format!(
221 "{}{} {}",
222 minus,
223 money,
224 lang.pluralize(&abs, &cr1)?
225 ));
226 }
227
228 let (d, has_decimal, is_float) = match &val {
229 CurrencyValue::Decimal { value, has_decimal, is_float } => (value.clone(), *has_decimal, *is_float),
230 _ => unreachable!(),
231 };
232 let scaled = &d * BigDecimal::from(divisor);
234 let has_fractional_cents = &scaled - scaled.with_scale(0) != BigDecimal::zero();
235
236 let (left, right, is_negative) = parse_currency_parts(
237 &CurrencyValue::Decimal { value: d, has_decimal, is_float },
238 false,
239 has_fractional_cents,
240 divisor,
241 );
242
243 let minus = if is_negative {
244 format!("{} ", lang.negword().trim())
245 } else {
246 String::new()
247 };
248 let money = lang.money_verbose(&left, currency)?;
249 let right_int = right.as_bigint_and_exponent().0;
250
251 if !has_decimal && right_int.is_zero() {
255 return Ok(format!(
256 "{}{} {}",
257 minus,
258 money,
259 lang.pluralize(&left, &cr1)?
260 ));
261 }
262 if has_fractional_cents {
263 let sub = cr2.get(1).or_else(|| cr2.first()).cloned().unwrap_or_default();
267 return Ok(format!(
268 "{}{} {}{} {} {}",
269 minus,
270 money,
271 lang.pluralize(&left, &cr1)?,
272 separator,
273 lang.cardinal_from_decimal(&right)?,
274 sub
275 ));
276 }
277 let cents_str = if cents {
278 lang.cents_verbose(&right_int, currency)?
279 } else {
280 lang.cents_terse(&right_int, currency)?
281 };
282
283 Ok(format!(
284 "{}{} {}{} {} {}",
285 minus,
286 money,
287 lang.pluralize(&left, &cr1)?,
288 separator,
289 cents_str,
290 lang.pluralize(&right_int, &cr2)?
291 ))
292}
293
294pub fn default_cents_terse(n: &BigInt, divisor: i64) -> String {
296 if divisor <= 1 {
297 return n.to_string();
298 }
299 let width = divisor.to_string().len() - 1;
300 format!("{:0>width$}", n.to_string(), width = width)
301}
302
303pub fn default_to_cheque<L: Lang + ?Sized>(
305 lang: &L,
306 val: &BigDecimal,
307 currency: &str,
308) -> Result<String> {
309 let forms = lang
310 .currency_forms(currency)
311 .ok_or_else(|| N2WError::NotImplemented(format!(
312 "Currency code \"{}\" not implemented for \"{}\"",
313 currency,
314 lang.lang_name()
315 )))?;
316
317 let divisor = lang.currency_precision(currency);
318 let is_negative = val.is_negative();
319 let abs_val = val.abs();
320 let whole = abs_val.with_scale(0).as_bigint_and_exponent().0;
321
322 let fraction_str = if divisor > 1 {
323 let sub = (&abs_val - BigDecimal::from(whole.clone())) * BigDecimal::from(divisor);
324 let sub = sub.with_scale(0).as_bigint_and_exponent().0;
325 let digits = divisor.to_string().len() - 1;
326 format!(
327 "{:0>width$}/{}",
328 sub.to_string(),
329 divisor,
330 width = digits
331 )
332 } else {
333 String::new()
334 };
335
336 let words = lang.money_verbose(&whole, currency)?;
337 let unit = forms.unit.last().cloned().unwrap_or_default();
339 let sign = if is_negative { "MINUS " } else { "" };
340 let body = if fraction_str.is_empty() {
341 format!("{} {}", words, unit)
342 } else {
343 format!("{} AND {} {}", words, fraction_str, unit)
344 };
345 Ok(format!("{}{}", sign, body).to_uppercase())
346}