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    // ==================== Special Value Constructors ====================
197
198    /// Creates positive infinity.
199    #[inline]
200    pub const fn infinity() -> Self {
201        Self {
202            value: SENTINEL_POS_INFINITY,
203        }
204    }
205
206    /// Creates negative infinity.
207    #[inline]
208    pub const fn neg_infinity() -> Self {
209        Self {
210            value: SENTINEL_NEG_INFINITY,
211        }
212    }
213
214    /// Creates NaN (Not a Number).
215    ///
216    /// Follows PostgreSQL semantics: `NaN == NaN` is `true`.
217    #[inline]
218    pub const fn nan() -> Self {
219        Self {
220            value: SENTINEL_NAN,
221        }
222    }
223
224    // ==================== Accessors ====================
225
226    /// Returns the raw i64 value.
227    ///
228    /// For normal values, this is `actual_value * 10^scale`.
229    /// For special values, this returns the sentinel value.
230    #[inline]
231    pub const fn value(&self) -> i64 {
232        self.value
233    }
234
235    /// Returns the raw i64 value (alias for columnar storage compatibility).
236    #[inline]
237    pub const fn raw(&self) -> i64 {
238        self.value
239    }
240
241    /// Returns true if this value is zero.
242    #[inline]
243    pub fn is_zero(&self) -> bool {
244        !self.is_special() && self.value == 0
245    }
246
247    /// Returns true if this value is negative (excluding special values).
248    #[inline]
249    pub fn is_negative(&self) -> bool {
250        !self.is_special() && self.value < 0
251    }
252
253    /// Returns true if this value is positive (excluding special values).
254    #[inline]
255    pub fn is_positive(&self) -> bool {
256        !self.is_special() && self.value > 0
257    }
258
259    /// Returns true if this value is positive infinity.
260    #[inline]
261    pub fn is_pos_infinity(&self) -> bool {
262        self.value == SENTINEL_POS_INFINITY
263    }
264
265    /// Returns true if this value is negative infinity.
266    #[inline]
267    pub fn is_neg_infinity(&self) -> bool {
268        self.value == SENTINEL_NEG_INFINITY
269    }
270
271    /// Returns true if this value is positive or negative infinity.
272    #[inline]
273    pub fn is_infinity(&self) -> bool {
274        self.is_pos_infinity() || self.is_neg_infinity()
275    }
276
277    /// Returns true if this value is NaN (Not a Number).
278    #[inline]
279    pub fn is_nan(&self) -> bool {
280        self.value == SENTINEL_NAN
281    }
282
283    /// Returns true if this is a special value (Infinity or NaN).
284    #[inline]
285    pub fn is_special(&self) -> bool {
286        self.value == SENTINEL_NAN
287            || self.value == SENTINEL_NEG_INFINITY
288            || self.value == SENTINEL_POS_INFINITY
289    }
290
291    /// Returns true if this is a finite number (not Infinity or NaN).
292    #[inline]
293    pub fn is_finite(&self) -> bool {
294        !self.is_special()
295    }
296
297    // ==================== Conversions ====================
298
299    /// Formats the value as a decimal string using the given scale.
300    ///
301    /// # Arguments
302    /// * `scale` - The scale to use for formatting
303    ///
304    /// # Examples
305    ///
306    /// ```
307    /// use decimal_bytes::Decimal64NoScale;
308    ///
309    /// let d = Decimal64NoScale::from_raw(12345);
310    /// assert_eq!(d.to_string_with_scale(2), "123.45");
311    /// assert_eq!(d.to_string_with_scale(3), "12.345");
312    /// assert_eq!(d.to_string_with_scale(0), "12345");
313    /// ```
314    pub fn to_string_with_scale(&self, scale: i32) -> String {
315        // Handle special values
316        if self.is_neg_infinity() {
317            return "-Infinity".to_string();
318        }
319        if self.is_pos_infinity() {
320            return "Infinity".to_string();
321        }
322        if self.is_nan() {
323            return "NaN".to_string();
324        }
325
326        let value = self.value;
327
328        if value == 0 {
329            return "0".to_string();
330        }
331
332        let is_negative = value < 0;
333        let abs_value = value.unsigned_abs();
334
335        if scale <= 0 {
336            // Negative or zero scale: multiply
337            let result = if scale < 0 {
338                abs_value * 10u64.pow((-scale) as u32)
339            } else {
340                abs_value
341            };
342            return if is_negative {
343                format!("-{}", result)
344            } else {
345                result.to_string()
346            };
347        }
348
349        let scale_factor = 10u64.pow(scale as u32);
350        let int_part = abs_value / scale_factor;
351        let frac_part = abs_value % scale_factor;
352
353        let result = if frac_part == 0 {
354            int_part.to_string()
355        } else {
356            let frac_str = format!("{:0>width$}", frac_part, width = scale as usize);
357            let frac_str = frac_str.trim_end_matches('0');
358            format!("{}.{}", int_part, frac_str)
359        };
360
361        if is_negative {
362            format!("-{}", result)
363        } else {
364            result
365        }
366    }
367
368    /// Returns the 8-byte big-endian representation.
369    #[inline]
370    pub fn to_be_bytes(&self) -> [u8; 8] {
371        self.value.to_be_bytes()
372    }
373
374    /// Creates a Decimal64NoScale from big-endian bytes.
375    #[inline]
376    pub fn from_be_bytes(bytes: [u8; 8]) -> Self {
377        Self {
378            value: i64::from_be_bytes(bytes),
379        }
380    }
381
382    /// Converts to the variable-length `Decimal` type.
383    ///
384    /// Note: This requires a scale to format correctly.
385    pub fn to_decimal(&self, scale: i32) -> Decimal {
386        if self.is_neg_infinity() {
387            return Decimal::neg_infinity();
388        }
389        if self.is_pos_infinity() {
390            return Decimal::infinity();
391        }
392        if self.is_nan() {
393            return Decimal::nan();
394        }
395
396        Decimal::from_str(&self.to_string_with_scale(scale))
397            .expect("Decimal64NoScale string is always valid")
398    }
399
400    /// Creates a Decimal64NoScale from a Decimal with the specified scale.
401    pub fn from_decimal(decimal: &Decimal, scale: i32) -> Result<Self, DecimalError> {
402        if decimal.is_nan() {
403            return Ok(Self::nan());
404        }
405        if decimal.is_pos_infinity() {
406            return Ok(Self::infinity());
407        }
408        if decimal.is_neg_infinity() {
409            return Ok(Self::neg_infinity());
410        }
411
412        Self::new(&decimal.to_string(), scale)
413    }
414
415    /// Returns the minimum finite value.
416    #[inline]
417    pub const fn min_value() -> Self {
418        Self { value: MIN_VALUE }
419    }
420
421    /// Returns the maximum finite value.
422    #[inline]
423    pub const fn max_value() -> Self {
424        Self { value: MAX_VALUE }
425    }
426
427    // ==================== Comparison with Scale ====================
428
429    /// Compares two values, normalizing scales if different.
430    ///
431    /// This is needed when comparing values that might have been stored
432    /// with different scales in different columns.
433    pub fn cmp_with_scale(&self, other: &Self, self_scale: i32, other_scale: i32) -> Ordering {
434        // Handle special values
435        match (self.is_special(), other.is_special()) {
436            (true, true) => {
437                // Both special: NaN > Infinity > -Infinity
438                if self.is_nan() && other.is_nan() {
439                    return Ordering::Equal;
440                }
441                if self.is_nan() {
442                    return Ordering::Greater;
443                }
444                if other.is_nan() {
445                    return Ordering::Less;
446                }
447                if self.is_pos_infinity() && other.is_pos_infinity() {
448                    return Ordering::Equal;
449                }
450                if self.is_neg_infinity() && other.is_neg_infinity() {
451                    return Ordering::Equal;
452                }
453                if self.is_pos_infinity() {
454                    return Ordering::Greater;
455                }
456                if self.is_neg_infinity() {
457                    return Ordering::Less;
458                }
459                if other.is_pos_infinity() {
460                    return Ordering::Less;
461                }
462                Ordering::Greater // other is -Infinity
463            }
464            (true, false) => {
465                if self.is_neg_infinity() {
466                    Ordering::Less
467                } else {
468                    Ordering::Greater
469                }
470            }
471            (false, true) => {
472                if other.is_neg_infinity() {
473                    Ordering::Greater
474                } else {
475                    Ordering::Less
476                }
477            }
478            (false, false) => {
479                // Both normal: normalize to common scale
480                if self_scale == other_scale {
481                    self.value.cmp(&other.value)
482                } else {
483                    let max_scale = self_scale.max(other_scale);
484
485                    let self_normalized = if self_scale < max_scale {
486                        self.value
487                            .saturating_mul(10i64.pow((max_scale - self_scale) as u32))
488                    } else {
489                        self.value
490                    };
491
492                    let other_normalized = if other_scale < max_scale {
493                        other
494                            .value
495                            .saturating_mul(10i64.pow((max_scale - other_scale) as u32))
496                    } else {
497                        other.value
498                    };
499
500                    self_normalized.cmp(&other_normalized)
501                }
502            }
503        }
504    }
505
506    // ==================== Internal Helpers ====================
507
508    fn compute_scaled_value(
509        int_part: &str,
510        frac_part: &str,
511        is_negative: bool,
512        scale: i32,
513    ) -> Result<i64, DecimalError> {
514        if scale < 0 {
515            // Negative scale: round to powers of 10, store the quotient
516            // stored = actual_value / 10^(-scale)
517            // e.g., 12345 with scale=-2 → store 123 (rounds to 12300, then /100)
518            let round_digits = (-scale) as usize;
519            let int_value: i64 = int_part.parse().unwrap_or(0);
520
521            if int_part.len() <= round_digits {
522                return Ok(0);
523            }
524
525            let divisor = 10i64.pow(round_digits as u32);
526            // Round and divide (don't multiply back)
527            let rounded = (int_value + divisor / 2) / divisor;
528            return Ok(if is_negative { -rounded } else { rounded });
529        }
530
531        let scale_u = scale as usize;
532
533        // Parse integer part
534        let int_value: i64 = if int_part == "0" || int_part.is_empty() {
535            0
536        } else {
537            int_part.parse().map_err(|_| {
538                DecimalError::InvalidFormat(format!("Invalid integer part: {}", int_part))
539            })?
540        };
541
542        // Apply scale to get scaled integer part
543        let scale_factor = 10i64.pow(scale as u32);
544        let scaled_int = int_value.checked_mul(scale_factor).ok_or_else(|| {
545            DecimalError::InvalidFormat("Value too large for Decimal64NoScale".to_string())
546        })?;
547
548        // Parse fractional part (truncate or pad to scale)
549        let frac_value: i64 = if frac_part.is_empty() {
550            0
551        } else if frac_part.len() <= scale_u {
552            // Pad with zeros
553            let padded = format!("{:0<width$}", frac_part, width = scale_u);
554            padded.parse().unwrap_or(0)
555        } else {
556            // Truncate (with rounding)
557            let truncated = &frac_part[..scale_u];
558            let next_digit = frac_part.chars().nth(scale_u).unwrap_or('0');
559            let mut value: i64 = truncated.parse().unwrap_or(0);
560            if next_digit >= '5' {
561                value += 1;
562            }
563            value
564        };
565
566        let result = scaled_int + frac_value;
567        Ok(if is_negative && result != 0 {
568            -result
569        } else {
570            result
571        })
572    }
573}
574
575// ==================== Trait Implementations ====================
576
577impl PartialOrd for Decimal64NoScale {
578    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
579        Some(self.cmp(other))
580    }
581}
582
583impl Ord for Decimal64NoScale {
584    /// Compares values assuming same scale.
585    ///
586    /// For cross-scale comparison, use `cmp_with_scale()`.
587    fn cmp(&self, other: &Self) -> Ordering {
588        // Handle special values with PostgreSQL ordering: -Inf < numbers < +Inf < NaN
589        match (self.is_special(), other.is_special()) {
590            (true, true) => {
591                if self.is_nan() && other.is_nan() {
592                    Ordering::Equal
593                } else if self.is_nan() {
594                    Ordering::Greater
595                } else if other.is_nan() {
596                    Ordering::Less
597                } else if (self.is_pos_infinity() && other.is_pos_infinity())
598                    || (self.is_neg_infinity() && other.is_neg_infinity())
599                {
600                    Ordering::Equal
601                } else if self.is_neg_infinity() {
602                    Ordering::Less
603                } else if self.is_pos_infinity() || other.is_neg_infinity() {
604                    Ordering::Greater
605                } else {
606                    Ordering::Less
607                }
608            }
609            (true, false) => {
610                if self.is_neg_infinity() {
611                    Ordering::Less
612                } else {
613                    Ordering::Greater
614                }
615            }
616            (false, true) => {
617                if other.is_neg_infinity() {
618                    Ordering::Greater
619                } else {
620                    Ordering::Less
621                }
622            }
623            (false, false) => self.value.cmp(&other.value),
624        }
625    }
626}
627
628impl fmt::Debug for Decimal64NoScale {
629    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
630        if self.is_nan() {
631            write!(f, "Decimal64NoScale(NaN)")
632        } else if self.is_pos_infinity() {
633            write!(f, "Decimal64NoScale(Infinity)")
634        } else if self.is_neg_infinity() {
635            write!(f, "Decimal64NoScale(-Infinity)")
636        } else {
637            f.debug_struct("Decimal64NoScale")
638                .field("value", &self.value)
639                .finish()
640        }
641    }
642}
643
644impl fmt::Display for Decimal64NoScale {
645    /// Display without scale (shows raw value).
646    ///
647    /// For formatted output, use `to_string_with_scale()`.
648    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
649        if self.is_nan() {
650            write!(f, "NaN")
651        } else if self.is_pos_infinity() {
652            write!(f, "Infinity")
653        } else if self.is_neg_infinity() {
654            write!(f, "-Infinity")
655        } else {
656            write!(f, "{}", self.value)
657        }
658    }
659}
660
661impl From<i64> for Decimal64NoScale {
662    fn from(value: i64) -> Self {
663        Self { value }
664    }
665}
666
667impl From<i32> for Decimal64NoScale {
668    fn from(value: i32) -> Self {
669        Self {
670            value: value as i64,
671        }
672    }
673}
674
675// Serde support
676impl Serialize for Decimal64NoScale {
677    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
678    where
679        S: Serializer,
680    {
681        serializer.serialize_i64(self.value)
682    }
683}
684
685impl<'de> Deserialize<'de> for Decimal64NoScale {
686    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
687    where
688        D: Deserializer<'de>,
689    {
690        let value = i64::deserialize(deserializer)?;
691        Ok(Self::from_raw(value))
692    }
693}
694
695#[cfg(test)]
696mod tests {
697    use super::*;
698
699    #[test]
700    fn test_new_basic() {
701        let d = Decimal64NoScale::new("123.45", 2).unwrap();
702        assert_eq!(d.value(), 12345);
703        assert_eq!(d.to_string_with_scale(2), "123.45");
704
705        let d = Decimal64NoScale::new("100", 0).unwrap();
706        assert_eq!(d.value(), 100);
707        assert_eq!(d.to_string_with_scale(0), "100");
708
709        let d = Decimal64NoScale::new("-50.5", 1).unwrap();
710        assert_eq!(d.value(), -505);
711        assert_eq!(d.to_string_with_scale(1), "-50.5");
712    }
713
714    #[test]
715    fn test_18_digit_precision() {
716        // This should work with Decimal64NoScale but NOT with Decimal64
717        let d = Decimal64NoScale::new("123456789012345678", 0).unwrap();
718        assert_eq!(d.value(), 123456789012345678);
719        assert_eq!(d.to_string_with_scale(0), "123456789012345678");
720
721        // 18 digits with 2 decimal places
722        let d = Decimal64NoScale::new("1234567890123456.78", 2).unwrap();
723        assert_eq!(d.value(), 123456789012345678);
724        assert_eq!(d.to_string_with_scale(2), "1234567890123456.78");
725    }
726
727    #[test]
728    fn test_aggregates_work() {
729        // This is the key test: aggregates on raw i64 values should work
730        let scale = 2;
731        let a = Decimal64NoScale::new("100.50", scale).unwrap();
732        let b = Decimal64NoScale::new("200.25", scale).unwrap();
733        let c = Decimal64NoScale::new("300.75", scale).unwrap();
734
735        // Sum the raw values
736        let sum = a.value() + b.value() + c.value();
737        assert_eq!(sum, 60150); // 601.50 * 100
738
739        // Interpret with scale
740        let result = Decimal64NoScale::from_raw(sum);
741        assert_eq!(result.to_string_with_scale(scale), "601.5");
742
743        // Min/Max
744        let values = [a.value(), b.value(), c.value()];
745        let min = *values.iter().min().unwrap();
746        let max = *values.iter().max().unwrap();
747        assert_eq!(
748            Decimal64NoScale::from_raw(min).to_string_with_scale(scale),
749            "100.5"
750        );
751        assert_eq!(
752            Decimal64NoScale::from_raw(max).to_string_with_scale(scale),
753            "300.75"
754        );
755    }
756
757    #[test]
758    fn test_special_values() {
759        let nan = Decimal64NoScale::nan();
760        assert!(nan.is_nan());
761        assert!(nan.is_special());
762        assert_eq!(nan.to_string_with_scale(2), "NaN");
763
764        let inf = Decimal64NoScale::infinity();
765        assert!(inf.is_pos_infinity());
766        assert_eq!(inf.to_string_with_scale(2), "Infinity");
767
768        let neg_inf = Decimal64NoScale::neg_infinity();
769        assert!(neg_inf.is_neg_infinity());
770        assert_eq!(neg_inf.to_string_with_scale(2), "-Infinity");
771    }
772
773    #[test]
774    fn test_ordering() {
775        let neg_inf = Decimal64NoScale::neg_infinity();
776        let neg = Decimal64NoScale::from_raw(-1000);
777        let zero = Decimal64NoScale::from_raw(0);
778        let pos = Decimal64NoScale::from_raw(1000);
779        let inf = Decimal64NoScale::infinity();
780        let nan = Decimal64NoScale::nan();
781
782        assert!(neg_inf < neg);
783        assert!(neg < zero);
784        assert!(zero < pos);
785        assert!(pos < inf);
786        assert!(inf < nan);
787    }
788
789    #[test]
790    fn test_from_str_special() {
791        assert!(Decimal64NoScale::new("Infinity", 0)
792            .unwrap()
793            .is_pos_infinity());
794        assert!(Decimal64NoScale::new("-Infinity", 0)
795            .unwrap()
796            .is_neg_infinity());
797        assert!(Decimal64NoScale::new("NaN", 0).unwrap().is_nan());
798    }
799
800    #[test]
801    fn test_roundtrip() {
802        let scale = 4;
803        let values = ["0", "123.4567", "-99.9999", "1000000", "-1"];
804
805        for s in values {
806            let d = Decimal64NoScale::new(s, scale).unwrap();
807            let raw = d.value();
808            let restored = Decimal64NoScale::from_raw(raw);
809            assert_eq!(d.value(), restored.value(), "Roundtrip failed for {}", s);
810        }
811    }
812
813    #[test]
814    fn test_byte_roundtrip() {
815        let d = Decimal64NoScale::new("123.45", 2).unwrap();
816        let bytes = d.to_be_bytes();
817        let restored = Decimal64NoScale::from_be_bytes(bytes);
818        assert_eq!(d, restored);
819    }
820
821    #[test]
822    fn test_zero() {
823        let d = Decimal64NoScale::new("0", 0).unwrap();
824        assert!(d.is_zero());
825        assert!(!d.is_negative());
826        assert!(!d.is_positive());
827        assert!(d.is_finite());
828    }
829
830    #[test]
831    fn test_negative_scale() {
832        let d = Decimal64NoScale::new("12345", -2).unwrap();
833        assert_eq!(d.to_string_with_scale(-2), "12300");
834    }
835
836    #[test]
837    fn test_max_precision() {
838        // Max value that fits
839        let max = Decimal64NoScale::max_value();
840        assert!(max.is_finite());
841        assert!(max.value() > 0);
842
843        // Min value that fits
844        let min = Decimal64NoScale::min_value();
845        assert!(min.is_finite());
846        assert!(min.value() < 0);
847    }
848}