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.00");  // Preserves trailing zeros
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    /// This preserves trailing zeros to match the specified scale, which is
396    /// important for PostgreSQL NUMERIC display formatting.
397    ///
398    /// - Positive scale: adds decimal point with trailing zeros as needed
399    /// - Zero scale: integer output, no decimal point
400    /// - Negative scale: multiplies by power of 10, no decimal point
401    ///
402    /// # Arguments
403    /// * `scale` - The scale to use for formatting
404    ///
405    /// # Examples
406    ///
407    /// ```
408    /// use decimal_bytes::Decimal64NoScale;
409    ///
410    /// // Positive scale: includes decimal point and trailing zeros
411    /// let d = Decimal64NoScale::from_raw(12345);
412    /// assert_eq!(d.to_string_with_scale(2), "123.45");
413    /// assert_eq!(d.to_string_with_scale(3), "12.345");
414    ///
415    /// // Trailing zeros are preserved for positive scale
416    /// let d = Decimal64NoScale::from_raw(300);  // Represents 3.00 with scale 2
417    /// assert_eq!(d.to_string_with_scale(2), "3.00");
418    ///
419    /// // Zero also includes trailing zeros for positive scale
420    /// let zero = Decimal64NoScale::from_raw(0);
421    /// assert_eq!(zero.to_string_with_scale(2), "0.00");
422    ///
423    /// // Zero scale: no decimal point
424    /// let d = Decimal64NoScale::from_raw(12345);
425    /// assert_eq!(d.to_string_with_scale(0), "12345");
426    /// assert_eq!(zero.to_string_with_scale(0), "0");
427    ///
428    /// // Negative scale: multiplies, no decimal point
429    /// let d = Decimal64NoScale::from_raw(123);
430    /// assert_eq!(d.to_string_with_scale(-2), "12300");
431    /// ```
432    pub fn to_string_with_scale(&self, scale: i32) -> String {
433        // Handle special values
434        if self.is_neg_infinity() {
435            return "-Infinity".to_string();
436        }
437        if self.is_pos_infinity() {
438            return "Infinity".to_string();
439        }
440        if self.is_nan() {
441            return "NaN".to_string();
442        }
443
444        let value = self.value;
445
446        if value == 0 {
447            return if scale > 0 {
448                format!("0.{}", "0".repeat(scale as usize))
449            } else {
450                "0".to_string()
451            };
452        }
453
454        let is_negative = value < 0;
455        let abs_value = value.unsigned_abs();
456
457        if scale <= 0 {
458            // Negative or zero scale: multiply
459            let result = if scale < 0 {
460                abs_value * 10u64.pow((-scale) as u32)
461            } else {
462                abs_value
463            };
464            return if is_negative {
465                format!("-{}", result)
466            } else {
467                result.to_string()
468            };
469        }
470
471        let scale_factor = 10u64.pow(scale as u32);
472        let int_part = abs_value / scale_factor;
473        let frac_part = abs_value % scale_factor;
474
475        // Always include the decimal point and full scale digits (with trailing zeros)
476        let frac_str = format!("{:0>width$}", frac_part, width = scale as usize);
477        let result = format!("{}.{}", int_part, frac_str);
478
479        if is_negative {
480            format!("-{}", result)
481        } else {
482            result
483        }
484    }
485
486    /// Returns the 8-byte big-endian representation.
487    #[inline]
488    pub fn to_be_bytes(&self) -> [u8; 8] {
489        self.value.to_be_bytes()
490    }
491
492    /// Creates a Decimal64NoScale from big-endian bytes.
493    #[inline]
494    pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
495        Self {
496            value: i64::from_be_bytes(bytes),
497        }
498    }
499
500    /// Converts to the variable-length `Decimal` type.
501    ///
502    /// Note: This requires a scale to format correctly.
503    pub fn to_decimal(&self, scale: i32) -> Decimal {
504        if self.is_neg_infinity() {
505            return Decimal::neg_infinity();
506        }
507        if self.is_pos_infinity() {
508            return Decimal::infinity();
509        }
510        if self.is_nan() {
511            return Decimal::nan();
512        }
513
514        Decimal::from_str(&self.to_string_with_scale(scale))
515            .expect("Decimal64NoScale string is always valid")
516    }
517
518    /// Creates a Decimal64NoScale from a Decimal with the specified scale.
519    pub fn from_decimal(decimal: &Decimal, scale: i32) -> Result<Self, DecimalError> {
520        if decimal.is_nan() {
521            return Ok(Self::nan());
522        }
523        if decimal.is_pos_infinity() {
524            return Ok(Self::infinity());
525        }
526        if decimal.is_neg_infinity() {
527            return Ok(Self::neg_infinity());
528        }
529
530        Self::new(&decimal.to_string(), scale)
531    }
532
533    /// Returns the minimum finite value.
534    #[inline]
535    pub const fn min_value() -> Self {
536        Self { value: MIN_VALUE }
537    }
538
539    /// Returns the maximum finite value.
540    #[inline]
541    pub const fn max_value() -> Self {
542        Self { value: MAX_VALUE }
543    }
544
545    // ==================== Comparison with Scale ====================
546
547    /// Compares two values, normalizing scales if different.
548    ///
549    /// This is needed when comparing values that might have been stored
550    /// with different scales in different columns.
551    pub fn cmp_with_scale(&self, other: &Self, self_scale: i32, other_scale: i32) -> Ordering {
552        // Handle special values
553        match (self.is_special(), other.is_special()) {
554            (true, true) => {
555                // Both special: NaN > Infinity > -Infinity
556                if self.is_nan() && other.is_nan() {
557                    return Ordering::Equal;
558                }
559                if self.is_nan() {
560                    return Ordering::Greater;
561                }
562                if other.is_nan() {
563                    return Ordering::Less;
564                }
565                if self.is_pos_infinity() && other.is_pos_infinity() {
566                    return Ordering::Equal;
567                }
568                if self.is_neg_infinity() && other.is_neg_infinity() {
569                    return Ordering::Equal;
570                }
571                if self.is_pos_infinity() {
572                    return Ordering::Greater;
573                }
574                if self.is_neg_infinity() {
575                    return Ordering::Less;
576                }
577                if other.is_pos_infinity() {
578                    return Ordering::Less;
579                }
580                Ordering::Greater // other is -Infinity
581            }
582            (true, false) => {
583                if self.is_neg_infinity() {
584                    Ordering::Less
585                } else {
586                    Ordering::Greater
587                }
588            }
589            (false, true) => {
590                if other.is_neg_infinity() {
591                    Ordering::Greater
592                } else {
593                    Ordering::Less
594                }
595            }
596            (false, false) => {
597                // Both normal: normalize to common scale
598                if self_scale == other_scale {
599                    self.value.cmp(&other.value)
600                } else {
601                    let max_scale = self_scale.max(other_scale);
602
603                    let self_normalized = if self_scale < max_scale {
604                        self.value
605                            .saturating_mul(10i64.pow((max_scale - self_scale) as u32))
606                    } else {
607                        self.value
608                    };
609
610                    let other_normalized = if other_scale < max_scale {
611                        other
612                            .value
613                            .saturating_mul(10i64.pow((max_scale - other_scale) as u32))
614                    } else {
615                        other.value
616                    };
617
618                    self_normalized.cmp(&other_normalized)
619                }
620            }
621        }
622    }
623
624    // ==================== Internal Helpers ====================
625
626    fn compute_scaled_value(
627        int_part: &str,
628        frac_part: &str,
629        is_negative: bool,
630        scale: i32,
631    ) -> Result<i64, DecimalError> {
632        if scale < 0 {
633            // Negative scale: round to powers of 10, store the quotient
634            // stored = actual_value / 10^(-scale)
635            // e.g., 12345 with scale=-2 → store 123 (rounds to 12300, then /100)
636            let round_digits = (-scale) as usize;
637            let int_value: i64 = int_part.parse().unwrap_or(0);
638
639            if int_part.len() <= round_digits {
640                return Ok(0);
641            }
642
643            let divisor = 10i64.pow(round_digits as u32);
644            // Round and divide (don't multiply back)
645            let rounded = (int_value + divisor / 2) / divisor;
646            return Ok(if is_negative { -rounded } else { rounded });
647        }
648
649        let scale_u = scale as usize;
650
651        // Parse integer part
652        let int_value: i64 = if int_part == "0" || int_part.is_empty() {
653            0
654        } else {
655            int_part.parse().map_err(|_| {
656                DecimalError::InvalidFormat(format!("Invalid integer part: {}", int_part))
657            })?
658        };
659
660        // Apply scale to get scaled integer part
661        let scale_factor = 10i64.pow(scale as u32);
662        let scaled_int = int_value.checked_mul(scale_factor).ok_or_else(|| {
663            DecimalError::InvalidFormat("Value too large for Decimal64NoScale".to_string())
664        })?;
665
666        // Parse fractional part (truncate or pad to scale)
667        let frac_value: i64 = if frac_part.is_empty() {
668            0
669        } else if frac_part.len() <= scale_u {
670            // Pad with zeros
671            let padded = format!("{:0<width$}", frac_part, width = scale_u);
672            padded.parse().unwrap_or(0)
673        } else {
674            // Truncate (with rounding)
675            let truncated = &frac_part[..scale_u];
676            let next_digit = frac_part.chars().nth(scale_u).unwrap_or('0');
677            let mut value: i64 = truncated.parse().unwrap_or(0);
678            if next_digit >= '5' {
679                value += 1;
680            }
681            value
682        };
683
684        let result = scaled_int + frac_value;
685        Ok(if is_negative && result != 0 {
686            -result
687        } else {
688            result
689        })
690    }
691}
692
693// ==================== Trait Implementations ====================
694
695impl PartialOrd for Decimal64NoScale {
696    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
697        Some(self.cmp(other))
698    }
699}
700
701impl Ord for Decimal64NoScale {
702    /// Compares values assuming same scale.
703    ///
704    /// For cross-scale comparison, use `cmp_with_scale()`.
705    ///
706    /// The sentinel values are chosen so that standard i64 comparison gives
707    /// PostgreSQL ordering: -Infinity < numbers < +Infinity < NaN
708    fn cmp(&self, other: &Self) -> Ordering {
709        self.value.cmp(&other.value)
710    }
711}
712
713impl fmt::Debug for Decimal64NoScale {
714    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
715        if self.is_nan() {
716            write!(f, "Decimal64NoScale(NaN)")
717        } else if self.is_pos_infinity() {
718            write!(f, "Decimal64NoScale(Infinity)")
719        } else if self.is_neg_infinity() {
720            write!(f, "Decimal64NoScale(-Infinity)")
721        } else {
722            f.debug_struct("Decimal64NoScale")
723                .field("value", &self.value)
724                .finish()
725        }
726    }
727}
728
729impl fmt::Display for Decimal64NoScale {
730    /// Display without scale (shows raw value).
731    ///
732    /// For formatted output, use `to_string_with_scale()`.
733    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
734        if self.is_nan() {
735            write!(f, "NaN")
736        } else if self.is_pos_infinity() {
737            write!(f, "Infinity")
738        } else if self.is_neg_infinity() {
739            write!(f, "-Infinity")
740        } else {
741            write!(f, "{}", self.value)
742        }
743    }
744}
745
746impl From<i64> for Decimal64NoScale {
747    fn from(value: i64) -> Self {
748        Self { value }
749    }
750}
751
752impl From<i32> for Decimal64NoScale {
753    fn from(value: i32) -> Self {
754        Self {
755            value: value as i64,
756        }
757    }
758}
759
760// Serde support
761impl Serialize for Decimal64NoScale {
762    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
763    where
764        S: Serializer,
765    {
766        serializer.serialize_i64(self.value)
767    }
768}
769
770impl<'de> Deserialize<'de> for Decimal64NoScale {
771    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
772    where
773        D: Deserializer<'de>,
774    {
775        let value = i64::deserialize(deserializer)?;
776        Ok(Self::from_raw(value))
777    }
778}
779
780#[cfg(test)]
781mod tests {
782    use super::*;
783
784    #[test]
785    fn test_new_basic() {
786        let d = Decimal64NoScale::new("123.45", 2).unwrap();
787        assert_eq!(d.value(), 12345);
788        assert_eq!(d.to_string_with_scale(2), "123.45");
789
790        let d = Decimal64NoScale::new("100", 0).unwrap();
791        assert_eq!(d.value(), 100);
792        assert_eq!(d.to_string_with_scale(0), "100");
793
794        let d = Decimal64NoScale::new("-50.5", 1).unwrap();
795        assert_eq!(d.value(), -505);
796        assert_eq!(d.to_string_with_scale(1), "-50.5");
797    }
798
799    #[test]
800    fn test_18_digit_precision() {
801        // This should work with Decimal64NoScale but NOT with Decimal64
802        let d = Decimal64NoScale::new("123456789012345678", 0).unwrap();
803        assert_eq!(d.value(), 123456789012345678);
804        assert_eq!(d.to_string_with_scale(0), "123456789012345678");
805
806        // 18 digits with 2 decimal places
807        let d = Decimal64NoScale::new("1234567890123456.78", 2).unwrap();
808        assert_eq!(d.value(), 123456789012345678);
809        assert_eq!(d.to_string_with_scale(2), "1234567890123456.78");
810    }
811
812    #[test]
813    fn test_aggregates_work() {
814        // This is the key test: aggregates on raw i64 values should work
815        let scale = 2;
816        let a = Decimal64NoScale::new("100.50", scale).unwrap();
817        let b = Decimal64NoScale::new("200.25", scale).unwrap();
818        let c = Decimal64NoScale::new("300.75", scale).unwrap();
819
820        // Sum the raw values
821        let sum = a.value() + b.value() + c.value();
822        assert_eq!(sum, 60150); // 601.50 * 100
823
824        // Interpret with scale - preserves trailing zeros
825        let result = Decimal64NoScale::from_raw(sum);
826        assert_eq!(result.to_string_with_scale(scale), "601.50");
827
828        // Min/Max - preserves trailing zeros
829        let values = [a.value(), b.value(), c.value()];
830        let min = *values.iter().min().unwrap();
831        let max = *values.iter().max().unwrap();
832        assert_eq!(
833            Decimal64NoScale::from_raw(min).to_string_with_scale(scale),
834            "100.50"
835        );
836        assert_eq!(
837            Decimal64NoScale::from_raw(max).to_string_with_scale(scale),
838            "300.75"
839        );
840    }
841
842    #[test]
843    fn test_special_values() {
844        let nan = Decimal64NoScale::nan();
845        assert!(nan.is_nan());
846        assert!(nan.is_special());
847        assert_eq!(nan.to_string_with_scale(2), "NaN");
848
849        let inf = Decimal64NoScale::infinity();
850        assert!(inf.is_pos_infinity());
851        assert_eq!(inf.to_string_with_scale(2), "Infinity");
852
853        let neg_inf = Decimal64NoScale::neg_infinity();
854        assert!(neg_inf.is_neg_infinity());
855        assert_eq!(neg_inf.to_string_with_scale(2), "-Infinity");
856    }
857
858    #[test]
859    fn test_ordering() {
860        let neg_inf = Decimal64NoScale::neg_infinity();
861        let neg = Decimal64NoScale::from_raw(-1000);
862        let zero = Decimal64NoScale::from_raw(0);
863        let pos = Decimal64NoScale::from_raw(1000);
864        let inf = Decimal64NoScale::infinity();
865        let nan = Decimal64NoScale::nan();
866
867        assert!(neg_inf < neg);
868        assert!(neg < zero);
869        assert!(zero < pos);
870        assert!(pos < inf);
871        assert!(inf < nan);
872    }
873
874    #[test]
875    fn test_from_str_special() {
876        assert!(Decimal64NoScale::new("Infinity", 0)
877            .unwrap()
878            .is_pos_infinity());
879        assert!(Decimal64NoScale::new("-Infinity", 0)
880            .unwrap()
881            .is_neg_infinity());
882        assert!(Decimal64NoScale::new("NaN", 0).unwrap().is_nan());
883    }
884
885    #[test]
886    fn test_roundtrip() {
887        let scale = 4;
888        let values = ["0", "123.4567", "-99.9999", "1000000", "-1"];
889
890        for s in values {
891            let d = Decimal64NoScale::new(s, scale).unwrap();
892            let raw = d.value();
893            let restored = Decimal64NoScale::from_raw(raw);
894            assert_eq!(d.value(), restored.value(), "Roundtrip failed for {}", s);
895        }
896    }
897
898    #[test]
899    fn test_byte_roundtrip() {
900        let d = Decimal64NoScale::new("123.45", 2).unwrap();
901        let bytes = d.to_be_bytes();
902        let restored = Decimal64NoScale::from_be_bytes(bytes);
903        assert_eq!(d, restored);
904    }
905
906    #[test]
907    fn test_zero() {
908        let d = Decimal64NoScale::new("0", 0).unwrap();
909        assert!(d.is_zero());
910        assert!(!d.is_negative());
911        assert!(!d.is_positive());
912        assert!(d.is_finite());
913    }
914
915    #[test]
916    fn test_negative_scale() {
917        // Negative scale: rounds to left of decimal point, no decimal in output
918        let d = Decimal64NoScale::new("12345", -2).unwrap();
919        assert_eq!(d.to_string_with_scale(-2), "12300");
920
921        // More negative scale cases - should NOT have decimal points
922        let d = Decimal64NoScale::from_raw(123);
923        assert_eq!(d.to_string_with_scale(-1), "1230"); // 123 * 10
924        assert_eq!(d.to_string_with_scale(-2), "12300"); // 123 * 100
925
926        // Zero scale - should NOT have decimal point
927        let d = Decimal64NoScale::from_raw(12345);
928        assert_eq!(d.to_string_with_scale(0), "12345");
929
930        // Zero value with negative/zero scale - no decimal point
931        let zero = Decimal64NoScale::from_raw(0);
932        assert_eq!(zero.to_string_with_scale(0), "0");
933        assert_eq!(zero.to_string_with_scale(-2), "0");
934    }
935
936    #[test]
937    fn test_max_precision() {
938        // Max value that fits
939        let max = Decimal64NoScale::max_value();
940        assert!(max.is_finite());
941        assert!(max.value() > 0);
942
943        // Min value that fits
944        let min = Decimal64NoScale::min_value();
945        assert!(min.is_finite());
946        assert!(min.value() < 0);
947    }
948
949    #[test]
950    fn test_from_i64() {
951        // Basic scaling - preserves trailing zeros
952        let d = Decimal64NoScale::from_i64(123, 2).unwrap();
953        assert_eq!(d.value(), 12300);
954        assert_eq!(d.to_string_with_scale(2), "123.00");
955
956        // Zero scale
957        let d = Decimal64NoScale::from_i64(123, 0).unwrap();
958        assert_eq!(d.value(), 123);
959
960        // Negative value
961        let d = Decimal64NoScale::from_i64(-50, 2).unwrap();
962        assert_eq!(d.value(), -5000);
963
964        // Negative scale (divides)
965        let d = Decimal64NoScale::from_i64(12345, -2).unwrap();
966        assert_eq!(d.value(), 123);
967    }
968
969    #[test]
970    fn test_from_u64() {
971        let d = Decimal64NoScale::from_u64(123, 2).unwrap();
972        assert_eq!(d.value(), 12300);
973
974        // Value too large
975        assert!(Decimal64NoScale::from_u64(u64::MAX, 0).is_err());
976    }
977
978    #[test]
979    fn test_from_f64() {
980        // Basic conversion
981        let d = Decimal64NoScale::from_f64(123.45, 2).unwrap();
982        assert_eq!(d.value(), 12345);
983        assert_eq!(d.to_string_with_scale(2), "123.45");
984
985        // Special values
986        assert!(Decimal64NoScale::from_f64(f64::NAN, 2).unwrap().is_nan());
987        assert!(Decimal64NoScale::from_f64(f64::INFINITY, 2)
988            .unwrap()
989            .is_pos_infinity());
990        assert!(Decimal64NoScale::from_f64(f64::NEG_INFINITY, 2)
991            .unwrap()
992            .is_neg_infinity());
993
994        // Negative value
995        let d = Decimal64NoScale::from_f64(-99.99, 2).unwrap();
996        assert_eq!(d.value(), -9999);
997    }
998}