time_primitives/
balance.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
use anyhow::Result;
use serde::{Deserialize, Serialize};

const SI_PREFIX: [(i8, char); 12] = [
	(18, 'E'),
	(15, 'P'),
	(12, 'T'),
	(9, 'G'),
	(6, 'M'),
	(3, 'k'),
	(-3, 'm'),
	(-6, 'u'),
	(-9, 'n'),
	(-12, 'p'),
	(-15, 'f'),
	(-18, 'a'),
];

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
pub struct Currency {
	pub decimals: u8,
	pub symbol: String,
}

impl Currency {
	pub fn new(decimals: u8, symbol: String) -> Self {
		Self { decimals, symbol }
	}

	pub fn format(&self, balance: u128) -> String {
		if balance == 0 {
			return format!("{} {}", balance, self.symbol);
		};
		let digits = balance.ilog10();
		let rel_digits = digits as i32 - self.decimals as i32;
		let unit = rel_digits / 3 * 3;
		assert!((-18..=18).contains(&unit), "balance out of range {}", unit);
		let unit = unit as i8;
		let prefix = SI_PREFIX.iter().find(|(d, _)| *d == unit).map(|(_, p)| p);

		let base = f64::powi(10., unit as i32 + self.decimals as i32);
		let tokens = balance as f64 / base;
		let tokens_str = format!("{tokens:.3}");
		let tokens_str = tokens_str.trim_end_matches('0');
		let tokens_str = tokens_str.trim_end_matches('.');
		if let Some(prefix) = prefix {
			format!("{tokens_str}{prefix} {}", self.symbol)
		} else {
			format!("{tokens_str} {}", self.symbol)
		}
	}

	pub fn parse(&self, balance: &str) -> Result<u128> {
		let balance = balance.trim().replace('_', "");
		if !balance.contains('.') && !balance.ends_with(&self.symbol) {
			return balance.parse().map_err(|_| anyhow::anyhow!("balance is not a valid u128"));
		}
		let balance = balance.strip_suffix(&self.symbol).unwrap_or(&balance).trim_end();
		let (unit, balance) = SI_PREFIX
			.iter()
			.find(|(_, p)| balance.ends_with(*p))
			.map(|(d, p)| (*d, balance.strip_suffix(*p).unwrap()))
			.unwrap_or((0, balance));
		let balance: f64 =
			balance.parse().map_err(|_| anyhow::anyhow!("balance is not a valid f64"))?;
		let base = f64::powi(10., unit as i32 + self.decimals as i32);
		Ok((balance * base) as u128)
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn test_formatter() -> Result<()> {
		let fmt_cases = [
			(0, "0 ANLG"),
			(1, "1p ANLG"),
			(1_000_000_000_000, "1 ANLG"),
			(10_000_000_000_000, "10 ANLG"),
			(100_000_000_000_000, "100 ANLG"),
			(1_000_000_000_000_000, "1k ANLG"),
			(1_200_000, "1.2u ANLG"),
			(1_200_000_000_000_000, "1.2k ANLG"),
		];
		let parse_cases = [("0", 0), ("1_200_000", 1_200_000), ("1.", 1_000_000_000_000)];
		let fmt = Currency::new(12, "ANLG".into());
		for (n, s) in fmt_cases {
			assert_eq!(fmt.format(n), s);
			assert_eq!(fmt.parse(s).unwrap(), n);
		}
		for (s, n) in parse_cases {
			assert_eq!(fmt.parse(s).unwrap(), n);
		}
		Ok(())
	}
}