llkv_types/
decimal.rs

1//! Decimal utilities shared across LLKV crates.
2//!
3//! The runtime stores decimal values using Arrow's `Decimal128` semantics.
4//! This module provides a lightweight helper type for manipulating those
5//! values without pulling in heavier dependencies.
6
7use std::fmt;
8use std::str::FromStr;
9
10use arrow::datatypes::DECIMAL128_MAX_PRECISION;
11use arrow_buffer::i256;
12
13/// Maximum precision supported by `DecimalValue` (aligns with Arrow's Decimal128).
14pub const MAX_DECIMAL_PRECISION: u8 = DECIMAL128_MAX_PRECISION;
15const POW10_BASE: i256 = i256::from_i128(10);
16
17/// Errors that can occur while manipulating decimal values.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum DecimalError {
20    /// Requested scale falls outside the supported range.
21    ScaleOutOfRange { scale: i8 },
22    /// Result exceeded the maximum representable precision.
23    PrecisionOverflow { value: i128, scale: i8 },
24    /// Arithmetic operation overflowed the Decimal128 range.
25    Overflow,
26    /// Attempted to divide by zero.
27    DivisionByZero,
28    /// Rescale operation attempted to lower scale without exact divisibility.
29    InexactRescale { from: i8, to: i8 },
30}
31
32impl fmt::Display for DecimalError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            DecimalError::ScaleOutOfRange { scale } => {
36                write!(f, "decimal scale {scale} outside supported range")
37            }
38            DecimalError::PrecisionOverflow { value, scale } => {
39                write!(
40                    f,
41                    "decimal value {value} with scale {scale} exceeds maximum precision"
42                )
43            }
44            DecimalError::Overflow => write!(f, "decimal arithmetic overflow"),
45            DecimalError::DivisionByZero => write!(f, "decimal division by zero"),
46            DecimalError::InexactRescale { from, to } => {
47                write!(
48                    f,
49                    "cannot rescale decimal from scale {from} to {to} without losing precision"
50                )
51            }
52        }
53    }
54}
55
56impl std::error::Error for DecimalError {}
57
58/// Runtime representation of a Decimal128 value.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
60pub struct DecimalValue {
61    value: i128,
62    scale: i8,
63}
64
65impl DecimalValue {
66    /// Create a decimal from its raw parts, validating precision bounds.
67    pub fn new(value: i128, scale: i8) -> Result<Self, DecimalError> {
68        if !scale_within_bounds(scale as i16) {
69            return Err(DecimalError::ScaleOutOfRange { scale });
70        }
71        let precision = digit_count_i256(i256::from_i128(value));
72        if precision > MAX_DECIMAL_PRECISION {
73            return Err(DecimalError::PrecisionOverflow { value, scale });
74        }
75        Ok(Self { value, scale })
76    }
77
78    /// Construct a decimal from integer value with zero scale.
79    pub fn from_i64(value: i64) -> Self {
80        Self::new(value as i128, 0).expect("i64 fits within Decimal128 limits")
81    }
82
83    /// Return the scaled integer backing this decimal.
84    #[inline]
85    pub fn raw_value(self) -> i128 {
86        self.value
87    }
88
89    /// Return the scale (number of fractional digits).
90    #[inline]
91    pub fn scale(self) -> i8 {
92        self.scale
93    }
94
95    /// Return the decimal precision (total digit count).
96    #[inline]
97    pub fn precision(self) -> u8 {
98        digit_count_i256(i256::from_i128(self.value))
99    }
100
101    /// Convert the decimal into an `f64` (lossy for high precision inputs).
102    pub fn to_f64(self) -> f64 {
103        if self.value == 0 {
104            return 0.0;
105        }
106        let denominator = 10_f64.powi(self.scale as i32);
107        (self.value as f64) / denominator
108    }
109}
110
111impl fmt::Display for DecimalValue {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        if self.scale == 0 {
114            return write!(f, "{}", self.value);
115        }
116        let negative = self.value < 0;
117        let digits = digit_buffer(i256::from_i128(self.value));
118        if digits.len() <= self.scale as usize {
119            let mut result = String::with_capacity(self.scale as usize + 2);
120            if negative {
121                result.push('-');
122            }
123            result.push('0');
124            result.push('.');
125            for _ in digits.len()..self.scale as usize {
126                result.push('0');
127            }
128            result.push_str(&digits);
129            return f.write_str(&result);
130        }
131        let split = digits.len() - self.scale as usize;
132        if negative {
133            f.write_str("-")?;
134        }
135        f.write_str(&digits[..split])?;
136        f.write_str(".")?;
137        f.write_str(&digits[split..])
138    }
139}
140
141impl FromStr for DecimalValue {
142    type Err = DecimalError;
143
144    fn from_str(s: &str) -> Result<Self, Self::Err> {
145        let s = s.trim();
146        let (int_part, frac_part) = match s.split_once('.') {
147            Some((i, f)) => (i, f),
148            None => (s, ""),
149        };
150
151        let scale = frac_part.len();
152        if scale > MAX_DECIMAL_PRECISION as usize {
153            return Err(DecimalError::ScaleOutOfRange { scale: scale as i8 });
154        }
155
156        let combined = format!("{}{}", int_part, frac_part);
157        let value = combined
158            .parse::<i128>()
159            .map_err(|_| DecimalError::Overflow)?;
160
161        Self::new(value, scale as i8)
162    }
163}
164
165impl PartialOrd for DecimalValue {
166    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
167        Some(self.cmp(other))
168    }
169}
170
171impl Ord for DecimalValue {
172    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
173        if self.scale == other.scale {
174            return self.value.cmp(&other.value);
175        }
176
177        let max_scale = std::cmp::max(self.scale, other.scale);
178        let scale_diff_self = (max_scale - self.scale) as u32;
179        let scale_diff_other = (max_scale - other.scale) as u32;
180
181        let l_i256 = i256::from_i128(self.value);
182        let r_i256 = i256::from_i128(other.value);
183
184        // Use wrapping_pow/mul because i256 handles the overflow of i128 range
185        // and we are just comparing.
186        let l_scaled = l_i256.wrapping_mul(POW10_BASE.wrapping_pow(scale_diff_self));
187        let r_scaled = r_i256.wrapping_mul(POW10_BASE.wrapping_pow(scale_diff_other));
188
189        l_scaled.cmp(&r_scaled)
190    }
191}
192
193fn digit_count_i256(mut value: i256) -> u8 {
194    if value == i256::ZERO {
195        return 1;
196    }
197    if value < i256::ZERO {
198        value = value.wrapping_neg();
199    }
200    let mut count: u8 = 0;
201    while value != i256::ZERO {
202        value = value.wrapping_div(POW10_BASE);
203        count += 1;
204    }
205    count
206}
207
208fn digit_buffer(mut value: i256) -> String {
209    if value == i256::ZERO {
210        return "0".to_owned();
211    }
212    if value < i256::ZERO {
213        value = value.wrapping_neg();
214    }
215    let mut buf = Vec::new();
216    let ten = POW10_BASE;
217    let mut current = value;
218    while current != i256::ZERO {
219        let rem = current.wrapping_rem(ten);
220        let digit = rem
221            .to_i128()
222            .expect("remainder from decimal division fits in i128") as i32;
223        buf.push((b'0' + digit as u8) as char);
224        current = current.wrapping_div(ten);
225    }
226    buf.iter().rev().collect()
227}
228
229pub fn scale_within_bounds(scale: i16) -> bool {
230    let max = MAX_DECIMAL_PRECISION as i16;
231    (-max..=max).contains(&scale)
232}