Skip to content

Commit

Permalink
Implement Number.prototype.toPrecision (#962)
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanRoyer authored Dec 17, 2020
1 parent 751a037 commit 5c98676
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 24 deletions.
164 changes: 152 additions & 12 deletions boa/src/builtins/number/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -277,25 +334,108 @@ 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(
this: &Value,
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
Expand Down
41 changes: 29 additions & 12 deletions boa/src/builtins/number/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit 5c98676

Please sign in to comment.