Skip to main content

decimal_bytes/
decimal64_no_scale.rs

1//! Fixed-precision 64-bit decimal type with external scale.
2//!
3//! `Decimal64NoScale` provides maximum precision by using all 64 bits for the
4//! mantissa, with scale stored externally (e.g., in schema metadata).
5//!
6//! ## When to Use
7//!
8//! - **Decimal64NoScale**: For columnar storage where scale is in metadata
9//! - **Decimal64**: For self-contained decimals where scale must be embedded
10//! - **Decimal**: For arbitrary precision or when precision exceeds 18 digits
11//!
12//! ## Comparison with Decimal64
13//!
14//! |       Type         |   Scale Storage   | Mantissa Bits | Max Digits |           Aggregates               |
15//! |--------------------|-------------------|---------------|------------|------------------------------------|
16//! | `Decimal64`        | Embedded (8 bits) | 56 bits       | 16         | **Wrong** (scale bits corrupt sum) |
17//! | `Decimal64NoScale` | External          | 64 bits       | **18**     | **Correct** (raw integer math)     |
18//!
19//! ## Storage Layout
20//!
21//! ```text
22//! 64-bit representation:
23//! ┌─────────────────────────────────────────────────────────────────┐
24//! │ Value (64 bits, signed) - represents value * 10^scale           │
25//! └─────────────────────────────────────────────────────────────────┘
26//! ```
27//!
28//! Special values use sentinel i64 values (PostgreSQL sort order):
29//! - `i64::MIN`: -Infinity (sorts lowest)
30//! - `i64::MAX - 1`: +Infinity
31//! - `i64::MAX`: NaN (sorts highest, per PostgreSQL semantics)
32//!
33//! ## Example
34//!
35//! ```
36//! use decimal_bytes::Decimal64NoScale;
37//!
38//! // Create with external scale
39//! let scale = 2;
40//! let price = Decimal64NoScale::new("123.45", scale).unwrap();
41//! assert_eq!(price.value(), 12345);  // Raw scaled value
42//! assert_eq!(price.to_string_with_scale(scale), "123.45");
43//!
44//! // Aggregates work correctly!
45//! let a = Decimal64NoScale::new("100.50", scale).unwrap();
46//! let b = Decimal64NoScale::new("200.25", scale).unwrap();
47//! let sum = a.value() + b.value();  // 30075
48//! // Interpret: 30075 / 10^2 = 300.75 ✓
49//! ```
50
51use std::cmp::Ordering;
52use std::fmt;
53use std::hash::Hash;
54use std::str::FromStr;
55
56use serde::{Deserialize, Deserializer, Serialize, Serializer};
57
58use crate::encoding::DecimalError;
59use crate::Decimal;
60
61/// Maximum precision that fits in signed i64 (18 digits).
62/// i64::MAX = 9,223,372,036,854,775,807 ≈ 9.2 × 10^18
63pub const MAX_DECIMAL64_NO_SCALE_PRECISION: u32 = 18;
64
65/// Maximum scale supported.
66pub const MAX_DECIMAL64_NO_SCALE_SCALE: i32 = 18;
67
68// Sentinel values for special cases (PostgreSQL sort order: -Inf < numbers < +Inf < NaN)
69const SENTINEL_NEG_INFINITY: i64 = i64::MIN;
70const SENTINEL_POS_INFINITY: i64 = i64::MAX - 1;
71const SENTINEL_NAN: i64 = i64::MAX;
72
73// 18-digit precision limits (matching MAX_DECIMAL64_NO_SCALE_PRECISION)
74const MIN_VALUE: i64 = -999_999_999_999_999_999i64;
75const MAX_VALUE: i64 = 999_999_999_999_999_999i64;
76
77/// A fixed-precision decimal stored as a raw 64-bit integer.
78///
79/// ## Design
80///
81/// Unlike `Decimal64` which embeds scale, `Decimal64NoScale` stores only the
82/// mantissa. This provides:
83/// - **2 more digits of precision** (18 vs 16)
84/// - **Correct aggregates** (sum/min/max work with raw integer math)
85/// - **Columnar storage compatibility** (scale in metadata, not per-value)
86///
87/// ## Aggregates
88///
89/// ```text
90/// stored_values = [a*10^s, b*10^s, c*10^s]
91/// SUM(stored_values) = (a+b+c) * 10^s
92/// actual_sum = SUM / 10^s = a+b+c  ✓
93/// ```
94///
95/// ## Special Values
96///
97/// Special values use sentinel i64 values that sort correctly in standard i64 ordering:
98/// - `i64::MIN`: -Infinity (sorts lowest)
99/// - `i64::MAX - 1`: +Infinity
100/// - `i64::MAX`: NaN (sorts highest, per PostgreSQL semantics)
101#[derive(Clone, Copy, Default, Eq, PartialEq, Hash)]
102pub struct Decimal64NoScale {
103    /// Raw value: actual_value * 10^scale (scale stored externally)
104    value: i64,
105}
106
107impl Decimal64NoScale {
108    // ==================== Constructors ====================
109
110    /// Creates a Decimal64NoScale from a string with the given scale.
111    ///
112    /// # Arguments
113    /// * `s` - The decimal string (e.g., "123.45")
114    /// * `scale` - The number of decimal places to store
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use decimal_bytes::Decimal64NoScale;
120    ///
121    /// let d = Decimal64NoScale::new("123.45", 2).unwrap();
122    /// assert_eq!(d.value(), 12345);
123    /// assert_eq!(d.to_string_with_scale(2), "123.45");
124    /// ```
125    pub fn new(s: &str, scale: i32) -> Result<Self, DecimalError> {
126        let s = s.trim();
127
128        // Handle special values (case-insensitive)
129        let lower = s.to_lowercase();
130        match lower.as_str() {
131            "nan" | "-nan" | "+nan" => return Ok(Self::nan()),
132            "infinity" | "inf" | "+infinity" | "+inf" => return Ok(Self::infinity()),
133            "-infinity" | "-inf" => return Ok(Self::neg_infinity()),
134            _ => {}
135        }
136
137        // Validate scale
138        if scale.abs() > MAX_DECIMAL64_NO_SCALE_SCALE {
139            return Err(DecimalError::InvalidFormat(format!(
140                "Scale {} exceeds maximum {} for Decimal64NoScale",
141                scale.abs(),
142                MAX_DECIMAL64_NO_SCALE_SCALE
143            )));
144        }
145
146        // Parse sign
147        let (is_negative, s) = if let Some(rest) = s.strip_prefix('-') {
148            (true, rest)
149        } else if let Some(rest) = s.strip_prefix('+') {
150            (false, rest)
151        } else {
152            (false, s)
153        };
154
155        // Split into integer and fractional parts
156        let (int_part, frac_part) = if let Some(dot_pos) = s.find('.') {
157            (&s[..dot_pos], &s[dot_pos + 1..])
158        } else {
159            (s, "")
160        };
161
162        // Trim leading zeros from integer part
163        let int_part = int_part.trim_start_matches('0');
164        let int_part = if int_part.is_empty() { "0" } else { int_part };
165
166        // Convert to scaled integer
167        let value = Self::compute_scaled_value(int_part, frac_part, is_negative, scale)?;
168
169        if !(MIN_VALUE..=MAX_VALUE).contains(&value) {
170            return Err(DecimalError::InvalidFormat(
171                "Value too large for Decimal64NoScale".to_string(),
172            ));
173        }
174
175        Ok(Self { value })
176    }
177
178    /// Creates a Decimal64NoScale from a raw i64 value.
179    ///
180    /// Use this when you already have the scaled integer value.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use decimal_bytes::Decimal64NoScale;
186    ///
187    /// let d = Decimal64NoScale::from_raw(12345);
188    /// assert_eq!(d.value(), 12345);
189    /// assert_eq!(d.to_string_with_scale(2), "123.45");
190    /// ```
191    #[inline]
192    pub const fn from_raw(value: i64) -> Self {
193        Self { value }
194    }
195
196    /// Creates a Decimal64NoScale from an i64 with the given scale.
197    ///
198    /// Multiplies the value by 10^scale. Returns an error if the result overflows.
199    ///
200    /// # Examples
201    ///
202    /// ```
203    /// use decimal_bytes::Decimal64NoScale;
204    ///
205    /// let d = Decimal64NoScale::from_i64(123, 2).unwrap();
206    /// assert_eq!(d.value(), 12300);  // 123 * 10^2
207    /// assert_eq!(d.to_string_with_scale(2), "123");
208    /// ```
209    pub fn from_i64(value: i64, scale: i32) -> Result<Self, DecimalError> {
210        if scale < 0 {
211            // Negative scale: divide (rounds toward zero)
212            let divisor = 10i64.pow((-scale) as u32);
213            return Ok(Self {
214                value: value / divisor,
215            });
216        }
217
218        if scale == 0 {
219            return Ok(Self { value });
220        }
221
222        let scale_factor = 10i64.pow(scale as u32);
223        let scaled = value.checked_mul(scale_factor).ok_or_else(|| {
224            DecimalError::InvalidFormat(format!(
225                "Overflow: {} * 10^{} exceeds i64 range",
226                value, scale
227            ))
228        })?;
229
230        if !(MIN_VALUE..=MAX_VALUE).contains(&scaled) {
231            return Err(DecimalError::InvalidFormat(
232                "Value too large for Decimal64NoScale".to_string(),
233            ));
234        }
235
236        Ok(Self { value: scaled })
237    }
238
239    /// Creates a Decimal64NoScale from a u64 with the given scale.
240    ///
241    /// Multiplies the value by 10^scale. Returns an error if the result overflows.
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use decimal_bytes::Decimal64NoScale;
247    ///
248    /// let d = Decimal64NoScale::from_u64(123, 2).unwrap();
249    /// assert_eq!(d.value(), 12300);  // 123 * 10^2
250    /// ```
251    pub fn from_u64(value: u64, scale: i32) -> Result<Self, DecimalError> {
252        if value > i64::MAX as u64 {
253            return Err(DecimalError::InvalidFormat(format!(
254                "Value {} exceeds i64::MAX",
255                value
256            )));
257        }
258        Self::from_i64(value as i64, scale)
259    }
260
261    /// Creates a Decimal64NoScale from an f64 with the given scale.
262    ///
263    /// Converts via string to avoid floating-point precision loss during scaling.
264    /// Returns an error for NaN/Infinity or if the result overflows.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use decimal_bytes::Decimal64NoScale;
270    ///
271    /// let d = Decimal64NoScale::from_f64(123.45, 2).unwrap();
272    /// assert_eq!(d.value(), 12345);
273    /// assert_eq!(d.to_string_with_scale(2), "123.45");
274    /// ```
275    pub fn from_f64(value: f64, scale: i32) -> Result<Self, DecimalError> {
276        if value.is_nan() {
277            return Ok(Self::nan());
278        }
279        if value.is_infinite() {
280            return Ok(if value.is_sign_positive() {
281                Self::infinity()
282            } else {
283                Self::neg_infinity()
284            });
285        }
286        // Use string conversion to avoid precision loss
287        Self::new(&value.to_string(), scale)
288    }
289
290    // ==================== Special Value Constructors ====================
291
292    /// Creates positive infinity.
293    #[inline]
294    pub const fn infinity() -> Self {
295        Self {
296            value: SENTINEL_POS_INFINITY,
297        }
298    }
299
300    /// Creates negative infinity.
301    #[inline]
302    pub const fn neg_infinity() -> Self {
303        Self {
304            value: SENTINEL_NEG_INFINITY,
305        }
306    }
307
308    /// Creates NaN (Not a Number).
309    ///
310    /// Follows PostgreSQL semantics: `NaN == NaN` is `true`.
311    #[inline]
312    pub const fn nan() -> Self {
313        Self {
314            value: SENTINEL_NAN,
315        }
316    }
317
318    // ==================== Accessors ====================
319
320    /// Returns the raw i64 value.
321    ///
322    /// For normal values, this is `actual_value * 10^scale`.
323    /// For special values, this returns the sentinel value.
324    #[inline]
325    pub const fn value(&self) -> i64 {
326        self.value
327    }
328
329    /// Returns the raw i64 value (alias for columnar storage compatibility).
330    #[inline]
331    pub const fn raw(&self) -> i64 {
332        self.value
333    }
334
335    /// Returns true if this value is zero.
336    #[inline]
337    pub fn is_zero(&self) -> bool {
338        !self.is_special() && self.value == 0
339    }
340
341    /// Returns true if this value is negative (excluding special values).
342    #[inline]
343    pub fn is_negative(&self) -> bool {
344        !self.is_special() && self.value < 0
345    }
346
347    /// Returns true if this value is positive (excluding special values).
348    #[inline]
349    pub fn is_positive(&self) -> bool {
350        !self.is_special() && self.value > 0
351    }
352
353    /// Returns true if this value is positive infinity.
354    #[inline]
355    pub fn is_pos_infinity(&self) -> bool {
356        self.value == SENTINEL_POS_INFINITY
357    }
358
359    /// Returns true if this value is negative infinity.
360    #[inline]
361    pub fn is_neg_infinity(&self) -> bool {
362        self.value == SENTINEL_NEG_INFINITY
363    }
364
365    /// Returns true if this value is positive or negative infinity.
366    #[inline]
367    pub fn is_infinity(&self) -> bool {
368        self.is_pos_infinity() || self.is_neg_infinity()
369    }
370
371    /// Returns true if this value is NaN (Not a Number).
372    #[inline]
373    pub fn is_nan(&self) -> bool {
374        self.value == SENTINEL_NAN
375    }
376
377    /// Returns true if this is a special value (Infinity or NaN).
378    #[inline]
379    pub fn is_special(&self) -> bool {
380        self.value == SENTINEL_NAN
381            || self.value == SENTINEL_NEG_INFINITY
382            || self.value == SENTINEL_POS_INFINITY
383    }
384
385    /// Returns true if this is a finite number (not Infinity or NaN).
386    #[inline]
387    pub fn is_finite(&self) -> bool {
388        !self.is_special()
389    }
390
391    // ==================== Conversions ====================
392
393    /// Formats the value as a decimal string using the given scale.
394    ///
395    /// # Arguments
396    /// * `scale` - The scale to use for formatting
397    ///
398    /// # Examples
399    ///
400    /// ```
401    /// use decimal_bytes::Decimal64NoScale;
402    ///
403    /// let d = Decimal64NoScale::from_raw(12345);
404    /// assert_eq!(d.to_string_with_scale(2), "123.45");
405    /// assert_eq!(d.to_string_with_scale(3), "12.345");
406    /// assert_eq!(d.to_string_with_scale(0), "12345");
407    /// ```
408    pub fn to_string_with_scale(&self, scale: i32) -> String {
409        // Handle special values
410        if self.is_neg_infinity() {
411            return "-Infinity".to_string();
412        }
413        if self.is_pos_infinity() {
414            return "Infinity".to_string();
415        }
416        if self.is_nan() {
417            return "NaN".to_string();
418        }
419
420        let value = self.value;
421
422        if value == 0 {
423            return "0".to_string();
424        }
425
426        let is_negative = value < 0;
427        let abs_value = value.unsigned_abs();
428
429        if scale <= 0 {
430            // Negative or zero scale: multiply
431            let result = if scale < 0 {
432                abs_value * 10u64.pow((-scale) as u32)
433            } else {
434                abs_value
435            };
436            return if is_negative {
437                format!("-{}", result)
438            } else {
439                result.to_string()
440            };
441        }
442
443        let scale_factor = 10u64.pow(scale as u32);
444        let int_part = abs_value / scale_factor;
445        let frac_part = abs_value % scale_factor;
446
447        let result = if frac_part == 0 {
448            int_part.to_string()
449        } else {
450            let frac_str = format!("{:0>width$}", frac_part, width = scale as usize);
451            let frac_str = frac_str.trim_end_matches('0');
452            format!("{}.{}", int_part, frac_str)
453        };
454
455        if is_negative {
456            format!("-{}", result)
457        } else {
458            result
459        }
460    }
461
462    /// Returns the 8-byte big-endian representation.
463    #[inline]
464    pub fn to_be_bytes(&self) -> [u8; 8] {
465        self.value.to_be_bytes()
466    }
467
468    /// Creates a Decimal64NoScale from big-endian bytes.
469    #[inline]
470    pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
471        Self {
472            value: i64::from_be_bytes(bytes),
473        }
474    }
475
476    /// Converts to the variable-length `Decimal` type.
477    ///
478    /// Note: This requires a scale to format correctly.
479    pub fn to_decimal(&self, scale: i32) -> Decimal {
480        if self.is_neg_infinity() {
481            return Decimal::neg_infinity();
482        }
483        if self.is_pos_infinity() {
484            return Decimal::infinity();
485        }
486        if self.is_nan() {
487            return Decimal::nan();
488        }
489
490        Decimal::from_str(&self.to_string_with_scale(scale))
491            .expect("Decimal64NoScale string is always valid")
492    }
493
494    /// Creates a Decimal64NoScale from a Decimal with the specified scale.
495    pub fn from_decimal(decimal: &Decimal, scale: i32) -> Result<Self, DecimalError> {
496        if decimal.is_nan() {
497            return Ok(Self::nan());
498        }
499        if decimal.is_pos_infinity() {
500            return Ok(Self::infinity());
501        }
502        if decimal.is_neg_infinity() {
503            return Ok(Self::neg_infinity());
504        }
505
506        Self::new(&decimal.to_string(), scale)
507    }
508
509    /// Returns the minimum finite value.
510    #[inline]
511    pub const fn min_value() -> Self {
512        Self { value: MIN_VALUE }
513    }
514
515    /// Returns the maximum finite value.
516    #[inline]
517    pub const fn max_value() -> Self {
518        Self { value: MAX_VALUE }
519    }
520
521    // ==================== Comparison with Scale ====================
522
523    /// Compares two values, normalizing scales if different.
524    ///
525    /// This is needed when comparing values that might have been stored
526    /// with different scales in different columns.
527    pub fn cmp_with_scale(&self, other: &Self, self_scale: i32, other_scale: i32) -> Ordering {
528        // Handle special values
529        match (self.is_special(), other.is_special()) {
530            (true, true) => {
531                // Both special: NaN > Infinity > -Infinity
532                if self.is_nan() && other.is_nan() {
533                    return Ordering::Equal;
534                }
535                if self.is_nan() {
536                    return Ordering::Greater;
537                }
538                if other.is_nan() {
539                    return Ordering::Less;
540                }
541                if self.is_pos_infinity() && other.is_pos_infinity() {
542                    return Ordering::Equal;
543                }
544                if self.is_neg_infinity() && other.is_neg_infinity() {
545                    return Ordering::Equal;
546                }
547                if self.is_pos_infinity() {
548                    return Ordering::Greater;
549                }
550                if self.is_neg_infinity() {
551                    return Ordering::Less;
552                }
553                if other.is_pos_infinity() {
554                    return Ordering::Less;
555                }
556                Ordering::Greater // other is -Infinity
557            }
558            (true, false) => {
559                if self.is_neg_infinity() {
560                    Ordering::Less
561                } else {
562                    Ordering::Greater
563                }
564            }
565            (false, true) => {
566                if other.is_neg_infinity() {
567                    Ordering::Greater
568                } else {
569                    Ordering::Less
570                }
571            }
572            (false, false) => {
573                // Both normal: normalize to common scale
574                if self_scale == other_scale {
575                    self.value.cmp(&other.value)
576                } else {
577                    let max_scale = self_scale.max(other_scale);
578
579                    let self_normalized = if self_scale < max_scale {
580                        self.value
581                            .saturating_mul(10i64.pow((max_scale - self_scale) as u32))
582                    } else {
583                        self.value
584                    };
585
586                    let other_normalized = if other_scale < max_scale {
587                        other
588                            .value
589                            .saturating_mul(10i64.pow((max_scale - other_scale) as u32))
590                    } else {
591                        other.value
592                    };
593
594                    self_normalized.cmp(&other_normalized)
595                }
596            }
597        }
598    }
599
600    // ==================== Internal Helpers ====================
601
602    fn compute_scaled_value(
603        int_part: &str,
604        frac_part: &str,
605        is_negative: bool,
606        scale: i32,
607    ) -> Result<i64, DecimalError> {
608        if scale < 0 {
609            // Negative scale: round to powers of 10, store the quotient
610            // stored = actual_value / 10^(-scale)
611            // e.g., 12345 with scale=-2 → store 123 (rounds to 12300, then /100)
612            let round_digits = (-scale) as usize;
613            let int_value: i64 = int_part.parse().unwrap_or(0);
614
615            if int_part.len() <= round_digits {
616                return Ok(0);
617            }
618
619            let divisor = 10i64.pow(round_digits as u32);
620            // Round and divide (don't multiply back)
621            let rounded = (int_value + divisor / 2) / divisor;
622            return Ok(if is_negative { -rounded } else { rounded });
623        }
624
625        let scale_u = scale as usize;
626
627        // Parse integer part
628        let int_value: i64 = if int_part == "0" || int_part.is_empty() {
629            0
630        } else {
631            int_part.parse().map_err(|_| {
632                DecimalError::InvalidFormat(format!("Invalid integer part: {}", int_part))
633            })?
634        };
635
636        // Apply scale to get scaled integer part
637        let scale_factor = 10i64.pow(scale as u32);
638        let scaled_int = int_value.checked_mul(scale_factor).ok_or_else(|| {
639            DecimalError::InvalidFormat("Value too large for Decimal64NoScale".to_string())
640        })?;
641
642        // Parse fractional part (truncate or pad to scale)
643        let frac_value: i64 = if frac_part.is_empty() {
644            0
645        } else if frac_part.len() <= scale_u {
646            // Pad with zeros
647            let padded = format!("{:0<width$}", frac_part, width = scale_u);
648            padded.parse().unwrap_or(0)
649        } else {
650            // Truncate (with rounding)
651            let truncated = &frac_part[..scale_u];
652            let next_digit = frac_part.chars().nth(scale_u).unwrap_or('0');
653            let mut value: i64 = truncated.parse().unwrap_or(0);
654            if next_digit >= '5' {
655                value += 1;
656            }
657            value
658        };
659
660        let result = scaled_int + frac_value;
661        Ok(if is_negative && result != 0 {
662            -result
663        } else {
664            result
665        })
666    }
667}
668
669// ==================== Trait Implementations ====================
670
671impl PartialOrd for Decimal64NoScale {
672    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
673        Some(self.cmp(other))
674    }
675}
676
677impl Ord for Decimal64NoScale {
678    /// Compares values assuming same scale.
679    ///
680    /// For cross-scale comparison, use `cmp_with_scale()`.
681    ///
682    /// The sentinel values are chosen so that standard i64 comparison gives
683    /// PostgreSQL ordering: -Infinity < numbers < +Infinity < NaN
684    fn cmp(&self, other: &Self) -> Ordering {
685        self.value.cmp(&other.value)
686    }
687}
688
689impl fmt::Debug for Decimal64NoScale {
690    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
691        if self.is_nan() {
692            write!(f, "Decimal64NoScale(NaN)")
693        } else if self.is_pos_infinity() {
694            write!(f, "Decimal64NoScale(Infinity)")
695        } else if self.is_neg_infinity() {
696            write!(f, "Decimal64NoScale(-Infinity)")
697        } else {
698            f.debug_struct("Decimal64NoScale")
699                .field("value", &self.value)
700                .finish()
701        }
702    }
703}
704
705impl fmt::Display for Decimal64NoScale {
706    /// Display without scale (shows raw value).
707    ///
708    /// For formatted output, use `to_string_with_scale()`.
709    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710        if self.is_nan() {
711            write!(f, "NaN")
712        } else if self.is_pos_infinity() {
713            write!(f, "Infinity")
714        } else if self.is_neg_infinity() {
715            write!(f, "-Infinity")
716        } else {
717            write!(f, "{}", self.value)
718        }
719    }
720}
721
722impl From<i64> for Decimal64NoScale {
723    fn from(value: i64) -> Self {
724        Self { value }
725    }
726}
727
728impl From<i32> for Decimal64NoScale {
729    fn from(value: i32) -> Self {
730        Self {
731            value: value as i64,
732        }
733    }
734}
735
736// Serde support
737impl Serialize for Decimal64NoScale {
738    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
739    where
740        S: Serializer,
741    {
742        serializer.serialize_i64(self.value)
743    }
744}
745
746impl<'de> Deserialize<'de> for Decimal64NoScale {
747    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
748    where
749        D: Deserializer<'de>,
750    {
751        let value = i64::deserialize(deserializer)?;
752        Ok(Self::from_raw(value))
753    }
754}
755
756#[cfg(test)]
757mod tests {
758    use super::*;
759
760    #[test]
761    fn test_new_basic() {
762        let d = Decimal64NoScale::new("123.45", 2).unwrap();
763        assert_eq!(d.value(), 12345);
764        assert_eq!(d.to_string_with_scale(2), "123.45");
765
766        let d = Decimal64NoScale::new("100", 0).unwrap();
767        assert_eq!(d.value(), 100);
768        assert_eq!(d.to_string_with_scale(0), "100");
769
770        let d = Decimal64NoScale::new("-50.5", 1).unwrap();
771        assert_eq!(d.value(), -505);
772        assert_eq!(d.to_string_with_scale(1), "-50.5");
773    }
774
775    #[test]
776    fn test_18_digit_precision() {
777        // This should work with Decimal64NoScale but NOT with Decimal64
778        let d = Decimal64NoScale::new("123456789012345678", 0).unwrap();
779        assert_eq!(d.value(), 123456789012345678);
780        assert_eq!(d.to_string_with_scale(0), "123456789012345678");
781
782        // 18 digits with 2 decimal places
783        let d = Decimal64NoScale::new("1234567890123456.78", 2).unwrap();
784        assert_eq!(d.value(), 123456789012345678);
785        assert_eq!(d.to_string_with_scale(2), "1234567890123456.78");
786    }
787
788    #[test]
789    fn test_aggregates_work() {
790        // This is the key test: aggregates on raw i64 values should work
791        let scale = 2;
792        let a = Decimal64NoScale::new("100.50", scale).unwrap();
793        let b = Decimal64NoScale::new("200.25", scale).unwrap();
794        let c = Decimal64NoScale::new("300.75", scale).unwrap();
795
796        // Sum the raw values
797        let sum = a.value() + b.value() + c.value();
798        assert_eq!(sum, 60150); // 601.50 * 100
799
800        // Interpret with scale
801        let result = Decimal64NoScale::from_raw(sum);
802        assert_eq!(result.to_string_with_scale(scale), "601.5");
803
804        // Min/Max
805        let values = [a.value(), b.value(), c.value()];
806        let min = *values.iter().min().unwrap();
807        let max = *values.iter().max().unwrap();
808        assert_eq!(
809            Decimal64NoScale::from_raw(min).to_string_with_scale(scale),
810            "100.5"
811        );
812        assert_eq!(
813            Decimal64NoScale::from_raw(max).to_string_with_scale(scale),
814            "300.75"
815        );
816    }
817
818    #[test]
819    fn test_special_values() {
820        let nan = Decimal64NoScale::nan();
821        assert!(nan.is_nan());
822        assert!(nan.is_special());
823        assert_eq!(nan.to_string_with_scale(2), "NaN");
824
825        let inf = Decimal64NoScale::infinity();
826        assert!(inf.is_pos_infinity());
827        assert_eq!(inf.to_string_with_scale(2), "Infinity");
828
829        let neg_inf = Decimal64NoScale::neg_infinity();
830        assert!(neg_inf.is_neg_infinity());
831        assert_eq!(neg_inf.to_string_with_scale(2), "-Infinity");
832    }
833
834    #[test]
835    fn test_ordering() {
836        let neg_inf = Decimal64NoScale::neg_infinity();
837        let neg = Decimal64NoScale::from_raw(-1000);
838        let zero = Decimal64NoScale::from_raw(0);
839        let pos = Decimal64NoScale::from_raw(1000);
840        let inf = Decimal64NoScale::infinity();
841        let nan = Decimal64NoScale::nan();
842
843        assert!(neg_inf < neg);
844        assert!(neg < zero);
845        assert!(zero < pos);
846        assert!(pos < inf);
847        assert!(inf < nan);
848    }
849
850    #[test]
851    fn test_from_str_special() {
852        assert!(Decimal64NoScale::new("Infinity", 0)
853            .unwrap()
854            .is_pos_infinity());
855        assert!(Decimal64NoScale::new("-Infinity", 0)
856            .unwrap()
857            .is_neg_infinity());
858        assert!(Decimal64NoScale::new("NaN", 0).unwrap().is_nan());
859    }
860
861    #[test]
862    fn test_roundtrip() {
863        let scale = 4;
864        let values = ["0", "123.4567", "-99.9999", "1000000", "-1"];
865
866        for s in values {
867            let d = Decimal64NoScale::new(s, scale).unwrap();
868            let raw = d.value();
869            let restored = Decimal64NoScale::from_raw(raw);
870            assert_eq!(d.value(), restored.value(), "Roundtrip failed for {}", s);
871        }
872    }
873
874    #[test]
875    fn test_byte_roundtrip() {
876        let d = Decimal64NoScale::new("123.45", 2).unwrap();
877        let bytes = d.to_be_bytes();
878        let restored = Decimal64NoScale::from_be_bytes(bytes);
879        assert_eq!(d, restored);
880    }
881
882    #[test]
883    fn test_zero() {
884        let d = Decimal64NoScale::new("0", 0).unwrap();
885        assert!(d.is_zero());
886        assert!(!d.is_negative());
887        assert!(!d.is_positive());
888        assert!(d.is_finite());
889    }
890
891    #[test]
892    fn test_negative_scale() {
893        let d = Decimal64NoScale::new("12345", -2).unwrap();
894        assert_eq!(d.to_string_with_scale(-2), "12300");
895    }
896
897    #[test]
898    fn test_max_precision() {
899        // Max value that fits
900        let max = Decimal64NoScale::max_value();
901        assert!(max.is_finite());
902        assert!(max.value() > 0);
903
904        // Min value that fits
905        let min = Decimal64NoScale::min_value();
906        assert!(min.is_finite());
907        assert!(min.value() < 0);
908    }
909
910    #[test]
911    fn test_from_i64() {
912        // Basic scaling
913        let d = Decimal64NoScale::from_i64(123, 2).unwrap();
914        assert_eq!(d.value(), 12300);
915        assert_eq!(d.to_string_with_scale(2), "123");
916
917        // Zero scale
918        let d = Decimal64NoScale::from_i64(123, 0).unwrap();
919        assert_eq!(d.value(), 123);
920
921        // Negative value
922        let d = Decimal64NoScale::from_i64(-50, 2).unwrap();
923        assert_eq!(d.value(), -5000);
924
925        // Negative scale (divides)
926        let d = Decimal64NoScale::from_i64(12345, -2).unwrap();
927        assert_eq!(d.value(), 123);
928    }
929
930    #[test]
931    fn test_from_u64() {
932        let d = Decimal64NoScale::from_u64(123, 2).unwrap();
933        assert_eq!(d.value(), 12300);
934
935        // Value too large
936        assert!(Decimal64NoScale::from_u64(u64::MAX, 0).is_err());
937    }
938
939    #[test]
940    fn test_from_f64() {
941        // Basic conversion
942        let d = Decimal64NoScale::from_f64(123.45, 2).unwrap();
943        assert_eq!(d.value(), 12345);
944        assert_eq!(d.to_string_with_scale(2), "123.45");
945
946        // Special values
947        assert!(Decimal64NoScale::from_f64(f64::NAN, 2).unwrap().is_nan());
948        assert!(Decimal64NoScale::from_f64(f64::INFINITY, 2)
949            .unwrap()
950            .is_pos_infinity());
951        assert!(Decimal64NoScale::from_f64(f64::NEG_INFINITY, 2)
952            .unwrap()
953            .is_neg_infinity());
954
955        // Negative value
956        let d = Decimal64NoScale::from_f64(-99.99, 2).unwrap();
957        assert_eq!(d.value(), -9999);
958    }
959}