odbc_api/
conversion.rs

1use std::ops::{Add, 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    // We would be done now, if every database would include trailing zeroes, but they might choose
81    // to omit those. Therfore we see if we need to leftshift n further in order to meet scale.
82    for _ in 0..(scale - num_digits_low) {
83        n *= I::TEN;
84    }
85    n
86}
87
88trait ToDecimal:
89    FromRadix10 + FromRadix10Signed + Add<Output = Self> + Sub<Output = Self> + MulAssign + Ord
90{
91    const ZERO: Self;
92    const TEN: Self;
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    /// An user of an Oracle database got invalid values from decimal after setting
100    /// `NLS_NUMERIC_CHARACTERS` to ",." instead of ".".
101    ///
102    /// See issue:
103    /// <https://github.com/pacman82/arrow-odbc-py/discussions/74#discussioncomment-8083928>
104    #[test]
105    fn decimal_is_represented_with_comma_as_radix() {
106        let actual = decimal_text_to_i128(b"10,00000", 5);
107        assert_eq!(1_000_000, actual);
108    }
109
110    /// Since scale is 5 in this test case we would expect five digits after the radix, yet Oracle
111    /// seems to not emit trailing zeroes. Also see issue:
112    /// <https://github.com/pacman82/arrow-odbc-py/discussions/74#discussioncomment-8083928>
113    #[test]
114    fn decimal_with_less_zeroes() {
115        let actual = decimal_text_to_i128(b"10.0", 5);
116        assert_eq!(1_000_000, actual);
117    }
118
119    #[test]
120    fn negative_decimal() {
121        let actual = decimal_text_to_i128(b"-10.00000", 5);
122        assert_eq!(-1_000_000, actual);
123    }
124
125    #[test]
126    fn negative_decimal_small() {
127        let actual = decimal_text_to_i128(b"-0.1", 5);
128        assert_eq!(-10000, actual);
129    }
130
131    // i64
132
133    /// Since scale is 5 in this test case we would expect five digits after the radix, yet Oracle
134    /// seems to not emit trailing zeroes. Also see issue:
135    /// <https://github.com/pacman82/arrow-odbc-py/discussions/74#discussioncomment-8083928>
136    #[test]
137    fn decimal_with_less_zeroes_i64() {
138        let actual = decimal_text_to_i64(b"10.0", 5);
139        assert_eq!(1_000_000, actual);
140    }
141
142    #[test]
143    fn negative_decimal_i64() {
144        let actual = decimal_text_to_i64(b"-10.00000", 5);
145        assert_eq!(-1_000_000, actual);
146    }
147
148    #[test]
149    fn negative_decimal_small_i64() {
150        let actual = decimal_text_to_i64(b"-0.1", 5);
151        assert_eq!(-10000, actual);
152    }
153
154    // i32
155
156    /// Since scale is 5 in this test case we would expect five digits after the radix, yet Oracle
157    /// seems to not emit trailing zeroes. Also see issue:
158    /// <https://github.com/pacman82/arrow-odbc-py/discussions/74#discussioncomment-8083928>
159    #[test]
160    fn decimal_with_less_zeroes_i32() {
161        let actual = decimal_text_to_i32(b"10.0", 5);
162        assert_eq!(1_000_000, actual);
163    }
164
165    #[test]
166    fn negative_decimal_i32() {
167        let actual = decimal_text_to_i32(b"-10.00000", 5);
168        assert_eq!(-1_000_000, actual);
169    }
170
171    #[test]
172    fn negative_decimal_small_i32() {
173        let actual = decimal_text_to_i32(b"-0.1", 5);
174        assert_eq!(-10000, actual);
175    }
176}