iceberg/spec/values/
decimal_utils.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Compatibility layer for decimal operations.
19//!
20//! Provides rust_decimal-compatible API using fastnum's D128 internally.
21//! D128 supports 38-digit precision, meeting the Iceberg spec requirement.
22
23use fastnum::D128;
24use fastnum::decimal::Context;
25
26use crate::{Error, ErrorKind, Result};
27
28/// Re-export D128 as the Decimal type for use throughout the crate.
29pub type Decimal = D128;
30
31/// Create a D128 from mantissa (i128) and scale (u32).
32///
33/// This is equivalent to rust_decimal's `Decimal::from_i128_with_scale`.
34/// The value is computed as: mantissa * 10^(-scale)
35///
36/// For example:
37/// - mantissa=12345, scale=2 => 123.45
38/// - mantissa=-456, scale=3 => -0.456
39pub fn decimal_from_i128_with_scale(mantissa: i128, scale: u32) -> Decimal {
40    if scale == 0 {
41        return D128::from_i128(mantissa).expect("i128 always fits in D128");
42    }
43
44    // Convert mantissa to string and insert decimal point at the right position
45    let is_negative = mantissa < 0;
46    let abs_str = mantissa.unsigned_abs().to_string();
47    let scale_usize = scale as usize;
48
49    let decimal_str = if abs_str.len() <= scale_usize {
50        // Need leading zeros: e.g., mantissa=456, scale=3 => "0.456"
51        // Or mantissa=5, scale=3 => "0.005"
52        let zeros_needed = scale_usize - abs_str.len();
53        format!(
54            "{}0.{}{}",
55            if is_negative { "-" } else { "" },
56            "0".repeat(zeros_needed),
57            abs_str
58        )
59    } else {
60        // Insert decimal point: e.g., mantissa=12345, scale=2 => "123.45"
61        let decimal_pos = abs_str.len() - scale_usize;
62        format!(
63            "{}{}.{}",
64            if is_negative { "-" } else { "" },
65            &abs_str[..decimal_pos],
66            &abs_str[decimal_pos..]
67        )
68    };
69
70    D128::from_str(&decimal_str, Context::default())
71        .expect("constructed decimal string is always valid")
72}
73
74/// Try to create a D128 from mantissa and scale, with validation.
75///
76/// This is equivalent to rust_decimal's `Decimal::try_from_i128_with_scale`.
77/// Currently always succeeds for 38-digit decimals.
78pub fn try_decimal_from_i128_with_scale(mantissa: i128, scale: u32) -> Result<Decimal> {
79    // For now, always succeeds since D128 supports full 38-digit precision
80    Ok(decimal_from_i128_with_scale(mantissa, scale))
81}
82
83/// Create a D128 from i64 mantissa and scale.
84///
85/// This is equivalent to rust_decimal's `Decimal::new`.
86#[allow(dead_code)]
87pub fn decimal_new(mantissa: i64, scale: u32) -> Decimal {
88    decimal_from_i128_with_scale(mantissa as i128, scale)
89}
90
91/// Parse a decimal from string with exact representation.
92///
93/// This is equivalent to rust_decimal's `Decimal::from_str_exact`.
94pub fn decimal_from_str_exact(s: &str) -> Result<Decimal> {
95    D128::from_str(s, Context::default())
96        .map_err(|e| Error::new(ErrorKind::DataInvalid, format!("Can't parse decimal: {e}")))
97}
98
99/// Get the mantissa (unscaled coefficient) as i128.
100///
101/// This is equivalent to rust_decimal's `decimal.mantissa()`.
102///
103/// The mantissa is signed: negative decimals return negative mantissa.
104pub fn decimal_mantissa(d: &Decimal) -> i128 {
105    // digits() returns unsigned coefficient as UInt<N>
106    // For Iceberg decimals (max 38 digits), this always fits in u128/i128
107    let digits = d.digits();
108
109    // Convert UInt<2> to u128 - this always succeeds for Iceberg-compliant decimals
110    // since 38 digits requires ~127 bits and u128 has 128 bits
111    let unsigned: u128 = digits
112        .to_u128()
113        .expect("Iceberg decimals (max 38 digits) always fit in u128");
114
115    let signed = unsigned as i128;
116    if d.is_sign_negative() {
117        -signed
118    } else {
119        signed
120    }
121}
122
123/// Get the scale (number of digits after decimal point).
124///
125/// This is equivalent to rust_decimal's `decimal.scale()`.
126pub fn decimal_scale(d: &Decimal) -> u32 {
127    let frac = d.fractional_digits_count();
128    if frac < 0 { 0 } else { frac as u32 }
129}
130
131/// Rescale a decimal to the given scale, returning the rescaled value.
132///
133/// This is equivalent to rust_decimal's `decimal.rescale(scale)`.
134pub fn decimal_rescale(d: Decimal, scale: u32) -> Decimal {
135    d.rescale(scale as i16)
136}
137
138/// Convert big-endian signed bytes to i128.
139///
140/// This handles variable-length byte arrays (up to 16 bytes) with sign extension.
141/// Returns None if the byte array is longer than 16 bytes.
142pub fn i128_from_be_bytes(bytes: &[u8]) -> Option<i128> {
143    if bytes.is_empty() {
144        return Some(0);
145    }
146    if bytes.len() > 16 {
147        return None; // Too large for i128
148    }
149
150    // Check sign bit (most significant bit of first byte)
151    let is_negative = bytes[0] & 0x80 != 0;
152
153    // Pad to 16 bytes with sign extension
154    let mut padded = if is_negative { [0xFF; 16] } else { [0; 16] };
155    let start = 16 - bytes.len();
156    padded[start..].copy_from_slice(bytes);
157
158    Some(i128::from_be_bytes(padded))
159}
160
161/// Convert i128 to big-endian signed bytes with minimum length.
162///
163/// This produces the shortest two's complement representation of the value.
164/// The result is suitable for Iceberg decimal binary serialization.
165pub fn i128_to_be_bytes_min(value: i128) -> Vec<u8> {
166    let bytes = value.to_be_bytes();
167
168    // Find the first significant byte
169    // For positive numbers, skip leading 0x00 bytes (but keep sign bit)
170    // For negative numbers, skip leading 0xFF bytes (but keep sign bit)
171    let is_negative = value < 0;
172    let skip_byte = if is_negative { 0xFF } else { 0x00 };
173
174    let mut start = 0;
175    while start < 15 && bytes[start] == skip_byte {
176        // Check if the next byte has the correct sign bit
177        let next_byte = bytes[start + 1];
178        let next_is_negative = (next_byte & 0x80) != 0;
179        if next_is_negative == is_negative {
180            start += 1;
181        } else {
182            break;
183        }
184    }
185
186    bytes[start..].to_vec()
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_decimal_from_i128_with_scale() {
195        let d = decimal_from_i128_with_scale(12345, 2);
196        assert_eq!(d.to_string(), "123.45");
197
198        let d = decimal_from_i128_with_scale(-12345, 2);
199        assert_eq!(d.to_string(), "-123.45");
200
201        let d = decimal_from_i128_with_scale(0, 5);
202        assert_eq!(d.to_string(), "0.00000");
203    }
204
205    #[test]
206    fn test_decimal_new() {
207        let d = decimal_new(123, 2);
208        assert_eq!(d.to_string(), "1.23");
209
210        let d = decimal_new(-456, 3);
211        assert_eq!(d.to_string(), "-0.456");
212    }
213
214    #[test]
215    fn test_decimal_from_str_exact() {
216        let d = decimal_from_str_exact("123.45").unwrap();
217        assert_eq!(d.to_string(), "123.45");
218
219        let d = decimal_from_str_exact("-0.001").unwrap();
220        assert_eq!(d.to_string(), "-0.001");
221
222        let d = decimal_from_str_exact("99999999999999999999999999999999999999").unwrap();
223        assert_eq!(d.to_string(), "99999999999999999999999999999999999999");
224    }
225
226    #[test]
227    fn test_decimal_mantissa() {
228        let d = decimal_from_i128_with_scale(12345, 2);
229        assert_eq!(decimal_mantissa(&d), 12345);
230
231        let d = decimal_from_i128_with_scale(-12345, 2);
232        assert_eq!(decimal_mantissa(&d), -12345);
233    }
234
235    #[test]
236    fn test_decimal_scale() {
237        let d = decimal_from_i128_with_scale(12345, 2);
238        assert_eq!(decimal_scale(&d), 2);
239
240        let d = decimal_from_i128_with_scale(12345, 0);
241        assert_eq!(decimal_scale(&d), 0);
242    }
243
244    #[test]
245    fn test_decimal_rescale() {
246        let d = decimal_from_str_exact("123.45").unwrap();
247        let rescaled = decimal_rescale(d, 4);
248        assert_eq!(decimal_scale(&rescaled), 4);
249        assert_eq!(decimal_mantissa(&rescaled), 1234500);
250    }
251
252    #[test]
253    fn test_38_digit_precision() {
254        // Test that we can handle 38-digit decimals (Iceberg spec requirement)
255        let max_38_digits = "99999999999999999999999999999999999999";
256        let d = decimal_from_str_exact(max_38_digits).unwrap();
257        assert_eq!(d.to_string(), max_38_digits);
258
259        let min_38_digits = "-99999999999999999999999999999999999999";
260        let d = decimal_from_str_exact(min_38_digits).unwrap();
261        assert_eq!(d.to_string(), min_38_digits);
262    }
263
264    #[test]
265    fn test_i128_from_be_bytes() {
266        // Empty bytes
267        assert_eq!(i128_from_be_bytes(&[]), Some(0));
268
269        // Positive values
270        assert_eq!(i128_from_be_bytes(&[0x01]), Some(1));
271        assert_eq!(i128_from_be_bytes(&[0x7F]), Some(127));
272        assert_eq!(i128_from_be_bytes(&[0x00, 0xFF]), Some(255));
273        assert_eq!(i128_from_be_bytes(&[0x04, 0xD2]), Some(1234));
274
275        // Negative values (sign extension)
276        assert_eq!(i128_from_be_bytes(&[0xFF]), Some(-1));
277        assert_eq!(i128_from_be_bytes(&[0x80]), Some(-128));
278        assert_eq!(i128_from_be_bytes(&[0xFB, 0x2E]), Some(-1234));
279
280        // Too large (> 16 bytes)
281        assert_eq!(i128_from_be_bytes(&[0; 17]), None);
282    }
283
284    #[test]
285    fn test_i128_to_be_bytes_min() {
286        // Positive values
287        assert_eq!(i128_to_be_bytes_min(0), vec![0x00]);
288        assert_eq!(i128_to_be_bytes_min(1), vec![0x01]);
289        assert_eq!(i128_to_be_bytes_min(127), vec![0x7F]);
290        assert_eq!(i128_to_be_bytes_min(128), vec![0x00, 0x80]);
291        assert_eq!(i128_to_be_bytes_min(255), vec![0x00, 0xFF]);
292        assert_eq!(i128_to_be_bytes_min(1234), vec![0x04, 0xD2]);
293
294        // Negative values
295        assert_eq!(i128_to_be_bytes_min(-1), vec![0xFF]);
296        assert_eq!(i128_to_be_bytes_min(-128), vec![0x80]);
297        assert_eq!(i128_to_be_bytes_min(-129), vec![0xFF, 0x7F]);
298        assert_eq!(i128_to_be_bytes_min(-1234), vec![0xFB, 0x2E]);
299
300        // Round trip test
301        for val in [
302            0i128,
303            1,
304            -1,
305            127,
306            -128,
307            255,
308            -256,
309            12345,
310            -12345,
311            i128::MAX,
312            i128::MIN,
313        ] {
314            let bytes = i128_to_be_bytes_min(val);
315            assert_eq!(
316                i128_from_be_bytes(&bytes),
317                Some(val),
318                "Round trip failed for {val}"
319            );
320        }
321    }
322}