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}