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}