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:
29//! - `i64::MIN`: NaN
30//! - `i64::MIN + 1`: -Infinity
31//! - `i64::MAX`: +Infinity
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
69const SENTINEL_NAN: i64 = i64::MIN;
70const SENTINEL_NEG_INFINITY: i64 = i64::MIN + 1;
71const SENTINEL_POS_INFINITY: 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 (at the extremes of the range):
98/// - `i64::MIN`: NaN (sorts highest per PostgreSQL semantics)
99/// - `i64::MIN + 1`: -Infinity (sorts lowest)
100/// - `i64::MAX`: +Infinity
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    fn cmp(&self, other: &Self) -> Ordering {
682        // Handle special values with PostgreSQL ordering: -Inf < numbers < +Inf < NaN
683        match (self.is_special(), other.is_special()) {
684            (true, true) => {
685                if self.is_nan() && other.is_nan() {
686                    Ordering::Equal
687                } else if self.is_nan() {
688                    Ordering::Greater
689                } else if other.is_nan() {
690                    Ordering::Less
691                } else if (self.is_pos_infinity() && other.is_pos_infinity())
692                    || (self.is_neg_infinity() && other.is_neg_infinity())
693                {
694                    Ordering::Equal
695                } else if self.is_neg_infinity() {
696                    Ordering::Less
697                } else if self.is_pos_infinity() || other.is_neg_infinity() {
698                    Ordering::Greater
699                } else {
700                    Ordering::Less
701                }
702            }
703            (true, false) => {
704                if self.is_neg_infinity() {
705                    Ordering::Less
706                } else {
707                    Ordering::Greater
708                }
709            }
710            (false, true) => {
711                if other.is_neg_infinity() {
712                    Ordering::Greater
713                } else {
714                    Ordering::Less
715                }
716            }
717            (false, false) => self.value.cmp(&other.value),
718        }
719    }
720}
721
722impl fmt::Debug for Decimal64NoScale {
723    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
724        if self.is_nan() {
725            write!(f, "Decimal64NoScale(NaN)")
726        } else if self.is_pos_infinity() {
727            write!(f, "Decimal64NoScale(Infinity)")
728        } else if self.is_neg_infinity() {
729            write!(f, "Decimal64NoScale(-Infinity)")
730        } else {
731            f.debug_struct("Decimal64NoScale")
732                .field("value", &self.value)
733                .finish()
734        }
735    }
736}
737
738impl fmt::Display for Decimal64NoScale {
739    /// Display without scale (shows raw value).
740    ///
741    /// For formatted output, use `to_string_with_scale()`.
742    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
743        if self.is_nan() {
744            write!(f, "NaN")
745        } else if self.is_pos_infinity() {
746            write!(f, "Infinity")
747        } else if self.is_neg_infinity() {
748            write!(f, "-Infinity")
749        } else {
750            write!(f, "{}", self.value)
751        }
752    }
753}
754
755impl From<i64> for Decimal64NoScale {
756    fn from(value: i64) -> Self {
757        Self { value }
758    }
759}
760
761impl From<i32> for Decimal64NoScale {
762    fn from(value: i32) -> Self {
763        Self {
764            value: value as i64,
765        }
766    }
767}
768
769// Serde support
770impl Serialize for Decimal64NoScale {
771    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
772    where
773        S: Serializer,
774    {
775        serializer.serialize_i64(self.value)
776    }
777}
778
779impl<'de> Deserialize<'de> for Decimal64NoScale {
780    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
781    where
782        D: Deserializer<'de>,
783    {
784        let value = i64::deserialize(deserializer)?;
785        Ok(Self::from_raw(value))
786    }
787}
788
789#[cfg(test)]
790mod tests {
791    use super::*;
792
793    #[test]
794    fn test_new_basic() {
795        let d = Decimal64NoScale::new("123.45", 2).unwrap();
796        assert_eq!(d.value(), 12345);
797        assert_eq!(d.to_string_with_scale(2), "123.45");
798
799        let d = Decimal64NoScale::new("100", 0).unwrap();
800        assert_eq!(d.value(), 100);
801        assert_eq!(d.to_string_with_scale(0), "100");
802
803        let d = Decimal64NoScale::new("-50.5", 1).unwrap();
804        assert_eq!(d.value(), -505);
805        assert_eq!(d.to_string_with_scale(1), "-50.5");
806    }
807
808    #[test]
809    fn test_18_digit_precision() {
810        // This should work with Decimal64NoScale but NOT with Decimal64
811        let d = Decimal64NoScale::new("123456789012345678", 0).unwrap();
812        assert_eq!(d.value(), 123456789012345678);
813        assert_eq!(d.to_string_with_scale(0), "123456789012345678");
814
815        // 18 digits with 2 decimal places
816        let d = Decimal64NoScale::new("1234567890123456.78", 2).unwrap();
817        assert_eq!(d.value(), 123456789012345678);
818        assert_eq!(d.to_string_with_scale(2), "1234567890123456.78");
819    }
820
821    #[test]
822    fn test_aggregates_work() {
823        // This is the key test: aggregates on raw i64 values should work
824        let scale = 2;
825        let a = Decimal64NoScale::new("100.50", scale).unwrap();
826        let b = Decimal64NoScale::new("200.25", scale).unwrap();
827        let c = Decimal64NoScale::new("300.75", scale).unwrap();
828
829        // Sum the raw values
830        let sum = a.value() + b.value() + c.value();
831        assert_eq!(sum, 60150); // 601.50 * 100
832
833        // Interpret with scale
834        let result = Decimal64NoScale::from_raw(sum);
835        assert_eq!(result.to_string_with_scale(scale), "601.5");
836
837        // Min/Max
838        let values = [a.value(), b.value(), c.value()];
839        let min = *values.iter().min().unwrap();
840        let max = *values.iter().max().unwrap();
841        assert_eq!(
842            Decimal64NoScale::from_raw(min).to_string_with_scale(scale),
843            "100.5"
844        );
845        assert_eq!(
846            Decimal64NoScale::from_raw(max).to_string_with_scale(scale),
847            "300.75"
848        );
849    }
850
851    #[test]
852    fn test_special_values() {
853        let nan = Decimal64NoScale::nan();
854        assert!(nan.is_nan());
855        assert!(nan.is_special());
856        assert_eq!(nan.to_string_with_scale(2), "NaN");
857
858        let inf = Decimal64NoScale::infinity();
859        assert!(inf.is_pos_infinity());
860        assert_eq!(inf.to_string_with_scale(2), "Infinity");
861
862        let neg_inf = Decimal64NoScale::neg_infinity();
863        assert!(neg_inf.is_neg_infinity());
864        assert_eq!(neg_inf.to_string_with_scale(2), "-Infinity");
865    }
866
867    #[test]
868    fn test_ordering() {
869        let neg_inf = Decimal64NoScale::neg_infinity();
870        let neg = Decimal64NoScale::from_raw(-1000);
871        let zero = Decimal64NoScale::from_raw(0);
872        let pos = Decimal64NoScale::from_raw(1000);
873        let inf = Decimal64NoScale::infinity();
874        let nan = Decimal64NoScale::nan();
875
876        assert!(neg_inf < neg);
877        assert!(neg < zero);
878        assert!(zero < pos);
879        assert!(pos < inf);
880        assert!(inf < nan);
881    }
882
883    #[test]
884    fn test_from_str_special() {
885        assert!(Decimal64NoScale::new("Infinity", 0)
886            .unwrap()
887            .is_pos_infinity());
888        assert!(Decimal64NoScale::new("-Infinity", 0)
889            .unwrap()
890            .is_neg_infinity());
891        assert!(Decimal64NoScale::new("NaN", 0).unwrap().is_nan());
892    }
893
894    #[test]
895    fn test_roundtrip() {
896        let scale = 4;
897        let values = ["0", "123.4567", "-99.9999", "1000000", "-1"];
898
899        for s in values {
900            let d = Decimal64NoScale::new(s, scale).unwrap();
901            let raw = d.value();
902            let restored = Decimal64NoScale::from_raw(raw);
903            assert_eq!(d.value(), restored.value(), "Roundtrip failed for {}", s);
904        }
905    }
906
907    #[test]
908    fn test_byte_roundtrip() {
909        let d = Decimal64NoScale::new("123.45", 2).unwrap();
910        let bytes = d.to_be_bytes();
911        let restored = Decimal64NoScale::from_be_bytes(bytes);
912        assert_eq!(d, restored);
913    }
914
915    #[test]
916    fn test_zero() {
917        let d = Decimal64NoScale::new("0", 0).unwrap();
918        assert!(d.is_zero());
919        assert!(!d.is_negative());
920        assert!(!d.is_positive());
921        assert!(d.is_finite());
922    }
923
924    #[test]
925    fn test_negative_scale() {
926        let d = Decimal64NoScale::new("12345", -2).unwrap();
927        assert_eq!(d.to_string_with_scale(-2), "12300");
928    }
929
930    #[test]
931    fn test_max_precision() {
932        // Max value that fits
933        let max = Decimal64NoScale::max_value();
934        assert!(max.is_finite());
935        assert!(max.value() > 0);
936
937        // Min value that fits
938        let min = Decimal64NoScale::min_value();
939        assert!(min.is_finite());
940        assert!(min.value() < 0);
941    }
942
943    #[test]
944    fn test_from_i64() {
945        // Basic scaling
946        let d = Decimal64NoScale::from_i64(123, 2).unwrap();
947        assert_eq!(d.value(), 12300);
948        assert_eq!(d.to_string_with_scale(2), "123");
949
950        // Zero scale
951        let d = Decimal64NoScale::from_i64(123, 0).unwrap();
952        assert_eq!(d.value(), 123);
953
954        // Negative value
955        let d = Decimal64NoScale::from_i64(-50, 2).unwrap();
956        assert_eq!(d.value(), -5000);
957
958        // Negative scale (divides)
959        let d = Decimal64NoScale::from_i64(12345, -2).unwrap();
960        assert_eq!(d.value(), 123);
961    }
962
963    #[test]
964    fn test_from_u64() {
965        let d = Decimal64NoScale::from_u64(123, 2).unwrap();
966        assert_eq!(d.value(), 12300);
967
968        // Value too large
969        assert!(Decimal64NoScale::from_u64(u64::MAX, 0).is_err());
970    }
971
972    #[test]
973    fn test_from_f64() {
974        // Basic conversion
975        let d = Decimal64NoScale::from_f64(123.45, 2).unwrap();
976        assert_eq!(d.value(), 12345);
977        assert_eq!(d.to_string_with_scale(2), "123.45");
978
979        // Special values
980        assert!(Decimal64NoScale::from_f64(f64::NAN, 2).unwrap().is_nan());
981        assert!(Decimal64NoScale::from_f64(f64::INFINITY, 2)
982            .unwrap()
983            .is_pos_infinity());
984        assert!(Decimal64NoScale::from_f64(f64::NEG_INFINITY, 2)
985            .unwrap()
986            .is_neg_infinity());
987
988        // Negative value
989        let d = Decimal64NoScale::from_f64(-99.99, 2).unwrap();
990        assert_eq!(d.value(), -9999);
991    }
992}