clickhouse_arrow/native/values/
fixed_point.rs

1#![expect(clippy::cast_possible_truncation)]
2#![expect(clippy::cast_precision_loss)]
3#![expect(clippy::cast_sign_loss)]
4#![cfg_attr(feature = "rust_decimal", expect(clippy::cast_possible_wrap))]
5use crate::{FromSql, Result, ToSql, Type, Value, i256, unexpected_type};
6
7/// Wrapper type for `ClickHouse` `FixedPoint32` type.
8#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Debug, Default)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10pub struct FixedPoint32<const SCALE: u64>(pub i32);
11
12impl<const SCALE: u64> FixedPoint32<SCALE> {
13    pub const fn modulus(&self) -> i32 { 10i32.pow(SCALE as u32) }
14
15    pub fn integer(&self) -> i32 { self.0 / 10i32.pow(SCALE as u32) }
16
17    pub fn fraction(&self) -> i32 { self.0 % 10i32.pow(SCALE as u32) }
18}
19
20impl<const SCALE: u64> ToSql for FixedPoint32<SCALE> {
21    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> {
22        Ok(Value::Decimal32(SCALE as usize, self.0))
23    }
24}
25
26impl<const SCALE: u64> FromSql for FixedPoint32<SCALE> {
27    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
28        if !matches!(type_, Type::Decimal32(x) if *x == SCALE as usize) {
29            return Err(unexpected_type(type_));
30        }
31        match value {
32            Value::Decimal32(_, x) => Ok(Self(x)),
33            _ => unimplemented!(),
34        }
35    }
36}
37
38impl<const SCALE: u64> From<FixedPoint32<SCALE>> for f64 {
39    fn from(fp: FixedPoint32<SCALE>) -> Self {
40        f64::from(fp.integer()) + (f64::from(fp.fraction()) / f64::from(fp.modulus()))
41    }
42}
43
44/// Wrapper type for `ClickHouse` `FixedPoint64` type.
45#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Debug, Default)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub struct FixedPoint64<const SCALE: u64>(pub i64);
48
49impl<const SCALE: u64> ToSql for FixedPoint64<SCALE> {
50    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> {
51        Ok(Value::Decimal64(SCALE as usize, self.0))
52    }
53}
54
55impl<const SCALE: u64> FromSql for FixedPoint64<SCALE> {
56    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
57        if !matches!(type_, Type::Decimal64(x) if *x == SCALE as usize) {
58            return Err(unexpected_type(type_));
59        }
60        match value {
61            Value::Decimal64(_, x) => Ok(Self(x)),
62            _ => unimplemented!(),
63        }
64    }
65}
66
67impl<const SCALE: u64> FixedPoint64<SCALE> {
68    pub const fn modulus(&self) -> i64 { 10i64.pow(SCALE as u32) }
69
70    pub fn integer(&self) -> i64 { self.0 / 10i64.pow(SCALE as u32) }
71
72    pub fn fraction(&self) -> i64 { self.0 % 10i64.pow(SCALE as u32) }
73}
74
75impl<const SCALE: u64> From<FixedPoint64<SCALE>> for f64 {
76    fn from(fp: FixedPoint64<SCALE>) -> Self {
77        fp.integer() as f64 + (fp.fraction() as f64 / fp.modulus() as f64)
78    }
79}
80
81/// Wrapper type for `ClickHouse` `FixedPoint128` type.
82#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Debug, Default)]
83#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
84pub struct FixedPoint128<const SCALE: u64>(pub i128);
85
86impl<const SCALE: u64> ToSql for FixedPoint128<SCALE> {
87    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> {
88        Ok(Value::Decimal128(SCALE as usize, self.0))
89    }
90}
91
92impl<const SCALE: u64> FromSql for FixedPoint128<SCALE> {
93    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
94        if !matches!(type_, Type::Decimal128(x) if *x == SCALE as usize) {
95            return Err(unexpected_type(type_));
96        }
97        match value {
98            Value::Decimal128(_, x) => Ok(Self(x)),
99            _ => unimplemented!(),
100        }
101    }
102}
103
104impl<const SCALE: u64> FixedPoint128<SCALE> {
105    pub const fn modulus(&self) -> i128 { 10i128.pow(SCALE as u32) }
106
107    pub fn integer(&self) -> i128 { self.0 / 10i128.pow(SCALE as u32) }
108
109    pub fn fraction(&self) -> i128 { self.0 % 10i128.pow(SCALE as u32) }
110}
111
112impl<const SCALE: u64> From<FixedPoint128<SCALE>> for f64 {
113    fn from(fp: FixedPoint128<SCALE>) -> Self {
114        fp.integer() as f64 + (fp.fraction() as f64 / fp.modulus() as f64)
115    }
116}
117
118/// Wrapper type for `ClickHouse` `FixedPoint256` type.
119#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Debug, Default)]
120#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
121pub struct FixedPoint256<const SCALE: u64>(pub i256);
122
123impl<const SCALE: u64> FixedPoint256<SCALE> {
124    /// The maximum value for [`FixedPoint256`]
125    pub const MAX: Self = FixedPoint256(Self::max_i256());
126    /// The minimum value for [`FixedPoint256`]
127    pub const MIN: Self = FixedPoint256(Self::min_i256());
128
129    const fn max_i256() -> i256 {
130        // For a two's complement signed integer, the maximum value has
131        // all bits set to 1 except the sign bit
132        let mut bytes = [0xFF; 32];
133        bytes[0] = 0x7F; // Clear the sign bit of the highest byte
134        i256(bytes)
135    }
136
137    const fn min_i256() -> i256 {
138        // For a two's complement signed integer, the minimum value has
139        // just the sign bit set to 1, all others to 0
140        let mut bytes = [0; 32];
141        bytes[0] = 0x80; // Set only the sign bit
142        i256(bytes)
143    }
144
145    /// Create a fixed-point number from a raw scaled integer value
146    /// The value is assumed to already have the correct scaling applied
147    pub fn from_raw(value: i128) -> Self { FixedPoint256(i256::from(value)) }
148
149    /// Create a fixed-point number from a value and decimal exponent
150    /// e.g., (123, -2) represents 123 × 10^-2 = 1.23
151    pub fn from_parts(value: i128, exponent: i32) -> Self {
152        let effective_scale = SCALE as i32 - exponent;
153
154        if effective_scale > 38 {
155            // Would overflow i128 - handle by converting to i256 first, then scaling
156            let base = i256::from(value);
157            let mut result = base;
158
159            // Scale by multiplying by 10, effective_scale times
160            for _ in 0..effective_scale {
161                result = result * i256::from(10i128);
162            }
163
164            FixedPoint256(result)
165        } else if effective_scale >= 0 {
166            // Need to multiply by 10^effective_scale
167            let scaled_value = value * 10i128.pow(effective_scale as u32);
168            FixedPoint256(i256::from(scaled_value))
169        } else {
170            // Need to divide by 10^(-effective_scale)
171            let scaled_value = value / 10i128.pow((-effective_scale) as u32);
172            FixedPoint256(i256::from(scaled_value))
173        }
174    }
175
176    /// Create a fixed-point number from a decimal
177    #[cfg(feature = "rust_decimal")]
178    pub fn from_decimal(decimal: rust_decimal::Decimal) -> Self {
179        let scale = decimal.scale();
180        let mantissa = decimal.mantissa();
181
182        Self::from_parts(mantissa, scale as i32)
183    }
184
185    /// Convert to a Decimal
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the value is too large to fit in a Decimal
190    #[cfg(feature = "rust_decimal")]
191    pub fn to_decimal(&self) -> Result<rust_decimal::Decimal, rust_decimal::Error> {
192        // Extract raw value from i256
193        let (high, low) = self.0.into();
194
195        if high != 0 && high != u128::MAX {
196            // Value too large for Decimal
197            return Err(rust_decimal::Error::ExceedsMaximumPossibleValue);
198        }
199
200        let raw_value = if self.is_negative() {
201            // For negative values, we need to convert from two's complement
202            let mut high_bits = !high;
203            let low_bits = !low;
204
205            let low_plus_one = low_bits.wrapping_add(1);
206            if low_plus_one == 0 {
207                high_bits = high_bits.wrapping_add(1);
208            }
209
210            if high_bits != 0 {
211                // Value too large for Decimal
212                return Err(rust_decimal::Error::ExceedsMaximumPossibleValue);
213            }
214
215            -(low_plus_one as i128)
216        } else {
217            // For positive values, just use the low bits if high is 0
218            low as i128
219        };
220
221        // Create a decimal with the raw value and precision
222        rust_decimal::Decimal::try_from_i128_with_scale(raw_value, SCALE as u32)
223    }
224
225    /// Check if the value is negative
226    pub fn is_negative(&self) -> bool {
227        let (high, _) = self.0.into();
228        (high & (1u128 << 127)) != 0
229    }
230}
231
232impl<const SCALE: u64> ToSql for FixedPoint256<SCALE> {
233    fn to_sql(self, _type_hint: Option<&Type>) -> Result<Value> {
234        Ok(Value::Decimal256(SCALE as usize, self.0))
235    }
236}
237
238impl<const SCALE: u64> FromSql for FixedPoint256<SCALE> {
239    fn from_sql(type_: &Type, value: Value) -> Result<Self> {
240        if !matches!(type_, Type::Decimal256(x) if *x == SCALE as usize) {
241            return Err(unexpected_type(type_));
242        }
243        match value {
244            Value::Decimal256(_, x) => Ok(Self(x)),
245            _ => unimplemented!(),
246        }
247    }
248}
249
250// Implement standard From trait for user-friendly conversions
251impl<const SCALE: u64> From<i128> for FixedPoint256<SCALE> {
252    fn from(value: i128) -> Self {
253        // Interpret value as already having SCALE decimal places
254        Self::from_raw(value)
255    }
256}
257
258impl<const SCALE: u64> From<(i128, i32)> for FixedPoint256<SCALE> {
259    fn from(parts: (i128, i32)) -> Self { Self::from_parts(parts.0, parts.1) }
260}
261
262#[cfg(feature = "rust_decimal")]
263impl<const SCALE: u64> From<rust_decimal::Decimal> for FixedPoint256<SCALE> {
264    fn from(decimal: rust_decimal::Decimal) -> Self { Self::from_decimal(decimal) }
265}