dashu_float/parse.rs
1use crate::{
2 fbig::FBig,
3 repr::{Context, Repr, Word},
4 round::Round,
5};
6use core::{num::IntErrorKind, str::FromStr};
7use dashu_base::{ParseError, Sign};
8use dashu_int::{
9 fmt::{MAX_RADIX, MIN_RADIX},
10 UBig,
11};
12
13impl<const B: Word> Repr<B> {
14 /// Convert a string in the native base (i.e. radix `B`) to [Repr].
15 ///
16 /// Upon success, this method returns an [Repr] and the number of digits (in radix `B`)
17 /// contained in the string.
18 ///
19 /// This method is the underlying implementation of the [`FromStr`] impl on
20 /// [`FBig`]; see those docs for the accepted formats.
21 pub(crate) fn from_str_native(mut src: &str) -> Result<(Self, usize), ParseError> {
22 assert!(MIN_RADIX as Word <= B && B <= MAX_RADIX as Word);
23
24 // B is guaranteed to be in 2..=36 by the assert above; the cast to u32
25 // is needed because `from_str_radix` takes a u32 radix. On 32-bit Word
26 // targets the cast is a no-op.
27 #[allow(clippy::unnecessary_cast)]
28 let radix: u32 = B as u32;
29
30 // parse and remove the sign
31 let sign = match src.strip_prefix('-') {
32 Some(s) => {
33 src = s;
34 Sign::Negative
35 }
36 None => {
37 src = src.strip_prefix('+').unwrap_or(src);
38 Sign::Positive
39 }
40 };
41
42 // determine the position of scale markers
43 let has_prefix = src.starts_with("0x") || src.starts_with("0X");
44 let scale_pos = match B {
45 10 => src.rfind(['e', 'E', '@']),
46 2 => {
47 if has_prefix {
48 src.rfind(['p', 'P', '@'])
49 } else {
50 src.rfind(['b', 'B', '@'])
51 }
52 }
53 8 => src.rfind(['o', 'O', '@']),
54 16 => src.rfind(['h', 'H', '@']),
55 _ => src.rfind('@'),
56 };
57
58 // parse scale and remove the scale part from the str
59 let (scale, pmarker) = if let Some(pos) = scale_pos {
60 let value = match src[pos + 1..].parse::<isize>() {
61 Err(e) => match e.kind() {
62 IntErrorKind::Empty => return Err(ParseError::NoDigits),
63 _ => return Err(ParseError::InvalidDigit),
64 },
65 Ok(v) => v,
66 };
67 let use_p = if B == 2 {
68 src.as_bytes().get(pos) == Some(&b'p') || src.as_bytes().get(pos) == Some(&b'P')
69 } else {
70 false
71 };
72 src = &src[..pos];
73 (value, use_p)
74 } else {
75 (0, false)
76 };
77
78 // parse the body of the float number
79 let mut exponent = scale;
80 let ndigits;
81 let significand = if let Some(dot) = src.find('.') {
82 // check whether both integral part and fractional part are empty
83 if src.len() == 1 {
84 return Err(ParseError::NoDigits);
85 }
86
87 // parse integral part
88 let (int, int_digits, base) = if dot != 0 {
89 let int_str = &src[..dot];
90 if B == 2 && has_prefix {
91 // only base 2 float is allowed using prefix
92 let int_str = &int_str[2..];
93 let digits = 4 * (int_str.len() - int_str.matches('_').count());
94 if int_str.is_empty() {
95 (UBig::ZERO, digits, 16)
96 } else {
97 (UBig::from_str_radix(int_str, 16)?, digits, 16)
98 }
99 } else if B == 2 && pmarker && !has_prefix {
100 return Err(ParseError::UnsupportedRadix);
101 } else {
102 let digits = int_str.len() - int_str.matches('_').count();
103 (UBig::from_str_radix(&src[..dot], radix)?, digits, radix)
104 }
105 } else {
106 if pmarker {
107 // prefix is required for using `p` as scale marker
108 return Err(ParseError::UnsupportedRadix);
109 }
110 (UBig::ZERO, 0, radix)
111 };
112
113 // parse fractional part
114 src = &src[dot + 1..];
115 let (fract, fract_digits) = if !src.is_empty() {
116 let mut digits = src.len() - src.matches('_').count();
117 if B == 2 && base == 16 {
118 digits *= 4;
119 }
120 (UBig::from_str_radix(src, base)?, digits)
121 } else {
122 (UBig::ZERO, 0)
123 };
124 ndigits = int_digits + fract_digits;
125
126 if fract.is_zero() {
127 int
128 } else {
129 exponent -= fract_digits as isize;
130 int * UBig::from_word(B).pow(fract_digits) + fract
131 }
132 } else {
133 let has_prefix = src.starts_with("0x") || src.starts_with("0X");
134 if B == 2 && has_prefix {
135 src = &src[2..];
136 ndigits = 4 * (src.len() - src.matches('_').count());
137 UBig::from_str_radix(src, 16)?
138 } else if B == 2 && pmarker && !has_prefix {
139 return Err(ParseError::UnsupportedRadix);
140 } else {
141 ndigits = src.len() - src.matches('_').count();
142 UBig::from_str_radix(src, radix)?
143 }
144 };
145
146 // A zero significand carries its sign in the exponent sentinel (`-0` is exponent -1), and
147 // `sign * significand` cannot express it — negating an `IBig` zero yields zero. Without
148 // this, `-0`, `-0.0` and `-0e0` all parsed as `+0` and the value did not survive its own
149 // `Display` round-trip.
150 let repr = if sign == Sign::Negative && significand.is_zero() {
151 Repr::neg_zero()
152 } else {
153 Repr::new(sign * significand, exponent)
154 };
155 Ok((repr, ndigits))
156 }
157}
158
159impl<R: Round, const B: Word> FromStr for FBig<R, B> {
160 type Err = ParseError;
161
162 /// Convert a string in the native base (i.e. radix `B`) to [`FBig`].
163 ///
164 /// If parsing succeeds, the result is **losslessly** parsed from the input
165 /// string, and its precision equals the number of significant digits in the
166 /// input.
167 ///
168 /// **Note**: Infinites are **intentionally not supported** by this function.
169 ///
170 /// # Format
171 ///
172 /// The valid representations include
173 /// 1. `aaa` or `aaa.`
174 /// * `aaa` is represented in native base `B` without base prefixes.
175 /// 1. `aaa.bbb` = `aaabbb / base ^ len(bbb)`
176 /// * `aaa` and `bbb` are represented in native base `B` without base prefixes.
177 /// * `len(bbb)` represents the number of digits in `bbb`, e.g `len(bbb)` is 3. (Same below)
178 /// 1. `aaa.bbb@cc` = `aaabbb * base ^ (cc - len(bbb))`
179 /// * `aaa` and `bbb` are represented in native base `B`
180 /// * This is consistent with the representation used by [GNU GMP](https://gmplib.org/manual/I_002fO-of-Floats).
181 /// 1. `aaa.bbbEcc` = `aaabbb * 10 ^ (cc - len(bbb))`
182 /// * `E` could be lower case, base `B` must be 10
183 /// * `aaa` and `bbb` are all represented in decimal
184 /// 1. `0xaaa` or `0xaaa`
185 /// 1. `0xaaa.bbb` = `0xaaabbb / 16 ^ len(bbb)`
186 /// 1. `0xaaa.bbbPcc` = `0xaaabbb / 16 ^ len(bbb) * 2 ^ cc`
187 /// * `P` could be lower case, base `B` must be 2 (not 16!)
188 /// * `aaa` and `bbb` are represented in hexadecimal
189 /// * This is consistent with the [C++ hexadecimal literals](https://en.cppreference.com/w/cpp/language/floating_literal).
190 /// 1. `aaa.bbbBcc` = `aaabbb * 2 ^ (cc - len(bbb))`
191 /// 1. `aaa.bbbOcc` = `aaabbb * 8 ^ (cc - len(bbb))`
192 /// 1. `aaa.bbbHcc` = `aaabbb * 16 ^ (cc - len(bbb))`
193 /// * `B`/`O`/`H` could be lower case, and base `B` must be consistent with the marker.
194 /// * `aaa` and `bbb` are represented in binary/octal/hexadecimal correspondingly without prefix.
195 /// * This is consistent with some scientific notations described in [Wikipedia](https://en.wikipedia.org/wiki/Scientific_notation#Other_bases).
196 ///
197 /// Digits 10-35 are represented by `a-z` or `A-Z`.
198 ///
199 /// Literal `aaa` and `cc` above can be signed, but `bbb` must be unsigned.
200 /// All `cc` are represented in decimal. Either `aaa` or `bbb` can be omitted
201 /// when its value is zero, but they are not allowed to be omitted at the same time.
202 ///
203 /// # Precision
204 ///
205 /// The precision of the parsed number is determined by the number of digits that are presented
206 /// in the input string. For example, the numbers parsed from `12.34` or `1.234e-1` will have a
207 /// precision of 4, while the ones parsed from `12.34000` or `00012.34` will have a precision of 7.
208 ///
209 /// # Panics
210 ///
211 /// Panics if the base `B` is not between [MIN_RADIX] and [MAX_RADIX] inclusive.
212 ///
213 /// # Examples
214 ///
215 /// ```
216 /// # use dashu_base::ParseError;
217 /// # use dashu_float::DBig;
218 /// # use core::str::FromStr;
219 /// let a = DBig::from_str("-1.23400e-3")?;
220 /// let b = DBig::from_str("-123.4@-05")?;
221 /// assert_eq!(a, b);
222 /// assert_eq!(a.precision(), 6);
223 /// assert_eq!(b.precision(), 4);
224 ///
225 /// assert!(DBig::from_str("-0x1.234p-3").is_err());
226 /// assert!(DBig::from_str("-1.234H-3").is_err());
227 /// # Ok::<(), ParseError>(())
228 /// ```
229 #[inline]
230 fn from_str(s: &str) -> Result<Self, ParseError> {
231 let (repr, ndigits) = Repr::from_str_native(s)?;
232 Ok(Self {
233 repr,
234 context: Context::new(ndigits),
235 })
236 }
237}