evobench_tools/
digit_num.rs

1//! Numbers based on vectors of decimal digits, for testing purposes
2//!
3//! These are for correctness and introspection, not performance. Useful
4//! for writing some kinds of tests.
5
6use std::{fmt::Display, io::Write};
7
8use rand::Rng;
9
10// -----------------------------------------------------------------------------
11
12#[derive(Debug, Clone, Copy)]
13pub struct Digit(u8);
14
15impl Display for Digit {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        std::fmt::Write::write_char(f, (self.0 + b'0') as char)
18    }
19}
20
21#[derive(thiserror::Error, Debug)]
22#[error("out of range")]
23pub struct OutOfRange;
24
25impl TryFrom<u8> for Digit {
26    type Error = OutOfRange;
27
28    fn try_from(value: u8) -> Result<Self, Self::Error> {
29        if value < 10 {
30            Ok(Self(value))
31        } else {
32            Err(OutOfRange)
33        }
34    }
35}
36
37impl Digit {
38    pub fn random() -> Self {
39        let mut rng = rand::thread_rng();
40        let d = rng.gen_range(0..10);
41        d.try_into().expect("no bug")
42    }
43}
44
45// -----------------------------------------------------------------------------
46
47// `PRECISION` is the number of decimal digits after the dot.
48#[derive(Debug, Clone)]
49pub struct DigitNum<const PRECISION: usize>(Vec<Digit>);
50
51pub struct DigitNumFormat {
52    pub underscores: bool,
53    /// Omit "." if no digits come after it
54    pub omit_trailing_dot: bool,
55}
56
57impl<const PRECISION: usize> DigitNum<PRECISION> {
58    pub fn new() -> Self {
59        Self(vec![])
60    }
61
62    pub fn into_changed_dot_position<const NEW_PRECISION: usize>(self) -> DigitNum<NEW_PRECISION> {
63        DigitNum(self.0)
64    }
65
66    pub fn push_lowest_digit(&mut self, d: Digit) {
67        if d.0 == 0 && self.0.is_empty() {
68            return;
69        }
70        self.0.push(d)
71    }
72
73    pub fn write<W: Write>(&self, params: DigitNumFormat, mut out: W) -> std::io::Result<()> {
74        let DigitNumFormat {
75            underscores,
76            omit_trailing_dot,
77        } = params;
78        let len = self.0.len();
79        let after_dot = PRECISION;
80        let mut digits = self.0.iter();
81        let len_missing_after_dot = if len > after_dot {
82            let len_before_dot = len - after_dot;
83            let mut position = len_before_dot % 3;
84            if position == 0 {
85                position = 3;
86            }
87            for _ in 0..len_before_dot {
88                if position == 0 {
89                    if underscores {
90                        write!(out, "_")?;
91                    }
92                    position = 3;
93                }
94                position -= 1;
95                let d = digits.next().expect("digits till lowest one are present");
96                write!(out, "{d}")?;
97            }
98            0
99        } else {
100            write!(out, "0")?;
101            after_dot - len
102        };
103
104        if after_dot == 0 && omit_trailing_dot {
105            return Ok(());
106        }
107
108        write!(out, ".")?;
109
110        let mut position = 3;
111
112        for _ in 0..len_missing_after_dot {
113            if position == 0 {
114                if underscores {
115                    write!(out, "_")?;
116                }
117                position = 3;
118            }
119            position -= 1;
120            write!(out, "0")?;
121        }
122        for d in digits {
123            if position == 0 {
124                if underscores {
125                    write!(out, "_")?;
126                }
127                position = 3;
128            }
129            position -= 1;
130            write!(out, "{d}")?;
131        }
132        Ok(())
133    }
134
135    pub fn to_string_with_params(&self, params: DigitNumFormat) -> String {
136        let mut s = Vec::new();
137        self.write(params, &mut s)
138            .expect("no errors writing to String");
139        String::from_utf8(s).expect("no non-utf8")
140    }
141}
142
143/// Note: converts to the lowest digit, ignores PRECISION!
144impl<const PRECISION: usize> TryFrom<&DigitNum<PRECISION>> for u64 {
145    type Error = OutOfRange;
146
147    fn try_from(value: &DigitNum<PRECISION>) -> Result<Self, Self::Error> {
148        let mut num: u64 = 0;
149        for Digit(d) in value.0.iter() {
150            num = num.checked_mul(10).ok_or(OutOfRange)?;
151            num = num.checked_add(u64::from(*d)).ok_or(OutOfRange)?;
152        }
153        Ok(num)
154    }
155}
156
157// Do we really want this, or silence Clippy instead?
158impl<const PRECISION: usize> Default for DigitNum<PRECISION> {
159    fn default() -> Self {
160        Self::new()
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn t_digit_num() {
170        assert!(Digit::try_from(10).is_err());
171        let mut num: DigitNum<6> = DigitNum::new();
172        num.push_lowest_digit(0.try_into().unwrap());
173        num.push_lowest_digit(3.try_into().unwrap());
174        num.push_lowest_digit(9.try_into().unwrap());
175        assert_eq!(
176            num.to_string_with_params(DigitNumFormat {
177                underscores: false,
178                omit_trailing_dot: false
179            }),
180            "0.000039"
181        );
182        assert_eq!(
183            num.to_string_with_params(DigitNumFormat {
184                underscores: true,
185                omit_trailing_dot: false
186            }),
187            "0.000_039"
188        );
189        for d in [7, 1, 3, 4] {
190            num.push_lowest_digit(d.try_into().unwrap());
191        }
192        assert_eq!(
193            num.to_string_with_params(DigitNumFormat {
194                underscores: false,
195                omit_trailing_dot: false
196            }),
197            "0.397134"
198        );
199        for d in [5, 3, 5, 2, 9] {
200            num.push_lowest_digit(d.try_into().unwrap());
201        }
202        assert_eq!(
203            num.to_string_with_params(DigitNumFormat {
204                underscores: false,
205                omit_trailing_dot: false
206            }),
207            "39713.453529"
208        );
209        assert_eq!(
210            num.to_string_with_params(DigitNumFormat {
211                underscores: true,
212                omit_trailing_dot: false
213            }),
214            "39_713.453_529"
215        );
216        let num_u64 = u64::try_from(&num).unwrap();
217        assert_eq!(num_u64, 39713453529);
218    }
219}