Browse Source

Implement Number.prototype.toPrecision (#962)

pull/977/head
Nathan Royer 4 years ago committed by GitHub
parent
commit
5c986767bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 164
      boa/src/builtins/number/mod.rs
  2. 41
      boa/src/builtins/number/tests.rs

164
boa/src/builtins/number/mod.rs

@ -18,7 +18,7 @@ use crate::{
builtins::BuiltIn,
object::{ConstructorBuilder, ObjectData},
property::Attribute,
value::{AbstractRelation, Value},
value::{AbstractRelation, IntegerOrInfinity, Value},
BoaProfiler, Context, Result,
};
use num_traits::float::FloatCore;
@ -269,6 +269,63 @@ impl Number {
Ok(Value::from(this_str_num))
}
/// flt_str_to_exp - used in to_precision
///
/// This function traverses a string representing a number,
/// returning the floored log10 of this number.
///
fn flt_str_to_exp(flt: &str) -> i32 {
let mut non_zero_encountered = false;
let mut dot_encountered = false;
for (i, c) in flt.chars().enumerate() {
if c == '.' {
if non_zero_encountered {
return (i as i32) - 1;
}
dot_encountered = true;
} else if c != '0' {
if dot_encountered {
return 1 - (i as i32);
}
non_zero_encountered = true;
}
}
(flt.len() as i32) - 1
}
/// round_to_precision - used in to_precision
///
/// This procedure has two roles:
/// - If there are enough or more than enough digits in the
/// string to show the required precision, the number
/// represented by these digits is rounded using string
/// manipulation.
/// - Else, zeroes are appended to the string.
///
/// When this procedure returns, `digits` is exactly `precision` long.
///
fn round_to_precision(digits: &mut String, precision: usize) {
if digits.len() > precision {
let to_round = digits.split_off(precision);
let mut digit = digits.pop().unwrap() as u8;
for c in to_round.chars() {
match c {
c if c < '4' => break,
c if c > '4' => {
digit += 1;
break;
}
_ => {}
}
}
digits.push(digit as char);
} else {
digits.push_str(&"0".repeat(precision - digits.len()));
}
}
/// `Number.prototype.toPrecision( [precision] )`
///
/// The `toPrecision()` method returns a string representing the Number object to the specified precision.
@ -277,7 +334,7 @@ impl Number {
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toexponential
/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toprecision
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision
#[allow(clippy::wrong_self_convention)]
pub(crate) fn to_precision(
@ -285,17 +342,100 @@ impl Number {
args: &[Value],
context: &mut Context,
) -> Result<Value> {
let this_num = Self::this_number_value(this, context)?;
let _num_str_len = format!("{}", this_num).len();
let _precision = match args.get(0) {
Some(n) => match n.to_integer(context)? as i32 {
x if x > 0 => n.to_integer(context)? as usize,
_ => 0,
},
None => 0,
let precision_var = args.get(0).cloned().unwrap_or_default();
// 1 & 6
let mut this_num = Self::this_number_value(this, context)?;
// 2 & 4
if precision_var == Value::undefined() || !this_num.is_finite() {
return Self::to_string(this, &[], context);
}
// 3
let precision = match precision_var.to_integer_or_infinity(context)? {
IntegerOrInfinity::Integer(x) if (1..=100).contains(&x) => x as usize,
_ => {
// 5
return context.throw_range_error(
"precision must be an integer at least 1 and no greater than 100",
);
}
};
// TODO: Implement toPrecision
unimplemented!("TODO: Implement toPrecision");
let precision_i32 = precision as i32;
// 7
let mut prefix = String::new(); // spec: 's'
let mut suffix: String; // spec: 'm'
let exponent: i32; // spec: 'e'
// 8
if this_num < 0.0 {
prefix.push('-');
this_num = -this_num;
}
// 9
if this_num == 0.0 {
suffix = "0".repeat(precision);
exponent = 0;
// 10
} else {
// Due to f64 limitations, this part differs a bit from the spec,
// but has the same effect. It manipulates the string constructed
// by ryu-js: digits with an optional dot between two of them.
let mut buffer = ryu_js::Buffer::new();
suffix = buffer.format(this_num).to_string();
// a: getting an exponent
exponent = Self::flt_str_to_exp(&suffix);
// b: getting relevant digits only
if exponent < 0 {
suffix = suffix.split_off((1 - exponent) as usize);
} else if let Some(n) = suffix.find('.') {
suffix.remove(n);
}
// impl: having exactly `precision` digits in `suffix`
Self::round_to_precision(&mut suffix, precision);
// c: switching to scientific notation
let great_exp = exponent >= precision_i32;
if exponent < -6 || great_exp {
// ii
if precision > 1 {
suffix.insert(1, '.');
}
// vi
suffix.push('e');
// iii
if great_exp {
suffix.push('+');
}
// iv, v
suffix.push_str(&exponent.to_string());
return Ok(Value::from(prefix + &suffix));
}
}
// 11
let e_inc = exponent + 1;
if e_inc == precision_i32 {
return Ok(Value::from(prefix + &suffix));
}
// 12
if exponent >= 0 {
suffix.insert(e_inc as usize, '.');
// 13
} else {
prefix.push('0');
prefix.push('.');
prefix.push_str(&"0".repeat(-e_inc as usize));
}
// 14
Ok(Value::from(prefix + &suffix))
}
// https://golang.org/src/math/nextafter.go

41
boa/src/builtins/number/tests.rs

@ -126,35 +126,52 @@ fn to_locale_string() {
}
#[test]
#[ignore]
fn to_precision() {
let mut context = Context::new();
let init = r#"
var infinity = (1/0).toPrecision(3);
var default_precision = Number().toPrecision();
var low_precision = Number(123456789).toPrecision(1);
var more_precision = Number(123456789).toPrecision(4);
var exact_precision = Number(123456789).toPrecision(9);
var over_precision = Number(123456789).toPrecision(50);
var neg_precision = Number(-123456789).toPrecision(4);
var explicit_ud_precision = Number().toPrecision(undefined);
var low_precision = (123456789).toPrecision(1);
var more_precision = (123456789).toPrecision(4);
var exact_precision = (123456789).toPrecision(9);
var over_precision = (123456789).toPrecision(50);
var neg_precision = (-123456789).toPrecision(4);
"#;
eprintln!("{}", forward(&mut context, init));
let infinity = forward(&mut context, "infinity");
let default_precision = forward(&mut context, "default_precision");
let explicit_ud_precision = forward(&mut context, "explicit_ud_precision");
let low_precision = forward(&mut context, "low_precision");
let more_precision = forward(&mut context, "more_precision");
let exact_precision = forward(&mut context, "exact_precision");
let over_precision = forward(&mut context, "over_precision");
let neg_precision = forward(&mut context, "neg_precision");
assert_eq!(default_precision, String::from("0"));
assert_eq!(low_precision, String::from("1e+8"));
assert_eq!(more_precision, String::from("1.235e+8"));
assert_eq!(exact_precision, String::from("123456789"));
assert_eq!(infinity, String::from("\"Infinity\""));
assert_eq!(default_precision, String::from("\"0\""));
assert_eq!(explicit_ud_precision, String::from("\"0\""));
assert_eq!(low_precision, String::from("\"1e+8\""));
assert_eq!(more_precision, String::from("\"1.235e+8\""));
assert_eq!(exact_precision, String::from("\"123456789\""));
assert_eq!(neg_precision, String::from("\"-1.235e+8\""));
assert_eq!(
over_precision,
String::from("123456789.00000000000000000000000000000000000000000")
String::from("\"123456789.00000000000000000000000000000000000000000\"")
);
assert_eq!(neg_precision, String::from("-1.235e+8"));
let expected = "Uncaught \"RangeError\": \"precision must be an integer at least 1 and no greater than 100\"";
let range_error_1 = r#"(1).toPrecision(101);"#;
let range_error_2 = r#"(1).toPrecision(0);"#;
let range_error_3 = r#"(1).toPrecision(-2000);"#;
let range_error_4 = r#"(1).toPrecision('%');"#;
assert_eq!(forward(&mut context, range_error_1), expected);
assert_eq!(forward(&mut context, range_error_2), expected);
assert_eq!(forward(&mut context, range_error_3), expected);
assert_eq!(forward(&mut context, range_error_4), expected);
}
#[test]

Loading…
Cancel
Save