1use thiserror::Error;
4
5pub const COIN: u64 = 100_000_000;
7
8#[derive(Debug, Error, PartialEq, Eq)]
9pub enum AmountError {
10 #[error("invalid amount: {0}")]
11 Invalid(String),
12}
13
14pub fn parse_amount(s: &str) -> Result<u64, AmountError> {
16 let err = || AmountError::Invalid(s.to_string());
17 let s = s.trim();
18 if s.is_empty() || s.starts_with('-') || s.starts_with('+') {
19 return Err(err());
20 }
21 let (whole, frac) = match s.split_once('.') {
22 Some((w, f)) => (w, f),
23 None => (s, ""),
24 };
25 if whole.is_empty() && frac.is_empty() {
26 return Err(err());
27 }
28 if frac.len() > 8 || !whole.chars().all(|c| c.is_ascii_digit()) || !frac.chars().all(|c| c.is_ascii_digit()) {
29 return Err(err());
30 }
31 let whole: u64 = if whole.is_empty() { 0 } else { whole.parse().map_err(|_| err())? };
32 let frac_units: u64 = if frac.is_empty() {
33 0
34 } else {
35 let padded = format!("{frac:0<8}");
36 padded.parse().map_err(|_| err())?
37 };
38 whole
39 .checked_mul(COIN)
40 .and_then(|v| v.checked_add(frac_units))
41 .ok_or_else(err)
42}
43
44pub fn format_amount(units: u64) -> String {
46 let whole = units / COIN;
47 let frac = units % COIN;
48 if frac == 0 {
49 format!("{whole}")
50 } else {
51 let s = format!("{frac:08}");
52 format!("{whole}.{}", s.trim_end_matches('0'))
53 }
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59
60 #[test]
61 fn parse_and_format() {
62 assert_eq!(parse_amount("1").unwrap(), COIN);
63 assert_eq!(parse_amount("0.5").unwrap(), COIN / 2);
64 assert_eq!(parse_amount("50").unwrap(), 50 * COIN);
65 assert_eq!(parse_amount("0.00000001").unwrap(), 1);
66 assert_eq!(parse_amount("210000").unwrap(), 210_000 * COIN);
67 assert!(parse_amount("-1").is_err());
68 assert!(parse_amount("1.123456789").is_err());
69 assert!(parse_amount("abc").is_err());
70 assert!(parse_amount("").is_err());
71
72 assert_eq!(format_amount(COIN), "1");
73 assert_eq!(format_amount(COIN / 2), "0.5");
74 assert_eq!(format_amount(1), "0.00000001");
75 assert_eq!(parse_amount(&format_amount(123_456_789)).unwrap(), 123_456_789);
76 }
77}