Skip to content

Commit

Permalink
Implement float parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
terrarier2111 committed Jan 27, 2024
1 parent 9a7f838 commit d16db64
Show file tree
Hide file tree
Showing 2 changed files with 220 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/bigint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ impl BigInt {

/// Implements addition of the 'rhs' sequence of words to this number.
#[allow(clippy::needless_range_loop)]
fn inplace_add_slice(&mut self, rhs: &[u64]) {
pub(crate) fn inplace_add_slice(&mut self, rhs: &[u64]) {
self.grow(rhs.len());
let mut carry: bool = false;
for i in 0..rhs.len() {
Expand Down
219 changes: 219 additions & 0 deletions src/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,207 @@ impl Display for Semantics {
}
}

#[cfg(feature = "std")]
mod from {
use core::fmt::{Debug, Display};
use std::error::Error;

use crate::{BigInt, Float, Semantics, FP64};

impl Float {
pub fn try_from_str(
value: &str,
sem: Semantics,
) -> Result<Self, ParseError> {
if value.is_empty() {
return Err(ParseError(ParseErrorKind::InputEmpty));
}

let chars = value.as_bytes();
let (sign, skip) = if chars[0] == '-' as u8 || chars[0] == '+' as u8
{
(chars[0] == '-' as u8, 1)
} else {
(false, 0)
};
let value = &value[skip..];

if value.eq_ignore_ascii_case("nan") {
return Ok(Self::nan(sem, sign));
}

if value.eq_ignore_ascii_case("inf") {
return Ok(Self::inf(sem, sign));
}

let l_r = value.split_once('.');
// handle cases where we have no `.` and as such no mantissa
if l_r.is_none() {
let ((num, _), exp_num) = parse_with_exp(value)?;
let mut num = Float::from_bigint(sem, num);
if let Some(exp) = exp_num {
if exp >= 0 {
num *= Float::from_bigint(
sem,
BigInt::from_u64(10).powi(exp as u64),
);
} else {
num /= Float::from_bigint(
sem,
BigInt::from_u64(10).powi((-exp) as u64),
);
}
}
num.set_sign(sign);
return Ok(num);
}
let (left, right) = l_r.unwrap();
// try parsing decimal value of 0 (as it has a different type than generic numbers)
if right
.chars()
.map(|chr| chr as u32 & !('0' as u32))
.sum::<u32>()
== 0
{
return parse_whole_num(left, sign, sem)
.map(|num| Ok(num))
.unwrap_or(Err(ParseError(
ParseErrorKind::ParsingNumberFailed,
)));
}
let left_num = parse_big_int(left).map(|num| Ok(num)).unwrap_or(
Err(ParseError(ParseErrorKind::ParsingNumberFailed)),
)?;
// parse the mantissa and an optional exponent part
let ((right_num, right_num_digits), exp_num) =
parse_with_exp(right)?;
// now construct the number from the info we parsed previously
let exp = BigInt::from_u64(10).powi(right_num_digits as u64);
let num = left_num * &exp + &right_num;

let mut ret =
Float::from_bigint(sem, num) / (Float::from_bigint(sem, exp));
if let Some(exp_num) = exp_num {
if exp_num >= 0 {
ret *= Float::from_bigint(
sem,
BigInt::from_u64(10).powi(exp_num as u64),
)
} else {
ret /= Float::from_bigint(
sem,
BigInt::from_u64(10).powi((-exp_num) as u64),
)
}
}
ret.set_sign(sign);
Ok(ret)
}
}

impl TryFrom<&str> for Float {
type Error = ParseError;

fn try_from(value: &str) -> Result<Self, Self::Error> {
const DEFAULT_SEM: Semantics = FP64;
// TODO: autodetect required semantics
Self::try_from_str(value, DEFAULT_SEM)
}
}

fn parse_with_exp(
value: &str,
) -> Result<((BigInt, usize), Option<i64>), ParseError> {
let idx = value.find(|c| c == 'e' || c == 'E');
let (num_raw, exp) = if let Some(idx) = idx {
let (l, r) = value.split_at(idx);
(l, Some(&r[1..]))
} else {
(value, None)
};

let num = parse_big_int(num_raw)
.map(|num| Ok((num, num_raw.len())))
.unwrap_or(Err(ParseError(ParseErrorKind::ParsingNumberFailed)))?;

if let Some(exp) = exp {
match exp.parse::<i64>() {
Ok(exp) => {
return Ok((num, Some(exp)));
}
Err(_) => {
return Err(ParseError(ParseErrorKind::ExponentParseFailed))
}
}
}
Ok((num, None))
}

fn parse_big_int(value: &str) -> Option<BigInt> {
let chars = value.as_bytes();
let ten = BigInt::from_u64(10);
let mut num = BigInt::from_u64(0);
for digit in chars.iter() {
if *digit > '9' as u8 || *digit < '0' as u8 {
return None;
}
let part = [*digit as u64 - '0' as u64];
num.inplace_mul(&ten);
num.inplace_add_slice(&part);
}
Some(num)
}

fn parse_whole_num(
value: &str,
sign: bool,
sem: Semantics,
) -> Option<Float> {
let chars = value.as_bytes();
if value.len() == 1 && chars[0] == '0' as u8 {
return Some(Float::zero(sem, sign));
}
let num = parse_big_int(value)?;

let mut ret = Float::from_bigint(sem, num);
ret.set_sign(sign);

return Some(ret);
}

enum ParseErrorKind {
InputEmpty,
ParsingNumberFailed,
ExponentParseFailed,
}

pub struct ParseError(ParseErrorKind);

impl Error for ParseError {}

impl Display for ParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self.0 {
ParseErrorKind::ParsingNumberFailed => f.write_str(
"Failed parsing number part of floating point number",
),
ParseErrorKind::ExponentParseFailed => {
f.write_str("Failed parsing exponent of float number")
}
ParseErrorKind::InputEmpty => {
f.write_str("The input provided was empty")
}
}
}
}

impl Debug for ParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Display::fmt(&self, f)
}
}
}

#[cfg(feature = "std")]
#[test]
fn test_convert_to_string() {
Expand Down Expand Up @@ -198,6 +399,24 @@ fn test_convert_to_string() {
assert_eq!("1995.1994999999999", to_str_w_fp64(1995.1995));
}

#[test]
fn test_from_string() {
assert_eq!("-3.", Float::try_from("-3.0").unwrap().to_string());
assert_eq!("30.", Float::try_from("30").unwrap().to_string());
assert_eq!("430.56", Float::try_from("430.56").unwrap().to_string());
assert_eq!("5.2", Float::try_from("5.2").unwrap().to_string());
assert_eq!("Inf", Float::try_from("inf").unwrap().to_string());
assert_eq!("NaN", Float::try_from("nan").unwrap().to_string());
assert_eq!("32.", Float::try_from("3.2e1").unwrap().to_string());
assert_eq!("4.4", Float::try_from("44.e-1").unwrap().to_string());
assert_eq!("5.4", Float::try_from("54e-1").unwrap().to_string());
assert_eq!("-5.485", Float::try_from("-54.85e-1").unwrap().to_string());
assert!(Float::try_from("abc.de").is_err());
assert!(Float::try_from("e.-21").is_err());
assert!(Float::try_from("-rlp.").is_err());
assert!(Float::try_from("").is_err());
}

#[test]
fn test_fuzz_printing() {
use crate::utils;
Expand Down

0 comments on commit d16db64

Please sign in to comment.