Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Number.toExponential and Number.toFixed #1620

Merged
merged 6 commits into from
Oct 5, 2021
Merged
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
81 changes: 66 additions & 15 deletions boa/src/builtins/number/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,6 @@ impl Number {
Err(context.construct_type_error("'this' is not a number"))
}

/// Helper function that formats a float as a ES6-style exponential number string.
fn num_to_exponential(n: f64) -> String {
match n.abs() {
x if x > 1.0 => format!("{:e}", n).replace("e", "e+"),
x if x == 0.0 => format!("{:e}", n).replace("e", "e+"),
_ => format!("{:e}", n),
}
}

/// `Number.prototype.toExponential( [fractionDigits] )`
///
/// The `toExponential()` method returns a string representing the Number object in exponential notation.
Expand All @@ -227,11 +218,32 @@ impl Number {
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_exponential(
this: &JsValue,
_: &[JsValue],
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let x be ? thisNumberValue(this value).
let this_num = Self::this_number_value(this, context)?;
let this_str_num = Self::num_to_exponential(this_num);
let precision = match args.get(0) {
None | Some(JsValue::Undefined) => None,
// 2. Let f be ? ToIntegerOrInfinity(fractionDigits).
Some(n) => Some(n.to_integer(context)? as i32),
};
// 4. If x is not finite, return ! Number::toString(x).
if !this_num.is_finite() {
return Ok(JsValue::new(Self::to_native_string(this_num)));
}
// Get rid of the '-' sign for -0.0
let this_num = if this_num == 0. { 0. } else { this_num };
let this_str_num = if let Some(precision) = precision {
// 5. If f < 0 or f > 100, throw a RangeError exception.
if !(0..=100).contains(&precision) {
return Err(context
.construct_range_error("toExponential() argument must be between 0 and 100"));
}
f64_to_exponential_with_precision(this_num, precision as usize)
} else {
f64_to_exponential(this_num)
};
Ok(JsValue::new(this_str_num))
}

Expand All @@ -251,16 +263,34 @@ impl Number {
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
// 1. Let this_num be ? thisNumberValue(this value).
let this_num = Self::this_number_value(this, context)?;
let precision = match args.get(0) {
// 2. Let f be ? ToIntegerOrInfinity(fractionDigits).
Some(n) => match n.to_integer(context)? as i32 {
x if x > 0 => n.to_integer(context)? as usize,
_ => 0,
0..=100 => n.to_integer(context)? as usize,
// 4, 5. If f < 0 or f > 100, throw a RangeError exception.
_ => {
return Err(context.construct_range_error(
"toFixed() digits argument must be between 0 and 100",
))
}
},
// 3. If fractionDigits is undefined, then f is 0.
None => 0,
};
let this_fixed_num = format!("{:.*}", precision, this_num);
Ok(JsValue::new(this_fixed_num))
// 6. If x is not finite, return ! Number::toString(x).
if !this_num.is_finite() {
Ok(JsValue::new(Self::to_native_string(this_num)))
// 10. If x ≥ 10^21, then let m be ! ToString(𝔽(x)).
} else if this_num >= 1.0e21 {
Ok(JsValue::new(f64_to_exponential(this_num)))
} else {
// Get rid of the '-' sign for -0.0 because of 9. If x < 0, then set s to "-".
let this_num = if this_num == 0. { 0. } else { this_num };
let this_fixed_num = format!("{:.*}", precision, this_num);
Ok(JsValue::new(this_fixed_num))
}
}

/// `Number.prototype.toLocaleString( [locales [, options]] )`
Expand Down Expand Up @@ -1124,3 +1154,24 @@ impl Number {
!x
}
}

/// Helper function that formats a float as a ES6-style exponential number string.
fn f64_to_exponential(n: f64) -> String {
match n.abs() {
x if x >= 1.0 || x == 0.0 => format!("{:e}", n).replace("e", "e+"),
_ => format!("{:e}", n),
}
}

/// Helper function that formats a float as a ES6-style exponential number string with a given precision.
// We can't use the same approach as in `f64_to_exponential`
// because in cases like (0.999).toExponential(0) the result will be 1e0.
// Instead we get the index of 'e', and if the next character is not '-' we insert the plus sign
fn f64_to_exponential_with_precision(n: f64, prec: usize) -> String {
let mut res = format!("{:.*e}", prec, n);
let idx = res.find('e').expect("'e' not found in exponential string");
if res.as_bytes()[idx + 1] != b'-' {
res.insert(idx + 1, '+');
}
res
}