precise_calc/
formatting.rs

1//! Contains functions used to format a [BigFloat] to a string.
2
3use std::collections::VecDeque;
4
5use astro_float::{BigFloat, Sign};
6
7use crate::{BASE_10_PREC, RM};
8
9/// Round mantissa to `precision` base 10 digits.
10fn round_to_digit(
11    mut exponent: i32,
12    mantissa: Vec<u8>,
13    precision: usize,
14) -> Option<(i32, Vec<u8>)> {
15    let mut mantissa = VecDeque::from(mantissa);
16
17    // Find index of first non-zero digit in mantissa
18    let i = match mantissa.iter().enumerate().find(|&(_, &digit)| digit != 0) {
19        Some((i, _)) => i,
20        None => return Some((exponent, Vec::from(mantissa))),
21    };
22
23    // Get digit precision digits after i
24    // TODO: Check this algorithm
25    let rounding_digit = match mantissa.get(i + precision) {
26        Some(digit) => digit,
27        None => {
28            return Some((exponent, Vec::from(mantissa)));
29        }
30    };
31    if *rounding_digit >= 5 {
32        // Round up
33        // Add 1 to mantissa[i+precision-1];
34        // Carry add to previous digit
35        let old_len = mantissa.len();
36        mantissa.truncate(i + precision);
37        mantissa = add1_to_vec(mantissa)?;
38        if mantissa.len() > old_len {
39            exponent += 1;
40        }
41    } else {
42        // Round down
43        // Just chop off extra digits
44        mantissa.truncate(i + precision);
45    }
46
47    let mantissa = Vec::from(mantissa);
48    Some((exponent, mantissa))
49}
50
51fn add1_to_vec(mut digits: VecDeque<u8>) -> Option<VecDeque<u8>> {
52    // Add one to last digit
53    *digits.get_mut(digits.len() - 1)? += 1;
54
55    // Perform carry
56    let mut i = digits.len() - 1;
57    while digits[i] == 10 {
58        digits[i] = 0;
59
60        if i == 0 {
61            // Add 1 to front of vector
62            digits.push_front(1);
63            break;
64        } else {
65            digits[i - 1] += 1;
66            i -= 1;
67        }
68    }
69
70    Some(digits)
71}
72
73fn format_num(sign: astro_float::Sign, mantissa: &[u8], mut expt: i32) -> String {
74    // Remove trailing zeros
75    let mut mantissa = Vec::from(mantissa);
76
77    // Handle 0 case
78    if mantissa.is_empty() {
79        return "0".to_string();
80    }
81
82    while *mantissa.last().unwrap() == 0 {
83        mantissa.pop();
84    }
85
86    if expt.abs() > 3 {
87        // The way astro_float does exponents is weird
88        // 0.123e1 = 1.23 (astro_float way)
89        // 1.23e0 = 1.23 (my way)
90        expt -= 1;
91
92        // Scientific notation
93        let mut bytes: Vec<u8> = Vec::new();
94
95        // Handle negative sign
96        if sign == Sign::Neg {
97            bytes.reserve(mantissa.len() + expt.abs() as usize / 10 + 3); // number of digits + exponent digits + 'e' + '.' + '-'
98            bytes.push(b'-');
99        } else {
100            bytes.reserve(mantissa.len() + expt.abs() as usize / 10 + 2);
101        }
102
103        // Push first digit
104        bytes.push(mantissa[0] + 48);
105
106        // Add digits after decimal place
107        bytes.push(b'.');
108        if mantissa.len() > 1 {
109            for d in &mantissa[1..] {
110                bytes.push(d + 48);
111            }
112        } else {
113            // Add '0'
114            bytes.push(b'0');
115        }
116
117        // Add exponent
118        bytes.push(b'e');
119        // TODO: Allocate space for this negative at start
120        if expt < 0 {
121            bytes.push(b'-');
122        }
123        let mut exponent_digits: Vec<u8> =
124            expt.abs().to_string().chars().map(|d| d as u8).collect();
125        bytes.append(&mut exponent_digits);
126
127        String::from_utf8(bytes).unwrap()
128    } else {
129        // Normal format
130        let mut bytes = Vec::new();
131
132        // First, sign
133        if sign == Sign::Neg {
134            bytes.push(b'-');
135        }
136
137        // Expt plus one digits
138        if expt >= mantissa.len() as i32 {
139            // TODO: Preallocate vector
140
141            // Add digits
142            for d in &mantissa {
143                bytes.push(d + 48);
144            }
145
146            // Add trailing zeros
147            for _ in 0..(expt - mantissa.len() as i32) {
148                bytes.push(b'0');
149            }
150        } else if expt <= 0 {
151            // TODO: Preallocate vector
152
153            bytes.push(b'0');
154            bytes.push(b'.');
155            for _ in 0..expt.abs() {
156                bytes.push(b'0');
157            }
158            for d in mantissa {
159                bytes.push(d + 48);
160            }
161        } else {
162            // TODO: Preallocate vector
163
164            // Add digits before decimal
165            for d in &mantissa[0..expt as usize] {
166                bytes.push(d + 48);
167            }
168
169            // Add decimal place
170            bytes.push(b'.');
171
172            // Add digits after decimal
173            for d in &mantissa[expt as usize..] {
174                bytes.push(d + 48);
175            }
176        }
177
178        String::from_utf8(bytes).unwrap()
179    }
180}
181
182/// Convert a [BigFloat] to a string, rounding off all digits after BASE_10_PREC.
183pub fn float_to_string(num: &BigFloat) -> String {
184    let (s, m, e) = num.convert_to_radix(astro_float::Radix::Dec, RM).unwrap();
185    let (e, m) = round_to_digit(e, m, BASE_10_PREC).unwrap();
186    format_num(s, &m, e)
187}
188
189#[cfg(test)]
190mod tests {
191    use std::collections::VecDeque;
192
193    use astro_float::{BigFloat, Radix};
194
195    use crate::{
196        formatting::{add1_to_vec, format_num, round_to_digit},
197        PREC, RM,
198    };
199
200    #[test]
201    fn test_add1() {
202        let digits = VecDeque::from([0, 0, 0, 0, 9]);
203        let digits = add1_to_vec(digits).unwrap();
204        assert_eq!(digits, vec![0, 0, 0, 1, 0]);
205
206        let digits = VecDeque::from([0, 0, 0, 0, 5]);
207        let digits = add1_to_vec(digits).unwrap();
208        assert_eq!(digits, vec![0, 0, 0, 0, 6]);
209
210        let digits = VecDeque::from([9, 9, 9]);
211        let digits = add1_to_vec(digits).unwrap();
212        assert_eq!(digits, vec![1, 0, 0, 0]);
213    }
214
215    #[test]
216    fn test_round() {
217        // Precision is log_10(2^PREC)
218
219        let (_s, m, e) = BigFloat::from_f64(0.1 + 0.2, PREC)
220            .convert_to_radix(astro_float::Radix::Dec, RM)
221            .unwrap();
222        let (e, m) = round_to_digit(e, m, 15).unwrap();
223        assert_eq!(m, vec![3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
224        assert_eq!(e, 0);
225
226        let (_s, m, e) = BigFloat::from_f64(0.3, PREC)
227            .convert_to_radix(astro_float::Radix::Dec, RM)
228            .unwrap();
229        let (e, m) = round_to_digit(e, m, 15).unwrap();
230        assert_eq!(m, vec![3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
231        assert_eq!(e, 0);
232    }
233
234    #[test]
235    fn test_format() {
236        let (s, m, e) = BigFloat::from_f64(0.3, PREC)
237            .convert_to_radix(Radix::Dec, RM)
238            .unwrap();
239        let (e, m) = round_to_digit(e, m, 15).unwrap();
240        let str_num = format_num(s, &m, e);
241        assert_eq!(str_num, "0.3");
242    }
243}