From 5c986767bcd49d0b376753b2142b1910945f28d5 Mon Sep 17 00:00:00 2001 From: Nathan Royer <61582713+NathanRoyer@users.noreply.github.com> Date: Thu, 17 Dec 2020 09:49:27 +0100 Subject: [PATCH] Implement Number.prototype.toPrecision (#962) --- boa/src/builtins/number/mod.rs | 164 ++++++++++++++++++++++++++++--- boa/src/builtins/number/tests.rs | 41 +++++--- 2 files changed, 181 insertions(+), 24 deletions(-) diff --git a/boa/src/builtins/number/mod.rs b/boa/src/builtins/number/mod.rs index c1c19828d2..3b7c10e31a 100644 --- a/boa/src/builtins/number/mod.rs +++ b/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 { - 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 diff --git a/boa/src/builtins/number/tests.rs b/boa/src/builtins/number/tests.rs index b53ddf5ea0..24760223fd 100644 --- a/boa/src/builtins/number/tests.rs +++ b/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]