Skip to content

Commit

Permalink
Implement Number.prototype.toFixed() (#35)
Browse files Browse the repository at this point in the history
* Implement draft `Number.prototype.toFixed()`

* Add more tests

* Some code clean up

* Some more code cleanup and add test

* Some more code cleanup and add test

* Reduce POW10_SPLIT from 1224 to 8 entries

* Add some boundry tests

* Code cleanup

* Reduce POW10_SPLIT_2 from 3133 to 564

* Reduce POW10_SPLIT_2 from 564 to 527

* Fix clippy warnings

* Reduce POW10_SPLIT_2 from 527 to 481

* Max buffer size

* Refactor and add documentation

* Put under feature flag

* Some cleanup

* Apply review

* Add missing newlines

* Fix clippy lints

* Remove panics in `format64_to_fixed

---------

Co-authored-by: José Julián Espina <jedel0124@gmail.com>
  • Loading branch information
HalidOdat and jedel1043 authored Oct 3, 2023
1 parent 6138e7c commit 2a6fc89
Show file tree
Hide file tree
Showing 8 changed files with 1,895 additions and 12 deletions.
235 changes: 235 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in library 'ryu-js'",
"cargo": {
"args": [
"test",
"--no-run",
"--lib",
"--package=ryu-js"
],
"filter": {
"name": "ryu-js",
"kind": "lib"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug example 'upstream_benchmark'",
"cargo": {
"args": [
"build",
"--example=upstream_benchmark",
"--package=ryu-js"
],
"filter": {
"name": "upstream_benchmark",
"kind": "example"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in example 'upstream_benchmark'",
"cargo": {
"args": [
"test",
"--no-run",
"--example=upstream_benchmark",
"--package=ryu-js"
],
"filter": {
"name": "upstream_benchmark",
"kind": "example"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 'd2s_test'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=d2s_test",
"--package=ryu-js"
],
"filter": {
"name": "d2s_test",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 's2f_test'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=s2f_test",
"--package=ryu-js"
],
"filter": {
"name": "s2f_test",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 'to_fixed'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=to_fixed",
"--package=ryu-js"
],
"filter": {
"name": "to_fixed",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 's2d_test'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=s2d_test",
"--package=ryu-js"
],
"filter": {
"name": "s2d_test",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 'common_test'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=common_test",
"--package=ryu-js"
],
"filter": {
"name": "common_test",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 'exhaustive'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=exhaustive",
"--package=ryu-js"
],
"filter": {
"name": "exhaustive",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 'f2s_test'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=f2s_test",
"--package=ryu-js"
],
"filter": {
"name": "f2s_test",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug integration test 'd2s_table_test'",
"cargo": {
"args": [
"test",
"--no-run",
"--test=d2s_table_test",
"--package=ryu-js"
],
"filter": {
"name": "d2s_table_test",
"kind": "test"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug benchmark 'bench'",
"cargo": {
"args": [
"test",
"--no-run",
"--bench=bench",
"--package=ryu-js"
],
"filter": {
"name": "bench",
"kind": "bench"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,3 @@ targets = ["x86_64-unknown-linux-gnu"]
[[bench]]
name = "bench"
harness = false

70 changes: 60 additions & 10 deletions src/buffer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::pretty::to_fixed::MAX_BUFFER_SIZE;

use crate::raw;
use core::mem::MaybeUninit;
use core::{slice, str};
Expand All @@ -8,6 +10,8 @@ const NAN: &str = "NaN";
const INFINITY: &str = "Infinity";
const NEG_INFINITY: &str = "-Infinity";

const BUFFER_SIZE: usize = MAX_BUFFER_SIZE;

/// Safe API for formatting floating point numbers to text.
///
/// ## Example
Expand All @@ -17,8 +21,9 @@ const NEG_INFINITY: &str = "-Infinity";
/// let printed = buffer.format_finite(1.234);
/// assert_eq!(printed, "1.234");
/// ```
#[derive(Copy, Clone)]
pub struct Buffer {
bytes: [MaybeUninit<u8>; 25],
bytes: [MaybeUninit<u8>; BUFFER_SIZE],
}

impl Buffer {
Expand All @@ -27,7 +32,7 @@ impl Buffer {
#[inline]
#[cfg_attr(feature = "no-panic", no_panic)]
pub fn new() -> Self {
let bytes = [MaybeUninit::<u8>::uninit(); 25];
let bytes = [MaybeUninit::<u8>::uninit(); BUFFER_SIZE];

Buffer { bytes }
}
Expand Down Expand Up @@ -80,14 +85,37 @@ impl Buffer {
str::from_utf8_unchecked(slice)
}
}
}

impl Copy for Buffer {}
/// Print a floating point number into this buffer using the `Number.prototype.toFixed()` notation
/// and return a reference to its string representation within the buffer.
///
/// The `fraction_digits` argument must be between `[0, 100]` inclusive,
/// If a values value that is greater than the max is passed in will be clamped to max.
///
/// # Special cases
///
/// This function formats NaN as the string "NaN", positive infinity as
/// "Infinity", and negative infinity as "-Infinity" to match the [ECMAScript specification][spec].
///
/// [spec]: https://tc39.es/ecma262/#sec-numeric-types-number-tofixed
#[cfg_attr(feature = "no-panic", inline)]
#[cfg_attr(feature = "no-panic", no_panic)]
pub fn format_to_fixed<F: FloatToFixed>(&mut self, f: F, fraction_digits: u8) -> &str {
let fraction_digits = fraction_digits.min(100);

if f.is_nonfinite() {
return f.format_nonfinite();
}

impl Clone for Buffer {
#[inline]
fn clone(&self) -> Self {
*self
unsafe {
let n = f.write_to_ryu_buffer_to_fixed(
fraction_digits,
self.bytes.as_mut_ptr().cast::<u8>(),
);
debug_assert!(n <= self.bytes.len());
let slice = slice::from_raw_parts(self.bytes.as_ptr().cast::<u8>(), n);
str::from_utf8_unchecked(slice)
}
}
}

Expand All @@ -99,19 +127,31 @@ impl Default for Buffer {
}
}

/// A floating point number, f32 or f64, that can be written into a
/// A floating point number, [`f32`] or [`f64`], that can be written into a
/// [`ryu_js::Buffer`][Buffer].
///
/// This trait is sealed and cannot be implemented for types outside of the
/// `ryu_js` crate.
/// `ryu-js` crate.
pub trait Float: Sealed {}
impl Float for f32 {}
impl Float for f64 {}

/// A floating point number that can be written into a
/// [`ryu_js::Buffer`][Buffer] using the fixed notation as defined in the
/// [`Number.prototype.toFixed( fractionDigits )`][spec] ECMAScript specification.
///
/// This trait is sealed and cannot be implemented for types outside of the
/// `ryu-js` crate.
///
/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tofixed
pub trait FloatToFixed: Sealed {}
impl FloatToFixed for f64 {}

pub trait Sealed: Copy {
fn is_nonfinite(self) -> bool;
fn format_nonfinite(self) -> &'static str;
unsafe fn write_to_ryu_buffer(self, result: *mut u8) -> usize;
unsafe fn write_to_ryu_buffer_to_fixed(self, fraction_digits: u8, result: *mut u8) -> usize;
}

impl Sealed for f32 {
Expand Down Expand Up @@ -141,6 +181,11 @@ impl Sealed for f32 {
unsafe fn write_to_ryu_buffer(self, result: *mut u8) -> usize {
raw::format32(self, result)
}

#[inline]
unsafe fn write_to_ryu_buffer_to_fixed(self, _fraction_digits: u8, _result: *mut u8) -> usize {
panic!("toFixed for f32 type is not implemented yet!")
}
}

impl Sealed for f64 {
Expand Down Expand Up @@ -170,4 +215,9 @@ impl Sealed for f64 {
unsafe fn write_to_ryu_buffer(self, result: *mut u8) -> usize {
raw::format64(self, result)
}

#[inline]
unsafe fn write_to_ryu_buffer_to_fixed(self, fraction_digits: u8, result: *mut u8) -> usize {
raw::format64_to_fixed(self, fraction_digits, result)
}
}
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,10 @@ mod f2s;
mod f2s_intrinsics;
mod pretty;

pub use crate::buffer::{Buffer, Float};
pub use crate::buffer::{Buffer, Float, FloatToFixed};

/// Unsafe functions that mirror the API of the C implementation of Ryū.
pub mod raw {
pub use crate::pretty::format64_to_fixed;
pub use crate::pretty::{format32, format64};
}
3 changes: 3 additions & 0 deletions src/pretty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use core::ptr;
#[cfg(feature = "no-panic")]
use no_panic::no_panic;

pub mod to_fixed;
pub use to_fixed::{format64_to_fixed, Cursor};

/// Print f64 to the given buffer and return number of bytes written.
///
/// At most 25 bytes will be written.
Expand Down
Loading

0 comments on commit 2a6fc89

Please sign in to comment.