Skip to main content

icydb_schema/decimal/
text.rs

1use crate::decimal::{Decimal, ParseDecimalError, ParseDecimalErrorReason};
2use std::{
3    fmt::{Display, Formatter},
4    str::FromStr,
5};
6
7impl Display for Decimal {
8    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
9        let (mantissa, scale) = self.normalized_parts();
10
11        if mantissa == 0 {
12            return f.write_str("0");
13        }
14
15        let negative = mantissa.is_negative();
16        let mut digits = mantissa.unsigned_abs().to_string();
17
18        if scale == 0 {
19            if negative {
20                return write!(f, "-{digits}");
21            }
22
23            return f.write_str(&digits);
24        }
25
26        let scale_usize = usize::try_from(scale).map_err(|_| std::fmt::Error)?;
27
28        if digits.len() <= scale_usize {
29            let zeros = "0".repeat(scale_usize - digits.len());
30            let body = format!("0.{zeros}{digits}");
31            if negative {
32                write!(f, "-{body}")
33            } else {
34                f.write_str(&body)
35            }
36        } else {
37            let split = digits.len() - scale_usize;
38            let frac = digits.split_off(split);
39            if negative {
40                write!(f, "-{digits}.{frac}")
41            } else {
42                write!(f, "{digits}.{frac}")
43            }
44        }
45    }
46}
47
48impl FromStr for Decimal {
49    type Err = ParseDecimalError;
50
51    fn from_str(s: &str) -> Result<Self, Self::Err> {
52        // Phase 1: parse sign.
53        let input = s.trim();
54        if input.is_empty() {
55            return Err(ParseDecimalError::new(ParseDecimalErrorReason::Empty));
56        }
57
58        let (negative, unsigned) = if let Some(rest) = input.strip_prefix('-') {
59            (true, rest)
60        } else if let Some(rest) = input.strip_prefix('+') {
61            (false, rest)
62        } else {
63            (false, input)
64        };
65
66        // Exponent notation is intentionally unsupported for predictable decimal
67        // parsing semantics in 0.23.
68        if unsigned.contains(['e', 'E']) {
69            return Err(ParseDecimalError::new(
70                ParseDecimalErrorReason::ExponentNotationUnsupported,
71            ));
72        }
73
74        // Phase 2: parse base-10 digits and decimal point.
75        let (int_digits, frac_digits) = split_decimal_significand(unsigned)?;
76        let combined = format!("{int_digits}{frac_digits}");
77        let combined = strip_leading_zeros(&combined);
78
79        let scale_i64 = i64::try_from(frac_digits.len()).map_err(|_| {
80            ParseDecimalError::new(ParseDecimalErrorReason::FractionalLengthOverflow)
81        })?;
82        let digits = combined.to_string();
83
84        let scale = u32::try_from(scale_i64)
85            .map_err(|_| ParseDecimalError::new(ParseDecimalErrorReason::ScaleOverflow))?;
86
87        // Phase 3: materialize mantissa without floating-point fallback.
88        let signed_digits = if negative {
89            format!("-{digits}")
90        } else {
91            digits
92        };
93        let mantissa = signed_digits
94            .parse::<i128>()
95            .map_err(|_| ParseDecimalError::new(ParseDecimalErrorReason::MantissaOverflow))?;
96
97        Self::checked_from_mantissa_scale(mantissa, scale).ok_or_else(|| {
98            ParseDecimalError::new(ParseDecimalErrorReason::ScaleExceedsSupportedRange)
99        })
100    }
101}
102
103fn split_decimal_significand(input: &str) -> Result<(&str, &str), ParseDecimalError> {
104    let mut segments = input.split('.');
105    let int_digits = segments
106        .next()
107        .ok_or_else(|| ParseDecimalError::new(ParseDecimalErrorReason::InvalidSignificand))?;
108    let frac_digits = segments.next().unwrap_or("");
109
110    if segments.next().is_some() {
111        return Err(ParseDecimalError::new(
112            ParseDecimalErrorReason::InvalidSignificand,
113        ));
114    }
115
116    if int_digits.is_empty() && frac_digits.is_empty() {
117        return Err(ParseDecimalError::new(
118            ParseDecimalErrorReason::InvalidSignificand,
119        ));
120    }
121
122    if !int_digits.chars().all(|c| c.is_ascii_digit()) {
123        return Err(ParseDecimalError::new(
124            ParseDecimalErrorReason::InvalidDigits,
125        ));
126    }
127
128    if !frac_digits.chars().all(|c| c.is_ascii_digit()) {
129        return Err(ParseDecimalError::new(
130            ParseDecimalErrorReason::InvalidDigits,
131        ));
132    }
133
134    Ok((int_digits, frac_digits))
135}
136
137fn strip_leading_zeros(digits: &str) -> &str {
138    let trimmed = digits.trim_start_matches('0');
139    if trimmed.is_empty() { "0" } else { trimmed }
140}