|
1 | 1 | use std::cmp;
|
2 | 2 | use std::convert::{TryFrom, TryInto};
|
3 | 3 |
|
4 |
| -use bigdecimal::BigDecimal; |
| 4 | +use bigdecimal::{BigDecimal, ToPrimitive, Zero}; |
5 | 5 | use num_bigint::{BigInt, Sign};
|
6 | 6 |
|
7 | 7 | use crate::decode::Decode;
|
@@ -77,65 +77,64 @@ impl TryFrom<&'_ BigDecimal> for PgNumeric {
|
77 | 77 | type Error = BoxDynError;
|
78 | 78 |
|
79 | 79 | fn try_from(decimal: &BigDecimal) -> Result<Self, BoxDynError> {
|
80 |
| - let base_10_to_10000 = |chunk: &[u8]| chunk.iter().fold(0i16, |a, &d| a * 10 + d as i16); |
| 80 | + if decimal.is_zero() { |
| 81 | + return Ok(PgNumeric::Number { |
| 82 | + sign: PgNumericSign::Positive, |
| 83 | + scale: 0, |
| 84 | + weight: 0, |
| 85 | + digits: vec![], |
| 86 | + }); |
| 87 | + } |
81 | 88 |
|
82 | 89 | // NOTE: this unfortunately copies the BigInt internally
|
83 | 90 | let (integer, exp) = decimal.as_bigint_and_exponent();
|
84 | 91 |
|
85 |
| - // this routine is specifically optimized for base-10 |
86 |
| - // FIXME: is there a way to iterate over the digits to avoid the Vec allocation |
87 |
| - let (sign, base_10) = integer.to_radix_be(10); |
88 |
| - |
89 |
| - // weight is positive power of 10000 |
90 |
| - // exp is the negative power of 10 |
91 |
| - let weight_10 = base_10.len() as i64 - exp; |
92 |
| - |
93 | 92 | // scale is only nonzero when we have fractional digits
|
94 | 93 | // since `exp` is the _negative_ decimal exponent, it tells us
|
95 | 94 | // exactly what our scale should be
|
96 | 95 | let scale: i16 = cmp::max(0, exp).try_into()?;
|
97 | 96 |
|
98 |
| - // there's an implicit +1 offset in the interpretation |
99 |
| - let weight: i16 = if weight_10 <= 0 { |
100 |
| - weight_10 / 4 - 1 |
101 |
| - } else { |
102 |
| - // the `-1` is a fix for an off by 1 error (4 digits should still be 0 weight) |
103 |
| - (weight_10 - 1) / 4 |
104 |
| - } |
105 |
| - .try_into()?; |
| 97 | + let (sign, uint) = integer.into_parts(); |
| 98 | + let mut mantissa = uint.to_u128().unwrap(); |
106 | 99 |
|
107 |
| - let digits_len = if base_10.len() % 4 != 0 { |
108 |
| - base_10.len() / 4 + 1 |
109 |
| - } else { |
110 |
| - base_10.len() / 4 |
111 |
| - }; |
| 100 | + // If our scale is not a multiple of 4, we need to go to the next |
| 101 | + // multiple. |
| 102 | + let groups_diff = scale % 4; |
| 103 | + if groups_diff > 0 { |
| 104 | + let remainder = 4 - groups_diff as u32; |
| 105 | + let power = 10u32.pow(remainder as u32) as u128; |
112 | 106 |
|
113 |
| - let offset = weight_10.rem_euclid(4) as usize; |
| 107 | + mantissa = mantissa * power; |
| 108 | + } |
114 | 109 |
|
115 |
| - let mut digits = Vec::with_capacity(digits_len); |
| 110 | + // Array to store max mantissa of Decimal in Postgres decimal format. |
| 111 | + let mut digits = Vec::with_capacity(8); |
116 | 112 |
|
117 |
| - if let Some(first) = base_10.get(..offset) { |
118 |
| - if offset != 0 { |
119 |
| - digits.push(base_10_to_10000(first)); |
120 |
| - } |
| 113 | + // Convert to base-10000. |
| 114 | + while mantissa != 0 { |
| 115 | + digits.push((mantissa % 10_000) as i16); |
| 116 | + mantissa /= 10_000; |
121 | 117 | }
|
122 | 118 |
|
123 |
| - if let Some(rest) = base_10.get(offset..) { |
124 |
| - digits.extend( |
125 |
| - rest.chunks(4) |
126 |
| - .map(|chunk| base_10_to_10000(chunk) * 10i16.pow(4 - chunk.len() as u32)), |
127 |
| - ); |
128 |
| - } |
| 119 | + // Change the endianness. |
| 120 | + digits.reverse(); |
| 121 | + |
| 122 | + // Weight is number of digits on the left side of the decimal. |
| 123 | + let digits_after_decimal = (scale + 3) as u16 / 4; |
| 124 | + let weight = digits.len() as i16 - digits_after_decimal as i16 - 1; |
129 | 125 |
|
| 126 | + // Remove non-significant zeroes. |
130 | 127 | while let Some(&0) = digits.last() {
|
131 | 128 | digits.pop();
|
132 | 129 | }
|
133 | 130 |
|
| 131 | + let sign = match sign { |
| 132 | + Sign::Plus | Sign::NoSign => PgNumericSign::Positive, |
| 133 | + Sign::Minus => PgNumericSign::Negative, |
| 134 | + }; |
| 135 | + |
134 | 136 | Ok(PgNumeric::Number {
|
135 |
| - sign: match sign { |
136 |
| - Sign::Plus | Sign::NoSign => PgNumericSign::Positive, |
137 |
| - Sign::Minus => PgNumericSign::Negative, |
138 |
| - }, |
| 137 | + sign, |
139 | 138 | scale,
|
140 | 139 | weight,
|
141 | 140 | digits,
|
|
0 commit comments