rbdc_pg/types/
bigdecimal.rs

1use crate::arguments::PgArgumentBuffer;
2use crate::types::decode::Decode;
3use crate::types::encode::{Encode, IsNull};
4use crate::types::numeric::{PgNumeric, PgNumericSign};
5use crate::value::{PgValue, PgValueFormat};
6use bigdecimal::BigDecimal;
7use num_bigint::{BigInt, Sign};
8use rbdc::Error;
9use std::cmp;
10use std::num::TryFromIntError;
11
12impl TryFrom<PgNumeric> for BigDecimal {
13    type Error = Error;
14
15    fn try_from(numeric: PgNumeric) -> Result<Self, Error> {
16        let (digits, sign, weight) = match numeric {
17            PgNumeric::Number {
18                digits,
19                sign,
20                weight,
21                ..
22            } => (digits, sign, weight),
23
24            PgNumeric::NotANumber => {
25                return Err("BigDecimal does not support NaN values".into());
26            }
27        };
28
29        if digits.is_empty() {
30            // Postgres returns an empty digit array for 0 but BigInt expects at least one zero
31            return Ok(0u64.into());
32        }
33
34        let sign = match sign {
35            PgNumericSign::Positive => Sign::Plus,
36            PgNumericSign::Negative => Sign::Minus,
37        };
38
39        // weight is 0 if the decimal point falls after the first base-10000 digit
40        let scale = (digits.len() as i64 - weight as i64 - 1) * 4;
41
42        // no optimized algorithm for base-10 so use base-100 for faster processing
43        let mut cents = Vec::with_capacity(digits.len() * 2);
44        for digit in &digits {
45            cents.push((digit / 100) as u8);
46            cents.push((digit % 100) as u8);
47        }
48
49        let bigint = BigInt::from_radix_be(sign, &cents, 100)
50            .ok_or("PgNumeric contained an out-of-range digit")?;
51
52        Ok(BigDecimal::new(bigint, scale))
53    }
54}
55
56impl TryFrom<&'_ BigDecimal> for PgNumeric {
57    type Error = Error;
58
59    fn try_from(decimal: &BigDecimal) -> Result<Self, Error> {
60        let base_10_to_10000 = |chunk: &[u8]| chunk.iter().fold(0i16, |a, &d| a * 10 + d as i16);
61
62        // NOTE: this unfortunately copies the BigInt internally
63        let (integer, exp) = decimal.as_bigint_and_exponent();
64
65        // this routine is specifically optimized for base-10
66        // FIXME: is there a way to iterate over the digits to avoid the Vec allocation
67        let (sign, base_10) = integer.to_radix_be(10);
68
69        // weight is positive power of 10000
70        // exp is the negative power of 10
71        let weight_10 = base_10.len() as i64 - exp;
72
73        // scale is only nonzero when we have fractional digits
74        // since `exp` is the _negative_ decimal exponent, it tells us
75        // exactly what our scale should be
76        let scale: i16 = cmp::max(0, exp)
77            .try_into()
78            .map_err(|e: TryFromIntError| Error::from(e.to_string()))?;
79
80        // there's an implicit +1 offset in the interpretation
81        let weight: i16 = if weight_10 <= 0 {
82            weight_10 / 4 - 1
83        } else {
84            // the `-1` is a fix for an off by 1 error (4 digits should still be 0 weight)
85            (weight_10 - 1) / 4
86        }
87        .try_into()
88        .map_err(|e: TryFromIntError| Error::from(e.to_string()))?;
89
90        let digits_len = if base_10.len() % 4 != 0 {
91            base_10.len() / 4 + 1
92        } else {
93            base_10.len() / 4
94        };
95
96        let offset = weight_10.rem_euclid(4) as usize;
97
98        let mut digits = Vec::with_capacity(digits_len);
99
100        if let Some(first) = base_10.get(..offset) {
101            if !first.is_empty() {
102                digits.push(base_10_to_10000(first));
103            }
104        } else if offset != 0 {
105            digits.push(base_10_to_10000(&base_10) * 10i16.pow((offset - base_10.len()) as u32));
106        }
107
108        if let Some(rest) = base_10.get(offset..) {
109            digits.extend(
110                rest.chunks(4)
111                    .map(|chunk| base_10_to_10000(chunk) * 10i16.pow(4 - chunk.len() as u32)),
112            );
113        }
114
115        while let Some(&0) = digits.last() {
116            digits.pop();
117        }
118
119        Ok(PgNumeric::Number {
120            sign: match sign {
121                Sign::Plus | Sign::NoSign => PgNumericSign::Positive,
122                Sign::Minus => PgNumericSign::Negative,
123            },
124            scale,
125            weight,
126            digits,
127        })
128    }
129}
130
131/// ### Panics
132/// If this `BigDecimal` cannot be represented by `PgNumeric`.
133impl Encode for BigDecimal {
134    fn encode(self, buf: &mut PgArgumentBuffer) -> Result<IsNull, Error> {
135        PgNumeric::try_from(&self)
136            .expect("BigDecimal magnitude too great for Postgres NUMERIC type")
137            .encode(buf);
138        Ok(IsNull::No)
139    }
140}
141
142impl Decode for BigDecimal {
143    fn decode(value: PgValue) -> Result<Self, Error> {
144        match value.format() {
145            PgValueFormat::Binary => PgNumeric::decode(value.as_bytes()?)?.try_into(),
146            PgValueFormat::Text => Ok(value
147                .as_str()?
148                .parse::<BigDecimal>()
149                .map_err(|e| Error::from(e.to_string()))?),
150        }
151    }
152}
153
154#[cfg(test)]
155mod bigdecimal_to_pgnumeric {
156    use super::{BigDecimal, PgNumeric, PgNumericSign};
157
158    #[test]
159    fn zero() {
160        let zero: BigDecimal = "0".parse().unwrap();
161
162        assert_eq!(
163            PgNumeric::try_from(&zero).unwrap(),
164            PgNumeric::Number {
165                sign: PgNumericSign::Positive,
166                scale: 0,
167                weight: 0,
168                digits: vec![]
169            }
170        );
171    }
172
173    #[test]
174    fn one() {
175        let one: BigDecimal = "1".parse().unwrap();
176        assert_eq!(
177            PgNumeric::try_from(&one).unwrap(),
178            PgNumeric::Number {
179                sign: PgNumericSign::Positive,
180                scale: 0,
181                weight: 0,
182                digits: vec![1]
183            }
184        );
185    }
186
187    #[test]
188    fn ten() {
189        let ten: BigDecimal = "10".parse().unwrap();
190        assert_eq!(
191            PgNumeric::try_from(&ten).unwrap(),
192            PgNumeric::Number {
193                sign: PgNumericSign::Positive,
194                scale: 0,
195                weight: 0,
196                digits: vec![10]
197            }
198        );
199    }
200
201    #[test]
202    fn one_hundred() {
203        let one_hundred: BigDecimal = "100".parse().unwrap();
204        assert_eq!(
205            PgNumeric::try_from(&one_hundred).unwrap(),
206            PgNumeric::Number {
207                sign: PgNumericSign::Positive,
208                scale: 0,
209                weight: 0,
210                digits: vec![100]
211            }
212        );
213    }
214
215    #[test]
216    fn ten_thousand() {
217        // BigDecimal doesn't normalize here
218        let ten_thousand: BigDecimal = "10000".parse().unwrap();
219        assert_eq!(
220            PgNumeric::try_from(&ten_thousand).unwrap(),
221            PgNumeric::Number {
222                sign: PgNumericSign::Positive,
223                scale: 0,
224                weight: 1,
225                digits: vec![1]
226            }
227        );
228    }
229
230    #[test]
231    fn two_digits() {
232        let two_digits: BigDecimal = "12345".parse().unwrap();
233        assert_eq!(
234            PgNumeric::try_from(&two_digits).unwrap(),
235            PgNumeric::Number {
236                sign: PgNumericSign::Positive,
237                scale: 0,
238                weight: 1,
239                digits: vec![1, 2345]
240            }
241        );
242    }
243
244    #[test]
245    fn one_tenth() {
246        let one_tenth: BigDecimal = "0.1".parse().unwrap();
247        assert_eq!(
248            PgNumeric::try_from(&one_tenth).unwrap(),
249            PgNumeric::Number {
250                sign: PgNumericSign::Positive,
251                scale: 1,
252                weight: -1,
253                digits: vec![1000]
254            }
255        );
256    }
257
258    #[test]
259    fn one_hundredth() {
260        let one_hundredth: BigDecimal = "0.01".parse().unwrap();
261        assert_eq!(
262            PgNumeric::try_from(&one_hundredth).unwrap(),
263            PgNumeric::Number {
264                sign: PgNumericSign::Positive,
265                scale: 2,
266                weight: -1,
267                digits: vec![100]
268            }
269        );
270    }
271
272    #[test]
273    fn twelve_thousandths() {
274        let twelve_thousandths: BigDecimal = "0.012".parse().unwrap();
275        assert_eq!(
276            PgNumeric::try_from(&twelve_thousandths).unwrap(),
277            PgNumeric::Number {
278                sign: PgNumericSign::Positive,
279                scale: 3,
280                weight: -1,
281                digits: vec![120]
282            }
283        );
284    }
285
286    #[test]
287    fn decimal_1() {
288        let decimal: BigDecimal = "1.2345".parse().unwrap();
289        assert_eq!(
290            PgNumeric::try_from(&decimal).unwrap(),
291            PgNumeric::Number {
292                sign: PgNumericSign::Positive,
293                scale: 4,
294                weight: 0,
295                digits: vec![1, 2345]
296            }
297        );
298    }
299
300    #[test]
301    fn decimal_2() {
302        let decimal: BigDecimal = "0.12345".parse().unwrap();
303        assert_eq!(
304            PgNumeric::try_from(&decimal).unwrap(),
305            PgNumeric::Number {
306                sign: PgNumericSign::Positive,
307                scale: 5,
308                weight: -1,
309                digits: vec![1234, 5000]
310            }
311        );
312    }
313
314    #[test]
315    fn decimal_3() {
316        let decimal: BigDecimal = "0.01234".parse().unwrap();
317        assert_eq!(
318            PgNumeric::try_from(&decimal).unwrap(),
319            PgNumeric::Number {
320                sign: PgNumericSign::Positive,
321                scale: 5,
322                weight: -1,
323                digits: vec![0123, 4000]
324            }
325        );
326    }
327
328    #[test]
329    fn decimal_4() {
330        let decimal: BigDecimal = "12345.67890".parse().unwrap();
331        assert_eq!(
332            PgNumeric::try_from(&decimal).unwrap(),
333            PgNumeric::Number {
334                sign: PgNumericSign::Positive,
335                scale: 5,
336                weight: 1,
337                digits: vec![1, 2345, 6789]
338            }
339        );
340    }
341
342    #[test]
343    fn one_digit_decimal() {
344        let one_digit_decimal: BigDecimal = "0.00001234".parse().unwrap();
345        assert_eq!(
346            PgNumeric::try_from(&one_digit_decimal).unwrap(),
347            PgNumeric::Number {
348                sign: PgNumericSign::Positive,
349                scale: 8,
350                weight: -2,
351                digits: vec![1234]
352            }
353        );
354    }
355
356    #[test]
357    fn issue_423_four_digit() {
358        let four_digit: BigDecimal = "1234".parse().unwrap();
359        assert_eq!(
360            PgNumeric::try_from(&four_digit).unwrap(),
361            PgNumeric::Number {
362                sign: PgNumericSign::Positive,
363                scale: 0,
364                weight: 0,
365                digits: vec![1234]
366            }
367        );
368    }
369
370    #[test]
371    fn issue_423_negative_four_digit() {
372        let negative_four_digit: BigDecimal = "-1234".parse().unwrap();
373        assert_eq!(
374            PgNumeric::try_from(&negative_four_digit).unwrap(),
375            PgNumeric::Number {
376                sign: PgNumericSign::Negative,
377                scale: 0,
378                weight: 0,
379                digits: vec![1234]
380            }
381        );
382    }
383
384    #[test]
385    fn issue_423_eight_digit() {
386        let eight_digit: BigDecimal = "12345678".parse().unwrap();
387        assert_eq!(
388            PgNumeric::try_from(&eight_digit).unwrap(),
389            PgNumeric::Number {
390                sign: PgNumericSign::Positive,
391                scale: 0,
392                weight: 1,
393                digits: vec![1234, 5678]
394            }
395        );
396    }
397
398    #[test]
399    fn issue_423_negative_eight_digit() {
400        let negative_eight_digit: BigDecimal = "-12345678".parse().unwrap();
401        assert_eq!(
402            PgNumeric::try_from(&negative_eight_digit).unwrap(),
403            PgNumeric::Number {
404                sign: PgNumericSign::Negative,
405                scale: 0,
406                weight: 1,
407                digits: vec![1234, 5678]
408            }
409        );
410    }
411}