radix_ecmascript/
lib.rs

1/*
2 * Copyright (c) 2023 Levi-Michael Taylor. All rights reserved.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
5 *
6 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7 *
8 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
9 *
10 * This library implements logic found in Google's open-source V8 library, specifically:
11 *  double.h (https://github.com/v8/v8/blob/f83601408c3207211bc8eb82a8802b01fd82c775/src/numbers/double.h)
12 *  DoubleToRadixCString (https://github.com/v8/v8/blob/f83601408c3207211bc8eb82a8802b01fd82c775/src/numbers/conversions.cc#L1269)
13 * Copyright 2014, the V8 project authors. All rights reserved.
14 * Redistribution and use in source and binary forms, with or without
15 * modification, are permitted provided that the following conditions are
16 * met:
17 *  Redistributions of source code must retain the above copyright
18 *    notice, this list of conditions and the following disclaimer.
19 *  Redistributions in binary form must reproduce the above
20 *    copyright notice, this list of conditions and the following
21 *    disclaimer in the documentation and/or other materials provided
22 *    with the distribution.
23 *  Neither the name of Google Inc. nor the names of its
24 *    contributors may be used to endorse or promote products derived
25 *    from this software without specific prior written permission.
26 *
27 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 *
39 * This license can also be found at: https://github.com/v8/v8/blob/f83601408c3207211bc8eb82a8802b01fd82c775/LICENSE
40 */
41
42//! `radix-ecmascript` adds a function, `to_radix_str`, to floating-point types (`f32` and `f64`)
43//! to allow callers to obtain their radix string representation, just like in JavaScript,
44//! in pure Rust. This library has no dependencies and is very lightweight.
45//!
46//! This library implements ECMAScript Language Specification Section 9.8.1,
47//! "ToString Applied to the Number Type", and uses the same logic as found in
48//! Google's open-source V8 engine.
49//!
50//! [DoubleToRadixCString](https://github.com/v8/v8/blob/f83601408c3207211bc8eb82a8802b01fd82c775/src/numbers/conversions.cc#L1269)
51//! [Double utility](https://github.com/v8/v8/blob/f83601408c3207211bc8eb82a8802b01fd82c775/src/numbers/double.h)
52//!
53//! Example:
54//! ```rust
55//! use radix_ecmascript::ToRadixStr;
56//!
57//! println!("{}", (0.123).to_radix_str(16).unwrap());
58//! ```
59//! This code prints `0.1f7ced916872b`, which can also be achieved by running
60//! `(0.123).toString(16)` in JavaScript.
61//!
62//! This code unwraps the returned `Result`, but you should (probably) handle the
63//! error in real cases. `to_radix_str` will only return `InvalidBaseError` if the
64//! given `Base` is outside of the valid range, `MIN_BASE` and `MAX_BASE`.
65
66mod f64_util;
67mod tests;
68
69use std::fmt::{Display, Formatter};
70
71/// A floating-point base.
72pub type Base = u8;
73
74/// The minimum [Base] that can be passed into [ToRadixStr::to_radix_str].
75pub const MIN_BASE: Base = 2;
76
77/// The maximum [Base] that can be passed into [ToRadixStr::to_radix_str].
78pub const MAX_BASE: Base = 36;
79
80/// An error indicating that a given [Base] value is out of range of
81/// [MIN_BASE] and [MAX_BASE].
82#[derive(Debug)]
83pub struct InvalidBaseError(Base);
84
85impl Display for InvalidBaseError {
86    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
87        write!(f, "invalid base: {}", self.0)
88    }
89}
90
91impl std::error::Error for InvalidBaseError {}
92
93/// Allows a type to be converted to radix string representation.
94pub trait ToRadixStr: Sized {
95    /// Returns the radix string representation of self using the functionality
96    /// as defined in the ECMAScript Language Specification Section 9.8.1
97    /// "ToString Applied to the Number Type".
98    ///
99    /// Returns [InvalidBaseError] if the given [Base] is out of range of
100    /// [MIN_BASE] and [MAX_BASE] (inclusive).
101    fn to_radix_str(self, base: Base) -> Result<String, InvalidBaseError>;
102}
103
104impl ToRadixStr for f64 {
105    fn to_radix_str(self, base: Base) -> Result<String, InvalidBaseError> {
106        use crate::f64_util::{exponent, next_float};
107
108        // Validate base
109        if !(MIN_BASE..=MAX_BASE).contains(&base) {
110            return Err(InvalidBaseError(base));
111        }
112
113        // The result is always "NaN" if self is NaN.
114        if self.is_nan() {
115            return Ok("NaN".into());
116        }
117
118        // If self is +0 or -0, return "0".
119        if self == 0.0 {
120            return Ok("0".into());
121        }
122
123        // If self is +Infinity, return "Infinity".
124        // If self is -Infinity, return "-Infinity".
125        if self.is_infinite() {
126            return Ok(if self.is_sign_positive() {
127                "Infinity"
128            } else {
129                "-Infinity"
130            }.into());
131        }
132
133        // Character array used for conversion.
134        const CHARS: [char; 36] = [
135            '0', '1', '2', '3', '4', '5',
136            '6', '7', '8', '9', 'a', 'b',
137            'c', 'd', 'e', 'f', 'g', 'h',
138            'i', 'j', 'k', 'l', 'm', 'n',
139            'o', 'p', 'q', 'r', 's', 't',
140            'u', 'v', 'w', 'x', 'y', 'z'
141        ];
142
143        // Temporary buffer for the result. We start with the decimal point in the
144        // middle and write to the left for the integer part and to the right for the
145        // fractional part. 1024 characters for the exponent and 52 for the mantissa
146        // either way, with additional space for sign, decimal point and string
147        // termination should be sufficient.
148        const BUFFER_LEN: usize = 2200;
149        // Allocate buffer and cursors.
150        let mut buf: [char; BUFFER_LEN] = ['\0'; BUFFER_LEN];
151        let mut int_cursor = BUFFER_LEN / 2;
152        let mut fraction_cursor = int_cursor;
153
154        // The value to reference and modify instead of self
155        let value = self.abs();
156
157        // Split the value into an integer part and a fractional part.
158        let mut integer = value.floor();
159        let mut fraction = value - integer;
160        // We only compute fractional digits up to the input's precision.
161        let mut delta = 0.5 * (next_float(value) - value);
162        delta = delta.max(next_float(0.0));
163        // Base as f64
164        let base_f64 = base as f64;
165        if fraction >= delta {
166            // Insert decimal point.
167            buf[fraction_cursor] = '.';
168            fraction_cursor += 1;
169
170            loop {
171                // Shift up by one digit.
172                fraction *= base_f64;
173                delta *= base_f64;
174
175                // Write digit.
176                let digit = fraction as usize;
177                buf[fraction_cursor] = CHARS[digit];
178                fraction_cursor += 1;
179
180                // Calculate remainder.
181                fraction -= digit as f64;
182
183                // Round to even.
184                if (fraction > 0.5 || (fraction == 0.5 && (digit & 1) == 1)) && fraction + delta > 1.0 {
185                    // We need to back trace already written digits in case of carry-over.
186                    loop {
187                        fraction_cursor -= 1;
188                        if fraction_cursor == BUFFER_LEN / 2 {
189                            // Carry over the integer part.
190                            integer += 1.0;
191                            break;
192                        }
193
194                        let c = buf[fraction_cursor];
195                        // Reconstruct digit.
196                        let digit = if c > '9' {
197                            (c as u32) - ('a' as u32) + 10
198                        } else {
199                            (c as u32) - ('0' as u32)
200                        };
201                        if digit + 1 < base as u32 {
202                            buf[fraction_cursor] = CHARS[digit as usize + 1];
203                            fraction_cursor += 1;
204                            break;
205                        }
206                    }
207
208                    break;
209                }
210
211                if fraction < delta {
212                    break;
213                }
214            }
215        }
216
217        // Compute integer digits. Fill unrepresented digits with zero.
218        while exponent(integer / base_f64) > 0 {
219            integer /= base_f64;
220            int_cursor -= 1;
221            buf[int_cursor] = '0';
222        }
223
224        loop {
225            let remainder = integer % base_f64;
226            int_cursor -= 1;
227            buf[int_cursor] = CHARS[remainder as usize];
228            integer = (integer - remainder) / base_f64;
229
230            if integer <= 0.0 {
231                break;
232            }
233        }
234
235        // Add sign if negative.
236        if self.is_sign_negative() {
237            int_cursor -= 1;
238            buf[int_cursor] = '-';
239        }
240
241        // Create result.
242        let mut result = String::with_capacity(fraction_cursor - int_cursor);
243        for c in &buf[int_cursor..fraction_cursor] {
244            result.push(*c);
245        }
246        Ok(result)
247    }
248}
249
250impl ToRadixStr for f32 {
251    fn to_radix_str(self, base: Base) -> Result<String, InvalidBaseError> {
252        (self as f64).to_radix_str(base)
253    }
254}