1use crate::error::{CliError, CliResult};
4
5pub const ZAT_PER_ZEC: u64 = 100_000_000;
7
8pub fn parse_decimal(text: &str, decimals: u32, what: &str) -> CliResult<u128> {
11 let bad = || {
12 CliError::usage(format!(
13 "{what} must be a positive decimal number with at most {decimals} decimals, got \"{text}\""
14 ))
15 };
16 let text = text.trim();
17 let (whole, frac) = text.split_once('.').unwrap_or((text, ""));
18 if (whole.is_empty() && frac.is_empty())
19 || !whole.bytes().all(|b| b.is_ascii_digit())
20 || !frac.bytes().all(|b| b.is_ascii_digit())
21 || frac.len() > decimals as usize
22 || (text.contains('.') && frac.is_empty())
23 {
24 return Err(bad());
25 }
26 let scale = 10u128.checked_pow(decimals).ok_or_else(bad)?;
27 let whole: u128 = if whole.is_empty() { 0 } else { whole.parse().map_err(|_| bad())? };
28 let frac_units: u128 = if frac.is_empty() {
29 0
30 } else {
31 let padded = format!("{frac:0<width$}", width = decimals as usize);
32 padded.parse().map_err(|_| bad())?
33 };
34 whole.checked_mul(scale).and_then(|w| w.checked_add(frac_units)).ok_or_else(bad)
35}
36
37pub fn parse_zec(text: &str) -> CliResult<u64> {
39 let zat = parse_decimal(text, 8, "--zec")?;
40 let zat = u64::try_from(zat).map_err(|_| CliError::usage("--zec is too large"))?;
41 if zat == 0 {
42 return Err(CliError::usage("--zec must be more than 0"));
43 }
44 Ok(zat)
45}
46
47pub fn parse_usd(text: &str, decimals: u32) -> CliResult<u128> {
49 let units = parse_decimal(text, decimals, "--usd")?;
50 if units == 0 {
51 return Err(CliError::usage("--usd must be more than 0"));
52 }
53 Ok(units)
54}
55
56pub fn zec(zat: u64) -> String {
58 let (whole, frac) = (zat / ZAT_PER_ZEC, zat % ZAT_PER_ZEC);
59 if frac == 0 { whole.to_string() } else { format!("{whole}.{}", format!("{frac:08}").trim_end_matches('0')) }
60}
61
62pub fn usd(units: u128, decimals: u32) -> String {
64 let scale = 10u128.saturating_pow(decimals).max(1);
65 let cents = units.saturating_mul(100) / scale;
66 format!("{}.{:02}", cents / 100, cents % 100)
67}
68
69pub fn exact(units: u128, decimals: u32) -> String {
71 let scale = 10u128.saturating_pow(decimals).max(1);
72 let (whole, frac) = (units / scale, units % scale);
73 if frac == 0 {
74 whole.to_string()
75 } else {
76 let digits = format!("{frac:0width$}", width = decimals as usize);
77 format!("{whole}.{}", digits.trim_end_matches('0'))
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
84 use super::*;
85
86 #[test]
87 fn parses_exact_decimals() {
88 assert_eq!(parse_zec("0.1").unwrap(), 10_000_000);
89 assert_eq!(parse_zec("1").unwrap(), 100_000_000);
90 assert_eq!(parse_zec(".5").unwrap(), 50_000_000);
91 assert_eq!(parse_zec("0.00000001").unwrap(), 1);
92 assert_eq!(parse_usd("25", 6).unwrap(), 25_000_000);
93 assert_eq!(parse_usd("0.000001", 6).unwrap(), 1);
94 for bad in ["", ".", "1.", "-1", "1e3", "0.000000001", "1,5", " ", "0", "0.0", "abc", "1.2.3", "+1"] {
95 assert!(parse_zec(bad).is_err(), "{bad}");
96 }
97 assert!(parse_usd("1.0000001", 6).is_err());
98 assert!(parse_decimal("340282366920938463463374607431768211456", 0, "x").is_err());
99 }
100
101 #[test]
102 fn formats_amounts() {
103 assert_eq!(zec(10_000_000), "0.1");
104 assert_eq!(zec(123_456_789), "1.23456789");
105 assert_eq!(zec(500_000_000), "5");
106 assert_eq!(usd(4_975_000, 6), "4.97");
107 assert_eq!(usd(5, 6), "0.00");
108 assert_eq!(exact(4_975_000, 6), "4.975");
109 assert_eq!(exact(25_000_000, 6), "25");
110 }
111}