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}