Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 9 additions & 29 deletions strconv/double.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -68,38 +68,18 @@ let max_mantissa_fast_path : UInt64 = 2UL << mantissa_explicit_bits
/// An exponent value exp scales the mantissa (significand) by 10^exp.
/// For example, "1.23e2" represents 1.23 × 10² = 123.
pub fn parse_double(str : StringView) -> Double raise StrConvError {
if str.length() == 0 {
syntax_err()
}
if !check_underscore(str) {
syntax_err()
}
guard str.length() > 0 else { syntax_err() }
guard check_underscore(str) else { syntax_err() }
// validate its a number
let (num, consumed) = match parse_number(str) {
Some(r) => r
None =>
match parse_inf_nan(str) {
Some((num, consumed)) =>
if str.length() != consumed {
syntax_err()
} else {
return num
}
None => syntax_err()
match parse_number(str) {
None => parse_inf_nan(str)
Some(num) =>
// Clinger's fast path (How to read floating point numbers accurately)[https://doi.org/10.1145/989393.989430]
match num.try_fast_path() {
Some(value) => value
None => parse_decimal_priv(str).to_double_priv() // fallback to slow path
}
}
if str.length() != consumed {
syntax_err()
}
// Clinger's fast path (How to read floating point numbers accurately)[https://doi.org/10.1145/989393.989430]
match num.try_fast_path() {
Some(value) => value
None => {
// fallback to slow path
let ret = parse_decimal_priv(str)
ret.to_double_priv()
}
}
}

///|
Expand Down
88 changes: 38 additions & 50 deletions strconv/number.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ fn try_parse_19digits(s : StringView, x : UInt64) -> (StringView, UInt64, Int) {

///|
fn parse_scientific(s : StringView) -> (StringView, Int64)? {
// the first character is 'e'/'E' and scientific mode is enabled
let mut s = match s.step(1) {
Some(s) => s
None => return None
}
let mut s = s
let exp_num = 0L
let mut neg_exp = false
if s is ['+' | '-' as ch, .. rest] {
Expand All @@ -77,18 +73,14 @@ fn parse_scientific(s : StringView) -> (StringView, Int64)? {
}

///|
/// Parse the number from the string, returning the number and the length of the parsed string.
fn parse_number(s : StringView) -> (Number, Int)? {
let mut s = s
/// Parse the number from the string, raising StrConvError if invalid.
fn parse_number(s : StringView) -> Number? raise StrConvError {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please state the motivation for the PR and update the corresponding comments in the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

let start = s

// handle optional +/- sign
let mut negative = false
if s is ['-', .. rest] {
negative = true
s = rest
} else if s is ['+', .. rest] {
s = rest
let (s, negative) = match s {
['-', .. rest] => (rest, true)
['+', .. rest] | rest => (rest, false)
}
if s.is_empty() {
return None
Expand Down Expand Up @@ -121,26 +113,28 @@ fn parse_number(s : StringView) -> (Number, Int)? {

// handle scientific format
let exp_number = 0L
if s is ['e' | 'E', ..] {
let (new_s, exp_number) = match parse_scientific(s) {
if s is ['e' | 'E', .. rest] {
let (new_s, exp_number) = match parse_scientific(rest) {
Some(res) => res
None => return None
}
s = new_s
exponent += exp_number
}
let len = start.length() - s.length()
guard s is "" else { syntax_err() }

// handle uncommon case with many digits
if n_digits <= 19 {
return Some(({ exponent, mantissa, negative, many_digits: false }, len))
return Some({ exponent, mantissa, negative, many_digits: false })
}
n_digits -= 19
let mut many_digits = false
let mut p = start
while p is ['0' | '.' as ch, .. rest] {
n_digits -= (ch.to_int() - 46) / 2 // '0' = b'.' + 2
p = rest
loop start {
['0' | '.' as ch, .. rest] => {
n_digits -= (ch.to_int() - 46) / 2 // '0' = b'.' + 2
continue rest
}
_ => ()
}
let mut mantissa = mantissa
if n_digits > 0 {
Expand All @@ -153,44 +147,38 @@ fn parse_number(s : StringView) -> (Number, Int)? {
exponent = (if mantissa >= min_19digit_int {
consumed_digit // big int
} else {
let s = match s.step(1) {
Some(s) => s
None => return None
} // fractional component, skip the '.'
// fractional component, skip the '.'
guard s is [_, .. s] else { return None }
let (_, new_mantissa, consumed_digit) = try_parse_19digits(s, mantissa)
mantissa = new_mantissa
consumed_digit
}).to_int64()
exponent += exp_number
} // add back the explicit part
Some(({ exponent, mantissa, negative, many_digits }, len))
Some({ exponent, mantissa, negative, many_digits })
}

///|
/// Parse the number from the string, returning the number and the length of the parsed string.
fn parse_inf_nan(s : StringView) -> (Double, Int)? {
let mut s = s
let mut pos = true
let mut len = 0
if s is ['-' | '+' as ch, .. rest] {
pos = ch == '+'
s = rest
len += 1
}
if s is ['n' | 'N', 'a' | 'A', 'n' | 'N', ..] {
Some((@double.not_a_number, len + 3))
} else if s is ['i' | 'I', 'n' | 'N', 'f' | 'F', .. rest] {
len += 3
if rest is ['i' | 'I', 'n' | 'N', 'i' | 'I', 't' | 'T', 'y' | 'Y', ..] {
len += 5
/// Parse the number from the string, raising `StrConvError` if invalid.
fn parse_inf_nan(rest : StringView) -> Double raise StrConvError {
let (pos, rest) = match rest {
['-', .. rest] => (false, rest)
['+', .. rest] | rest => (true, rest)
}
match rest {
['n' | 'N', 'a' | 'A', 'n' | 'N'] => @double.not_a_number
['i' | 'I', 'n' | 'N', 'f' | 'F', .. rest] => {
guard rest
is ([] | ['i' | 'I', 'n' | 'N', 'i' | 'I', 't' | 'T', 'y' | 'Y']) else {
syntax_err()
}
if pos {
@double.infinity
} else {
@double.neg_infinity
}
}
if pos {
Some((@double.infinity, len))
} else {
Some((@double.neg_infinity, len))
}
} else {
None
_ => syntax_err()
}
}

Expand Down
124 changes: 15 additions & 109 deletions strconv/number_wbtest.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -14,136 +14,42 @@

///|
test "parse_inf_nan basic cases" {
// Test basic NaN parsing
let result_nan = parse_inf_nan("nan")
match result_nan {
Some((value, length)) => {
inspect(value.is_nan(), content="true")
inspect(length, content="3")
}
None => fail("Expected to parse 'nan'")
}

// Test basic infinity parsing
let result_inf = parse_inf_nan("inf")
match result_inf {
Some((value, length)) => {
inspect(value.is_inf() && value > 0.0, content="true")
inspect(length, content="3")
}
None => fail("Expected to parse 'inf'")
}
inspect(parse_inf_nan("nan"), content="NaN")
inspect(parse_inf_nan("inf"), content="Infinity")
}

///|
test "parse_inf_nan with signs" {
// Test positive signed NaN
let result_pos_nan = parse_inf_nan("+nan")
match result_pos_nan {
Some((value, length)) => {
inspect(value.is_nan(), content="true")
inspect(length, content="4")
}
None => fail("Expected to parse '+nan'")
}

// Test negative signed infinity
let result_neg_inf = parse_inf_nan("-inf")
match result_neg_inf {
Some((value, length)) => {
inspect(value.is_inf() && value < 0.0, content="true")
inspect(length, content="4")
}
None => fail("Expected to parse '-inf'")
}
inspect(parse_inf_nan("+nan"), content="NaN")
inspect(parse_inf_nan("-inf"), content="-Infinity")
}

///|
test "parse_inf_nan case insensitive" {
// Test uppercase
let result_uppercase = parse_inf_nan("NAN")
match result_uppercase {
Some((value, length)) => {
inspect(value.is_nan(), content="true")
inspect(length, content="3")
}
None => fail("Expected to parse 'NAN'")
}

// Test mixed case
let result_mixed = parse_inf_nan("InF")
match result_mixed {
Some((value, length)) => {
inspect(value.is_inf() && value > 0.0, content="true")
inspect(length, content="3")
}
None => fail("Expected to parse 'InF'")
}
inspect(parse_inf_nan("NAN"), content="NaN")
inspect(parse_inf_nan("InF"), content="Infinity")
}

///|
test "parse_inf_nan with infinity suffix" {
// Test full infinity word
let result_infinity = parse_inf_nan("infinity")
match result_infinity {
Some((value, length)) => {
inspect(value.is_inf() && value > 0.0, content="true")
inspect(length, content="8")
}
None => fail("Expected to parse 'infinity'")
}

// Test signed full infinity
let result_neg_infinity = parse_inf_nan("-INFINITY")
match result_neg_infinity {
Some((value, length)) => {
inspect(value.is_inf() && value < 0.0, content="true")
inspect(length, content="9")
}
None => fail("Expected to parse '-INFINITY'")
}
inspect(parse_inf_nan("infinity"), content="Infinity")
inspect(parse_inf_nan("-INFINITY"), content="-Infinity")
}

///|
test "parse_inf_nan with trailing strings" {
// Test trailing characters after 'inf'
let result_trailing = parse_inf_nan("infabc")
match result_trailing {
Some((value, length)) => {
inspect(value.is_inf() && value > 0.0, content="true")
inspect(length, content="3") // should only parse 'inf'
}
None => fail("Expected to parse 'inf' but got None")
}

// Test trailing characters after 'nan'
let result_nan_trailing = parse_inf_nan("nanxyz")
match result_nan_trailing {
Some((value, length)) => {
inspect(value.is_nan(), content="true")
inspect(length, content="3") // should only parse 'nan'
}
None => fail("Expected to parse 'nan' but got None")
}

// Test trailing characters after signed infinity
let result_signed_trailing = parse_inf_nan("+infinity123")
match result_signed_trailing {
Some((value, length)) => {
inspect(value.is_inf() && value > 0.0, content="true")
inspect(length, content="9") // should only parse '+infinity'
}
None => fail("Expected to parse '+infinity' but got None")
}
inspect(try? parse_inf_nan("infabc"), content="Err(invalid syntax)")
inspect(try? parse_inf_nan("nanxyz"), content="Err(invalid syntax)")
inspect(try? parse_inf_nan("+infinity123"), content="Err(invalid syntax)")
}

///|
test "parse_inf_nan failures" {
// Test invalid input
inspect(parse_inf_nan("hello"), content="None")
inspect(parse_inf_nan(""), content="None")
inspect(parse_inf_nan("in"), content="None")
inspect(parse_inf_nan("na"), content="None")
inspect(try? parse_inf_nan("hello"), content="Err(invalid syntax)")
inspect(try? parse_inf_nan(""), content="Err(invalid syntax)")
inspect(try? parse_inf_nan("in"), content="Err(invalid syntax)")
inspect(try? parse_inf_nan("na"), content="Err(invalid syntax)")
}

///|
Expand Down
19 changes: 0 additions & 19 deletions strconv/string_view.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Step over `step` non-underscore characters.
/// Return None if there are not enough characters.
fn StringView::step(self : Self, step : Int) -> Self? {
let mut step = step
let mut str = self
while str is [ch, .. rest] && step > 0 {
if ch != '_' {
step -= 1
}
str = rest
}
if step == 0 {
Some(str)
} else {
None
}
}

///|
/// Returns the accumulated value, the slice left, and the number of digits consumed.
/// It ignores underscore and stops when a non-digit character is found.
Expand Down
Loading