odbc_api/
conversion.rs

1use std::ops::{Add, DivAssign, MulAssign, Sub};
2
3use atoi::{FromRadix10, FromRadix10Signed};
4
5/// Convert the text representation of a decimal into an integer representation. The integer
6/// representation is not truncating the fraction, but is instead the value of the decimal times 10
7/// to the power of scale. E.g. 123.45 of a Decimal with scale 3 is thought of as 123.450 and
8/// represented as 123450. This method will regard any non digit character as a radix character with
9/// the exception of a `+` or `-` at the beginning of the string.
10///
11/// This method is robust against representation which do not have trailing zeroes as well as
12/// arbitrary radix character. If you do not write a generic application and now the specific way
13/// your database formats decimals you may come up with faster methods to parse decimals.
14pub fn decimal_text_to_i128(text: &[u8], scale: usize) -> i128 {
15    decimal_text_to_integer(text, scale)
16}
17
18impl ToDecimal for i128 {
19    const ZERO: Self = 0;
20    const TEN: Self = 10;
21}
22
23/// Convert the text representation of a decimal into an integer representation. The integer
24/// representation is not truncating the fraction, but is instead the value of the decimal times 10
25/// to the power of scale. E.g. 123.45 of a Decimal with scale 3 is thought of as 123.450 and
26/// represented as 123450. This method will regard any non digit character as a radix character with
27/// the exception of a `+` or `-` at the beginning of the string.
28///
29/// This method is robust against representation which do not have trailing zeroes as well as
30/// arbitrary radix character. If you do not write a generic application and now the specific way
31/// your database formats decimals you may come up with faster methods to parse decimals.
32pub fn decimal_text_to_i64(text: &[u8], scale: usize) -> i64 {
33    decimal_text_to_integer(text, scale)
34}
35
36impl ToDecimal for i64 {
37    const ZERO: Self = 0;
38    const TEN: Self = 10;
39}
40
41/// Convert the text representation of a decimal into an integer representation. The integer
42/// representation is not truncating the fraction, but is instead the value of the decimal times 10
43/// to the power of scale. E.g. 123.45 of a Decimal with scale 3 is thought of as 123.450 and
44/// represented as 123450. This method will regard any non digit character as a radix character with
45/// the exception of a `+` or `-` at the beginning of the string.
46///
47/// This method is robust against representation which do not have trailing zeroes as well as
48/// arbitrary radix character. If you do not write a generic application and now the specific way
49/// your database formats decimals you may come up with faster methods to parse decimals.
50pub fn decimal_text_to_i32(text: &[u8], scale: usize) -> i32 {
51    decimal_text_to_integer(text, scale)
52}
53
54impl ToDecimal for i32 {
55    const ZERO: Self = 0;
56    const TEN: Self = 10;
57}
58
59fn decimal_text_to_integer<I>(text: &[u8], scale: usize) -> I
60where
61    I: ToDecimal,
62{
63    // High is now the number before the decimal point
64    let (mut high, num_digits_high) = I::from_radix_10_signed(text);
65    let (low, num_digits_low) = if num_digits_high == text.len() {
66        (I::ZERO, 0)
67    } else {
68        I::from_radix_10(&text[(num_digits_high + 1)..])
69    };
70    // Left shift high so it is compatible with low
71    for _ in 0..num_digits_low {
72        high *= I::TEN;
73    }
74    // We want to increase the absolute of high by low without changing highs sign
75    let mut n = if high < I::ZERO || (high == I::ZERO && text[0] == b'-') {
76        high - low
77    } else {
78        high + low
79    };
80
81    match num_digits_low.cmp(&scale) {
82        // If the number of digits in low is less than scale, we need to left shift n
83        std::cmp::Ordering::Less => {
84            // We need to left shift n to meet scale
85            for _ in 0..(scale - num_digits_low) {
86                n *= I::TEN;
87            }
88        }
89        // If the number of digits in low is greater than scale, we need to right shift n
90        std::cmp::Ordering::Greater => {
91            // We need to right shift n to meet scale (truncate)
92            for _ in 0..(num_digits_low - scale) {
93                // Use integer division for truncation
94                n /= I::TEN;
95            }
96        }
97        // If num_digits_low == scale, we do nothing
98        std::cmp::Ordering::Equal => {}
99    }
100
101    n
102}
103
104trait ToDecimal:
105    FromRadix10
106    + FromRadix10Signed
107    + Add<Output = Self>
108    + Sub<Output = Self>
109    + MulAssign
110    + DivAssign
111    + Ord
112{
113    const ZERO: Self;
114    const TEN: Self;
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    /// An user of an Oracle database got invalid values from decimal after setting
122    /// `NLS_NUMERIC_CHARACTERS` to ",." instead of ".".
123    ///
124    /// See issue:
125    /// <https://github.com/pacman82/arrow-odbc-py/discussions/74#discussioncomment-8083928>
126    #[test]
127    fn decimal_is_represented_with_comma_as_radix() {
128        let actual = decimal_text_to_i128(b"10,00000", 5);
129        assert_eq!(1_000_000, actual);
130    }
131
132    /// Since scale is 5 in this test case we would expect five digits after the radix, yet Oracle
133    /// seems to not emit trailing zeroes. Also see issue:
134    /// <https://github.com/pacman82/arrow-odbc-py/discussions/74#discussioncomment-8083928>
135    #[test]
136    fn decimal_with_less_zeroes() {
137        let actual = decimal_text_to_i128(b"10.0", 5);
138        assert_eq!(1_000_000, actual);
139    }
140
141    #[test]
142    fn negative_decimal() {
143        let actual = decimal_text_to_i128(b"-10.00000", 5);
144        assert_eq!(-1_000_000, actual);
145    }
146
147    #[test]
148    fn negative_decimal_small() {
149        let actual = decimal_text_to_i128(b"-0.1", 5);
150        assert_eq!(-10000, actual);
151    }
152
153    // i64
154
155    /// Since scale is 5 in this test case we would expect five digits after the radix, yet Oracle
156    /// seems to not emit trailing zeroes. Also see issue:
157    /// <https://github.com/pacman82/arrow-odbc-py/discussions/74#discussioncomment-8083928>
158    #[test]
159    fn decimal_with_less_zeroes_i64() {
160        let actual = decimal_text_to_i64(b"10.0", 5);
161        assert_eq!(1_000_000, actual);
162    }
163
164    #[test]
165    fn negative_decimal_i64() {
166        let actual = decimal_text_to_i64(b"-10.00000", 5);
167        assert_eq!(-1_000_000, actual);
168    }
169
170    #[test]
171    fn negative_decimal_small_i64() {
172        let actual = decimal_text_to_i64(b"-0.1", 5);
173        assert_eq!(-10000, actual);
174    }
175
176    // i32
177
178    /// Since scale is 5 in this test case we would expect five digits after the radix, yet Oracle
179    /// seems to not emit trailing zeroes. Also see issue:
180    /// <https://github.com/pacman82/arrow-odbc-py/discussions/74#discussioncomment-8083928>
181    #[test]
182    fn decimal_with_less_zeroes_i32() {
183        let actual = decimal_text_to_i32(b"10.0", 5);
184        assert_eq!(1_000_000, actual);
185    }
186
187    #[test]
188    fn negative_decimal_i32() {
189        let actual = decimal_text_to_i32(b"-10.00000", 5);
190        assert_eq!(-1_000_000, actual);
191    }
192
193    #[test]
194    fn negative_decimal_small_i32() {
195        let actual = decimal_text_to_i32(b"-0.1", 5);
196        assert_eq!(-10000, actual);
197    }
198
199    #[test]
200    fn decimal_with_too_much_scale() {
201        let actual = decimal_text_to_i128(b"10.000000", 5);
202
203        assert_eq!(1_000_000, actual);
204    }
205
206    #[test]
207    fn decimal_with_too_much_scale_negative() {
208        let actual = decimal_text_to_i128(b"-10.123456", 5);
209
210        assert_eq!(-1_012_345, actual);
211    }
212
213    #[test]
214    fn decimal_with_too_much_scale_small_negative() {
215        let actual = decimal_text_to_i128(b"-0.123456", 5);
216
217        assert_eq!(-12345, actual);
218    }
219}