ic_host_text/decimal/
mod.rs1use std::fmt;
4
5#[cfg(test)]
6mod tests;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub struct InvalidDecimal;
11
12impl fmt::Display for InvalidDecimal {
13 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
14 formatter.write_str("expected unsigned decimal text")
15 }
16}
17
18impl std::error::Error for InvalidDecimal {}
19
20pub fn fixed_decimal_text(value: &str, exponent: usize) -> Result<String, InvalidDecimal> {
30 render(value, exponent, false)
31}
32
33pub fn compact_decimal_text(value: &str, exponent: usize) -> Result<String, InvalidDecimal> {
38 render(value, exponent, true)
39}
40
41fn render(value: &str, exponent: usize, compact: bool) -> Result<String, InvalidDecimal> {
42 let (whole, fraction) = value.split_once('.').unwrap_or((value, ""));
43 if whole.is_empty()
44 || !whole.bytes().all(|digit| digit.is_ascii_digit())
45 || !fraction.bytes().all(|digit| digit.is_ascii_digit())
46 {
47 return Err(InvalidDecimal);
48 }
49 let whole = whole.trim_start_matches('0');
50 let (integer, integer_fraction) = if whole.len() > exponent {
51 whole.split_at(whole.len() - exponent)
52 } else {
53 ("0", whole)
54 };
55 let mut digits = std::iter::repeat_n(b'0', exponent.saturating_sub(whole.len()))
56 .chain(integer_fraction.bytes())
57 .chain(fraction.bytes());
58 let tenths = digits.next().unwrap_or(b'0') - b'0';
59 let hundredth = digits.next().unwrap_or(b'0') - b'0';
60 let mut hundredths = tenths * 10 + hundredth;
61 if digits.next().unwrap_or(b'0') >= b'5' {
62 hundredths += 1;
63 }
64 let mut integer = integer.to_string();
65 if hundredths == 100 {
66 increment(&mut integer);
67 hundredths = 0;
68 }
69 if !compact || hundredths != 0 {
70 use std::fmt::Write as _;
71 write!(integer, ".{hundredths:02}").expect("writing to a String cannot fail");
72 if compact && hundredths.is_multiple_of(10) {
73 integer.pop();
74 }
75 }
76 Ok(integer)
77}
78
79fn increment(value: &mut String) {
80 let mut digits = value.as_bytes().to_vec();
81 for digit in digits.iter_mut().rev() {
82 if *digit < b'9' {
83 *digit += 1;
84 *value = String::from_utf8(digits).expect("decimal digits remain UTF-8");
85 return;
86 }
87 *digit = b'0';
88 }
89 digits.insert(0, b'1');
90 *value = String::from_utf8(digits).expect("decimal digits remain UTF-8");
91}