Skip to main content

icydb_schema/decimal/
text.rs

1//! Decimal text conversion; parsing borrows digits and float scratch stays on-stack.
2
3use crate::decimal::{
4    DECIMAL_DIGIT_BUFFER_LEN, Decimal, MAX_SUPPORTED_SCALE, ParseDecimalError,
5    ParseDecimalErrorReason,
6};
7use std::{
8    fmt::{Arguments, Display, Formatter},
9    io::Write,
10    str::FromStr,
11};
12
13impl Decimal {
14    // Only the f32/f64 constructors use this adapter. Their unpadded Display
15    // text has no redundant leading or fractional trailing zeros: an accepted
16    // value needs at most sign + 39 integer digits + dot + 28 fractional digits.
17    // Longer output already fails mantissa/scale admission. Keep the standard
18    // formatter and parser as authorities, without heap strings or a fallback.
19    pub(in crate::decimal) fn from_float_text(args: Arguments<'_>) -> Option<Self> {
20        const CAPACITY: usize = 1 + DECIMAL_DIGIT_BUFFER_LEN + 1 + MAX_SUPPORTED_SCALE as usize;
21        let mut bytes = [0; CAPACITY];
22        let mut remaining = bytes.as_mut_slice();
23        remaining.write_fmt(args).ok()?;
24        let len = CAPACITY - remaining.len();
25        Self::from_str(std::str::from_utf8(&bytes[..len]).ok()?).ok()
26    }
27}
28
29impl Display for Decimal {
30    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
31        let (mantissa, scale) = self.normalized_parts();
32
33        if mantissa == 0 {
34            return f.write_str("0");
35        }
36
37        let negative = mantissa.is_negative();
38        let mut digits = mantissa.unsigned_abs().to_string();
39
40        if scale == 0 {
41            if negative {
42                return write!(f, "-{digits}");
43            }
44
45            return f.write_str(&digits);
46        }
47
48        let scale_usize = usize::try_from(scale).map_err(|_| std::fmt::Error)?;
49
50        if digits.len() <= scale_usize {
51            let zeros = "0".repeat(scale_usize - digits.len());
52            let body = format!("0.{zeros}{digits}");
53            if negative {
54                write!(f, "-{body}")
55            } else {
56                f.write_str(&body)
57            }
58        } else {
59            let split = digits.len() - scale_usize;
60            let frac = digits.split_off(split);
61            if negative {
62                write!(f, "-{digits}.{frac}")
63            } else {
64                write!(f, "{digits}.{frac}")
65            }
66        }
67    }
68}
69
70impl FromStr for Decimal {
71    type Err = ParseDecimalError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        // Phase 1: parse sign.
75        let input = s.trim();
76        if input.is_empty() {
77            return Err(ParseDecimalError::new(ParseDecimalErrorReason::Empty));
78        }
79
80        let (negative, unsigned) = if let Some(rest) = input.strip_prefix('-') {
81            (true, rest)
82        } else if let Some(rest) = input.strip_prefix('+') {
83            (false, rest)
84        } else {
85            (false, input)
86        };
87
88        // Exponent notation is intentionally unsupported so decimal parsing
89        // retains one predictable textual form.
90        if unsigned.contains(['e', 'E']) {
91            return Err(ParseDecimalError::new(
92                ParseDecimalErrorReason::ExponentNotationUnsupported,
93            ));
94        }
95
96        // Phase 2: parse base-10 digits and decimal point.
97        let (int_digits, frac_digits) = split_decimal_significand(unsigned)?;
98        let scale_i64 = i64::try_from(frac_digits.len()).map_err(|_| {
99            ParseDecimalError::new(ParseDecimalErrorReason::FractionalLengthOverflow)
100        })?;
101
102        let scale = u32::try_from(scale_i64)
103            .map_err(|_| ParseDecimalError::new(ParseDecimalErrorReason::ScaleOverflow))?;
104
105        // Phase 3: accumulate already-validated digits without joining or
106        // copying them. Subtract negative digits so i128::MIN remains valid;
107        // leading zeros naturally leave the accumulator unchanged.
108        let mantissa = int_digits
109            .bytes()
110            .chain(frac_digits.bytes())
111            .try_fold(0i128, |mantissa, digit| {
112                let mantissa = mantissa.checked_mul(10)?;
113                let digit = i128::from(digit - b'0');
114                if negative {
115                    mantissa.checked_sub(digit)
116                } else {
117                    mantissa.checked_add(digit)
118                }
119            })
120            .ok_or_else(|| ParseDecimalError::new(ParseDecimalErrorReason::MantissaOverflow))?;
121
122        Self::checked_from_mantissa_scale(mantissa, scale).ok_or_else(|| {
123            ParseDecimalError::new(ParseDecimalErrorReason::ScaleExceedsSupportedRange)
124        })
125    }
126}
127
128fn split_decimal_significand(input: &str) -> Result<(&str, &str), ParseDecimalError> {
129    let mut segments = input.split('.');
130    let int_digits = segments
131        .next()
132        .ok_or_else(|| ParseDecimalError::new(ParseDecimalErrorReason::InvalidSignificand))?;
133    let frac_digits = segments.next().unwrap_or("");
134
135    if segments.next().is_some() {
136        return Err(ParseDecimalError::new(
137            ParseDecimalErrorReason::InvalidSignificand,
138        ));
139    }
140
141    if int_digits.is_empty() && frac_digits.is_empty() {
142        return Err(ParseDecimalError::new(
143            ParseDecimalErrorReason::InvalidSignificand,
144        ));
145    }
146
147    if !int_digits.chars().all(|c| c.is_ascii_digit()) {
148        return Err(ParseDecimalError::new(
149            ParseDecimalErrorReason::InvalidDigits,
150        ));
151    }
152
153    if !frac_digits.chars().all(|c| c.is_ascii_digit()) {
154        return Err(ParseDecimalError::new(
155            ParseDecimalErrorReason::InvalidDigits,
156        ));
157    }
158
159    Ok((int_digits, frac_digits))
160}