lexical_write_float/hex.rs
1//! Optimized float serializer for hexadecimal floats.
2//!
3//! This actually works for any case where we can exactly represent
4//! any power of the mantissa radix using the exponent base. For example,
5//! given a mantissa radix of `16`, and an exponent base of `8`,
6//! `16^2` cannot be exactly represented in octal. In short:
7//! `⌊log2(r) / log2(b)⌋ == ⌈log2(r) / log2(b)⌉`.
8//!
9//! This gives us the following mantissa radix/exponent base combinations:
10//!
11//! - 4, 2
12//! - 8, 2
13//! - 16, 2
14//! - 32, 2
15//! - 16, 4
16
17#![cfg(feature = "power-of-two")]
18#![doc(hidden)]
19
20use lexical_util::algorithm::rtrim_char_count;
21use lexical_util::constants::{FormattedSize, BUFFER_SIZE};
22use lexical_util::format::NumberFormat;
23use lexical_util::num::{Float, Integer};
24use lexical_write_integer::write::WriteInteger;
25
26use crate::binary::{
27 calculate_shl,
28 fast_ceildiv,
29 fast_log2,
30 truncate_and_round,
31 write_float_negative_exponent,
32 write_float_positive_exponent,
33};
34use crate::options::Options;
35use crate::shared;
36
37/// Optimized float-to-string algorithm for hexadecimal strings.
38///
39/// This assumes the float is:
40/// 1). Non-special (NaN or Infinite).
41/// 2). Non-negative.
42///
43/// # Safety
44///
45/// Safe as long as the float isn't special (NaN or Infinity), and `bytes`
46/// is large enough to hold the significant digits.
47///
48/// # Panics
49///
50/// Panics if the radix for the significant digits is not 16, if
51/// the exponent base is not 2, or if the radix for the exponent
52/// digits is not 10.
53pub fn write_float<F: Float, const FORMAT: u128>(
54 float: F,
55 bytes: &mut [u8],
56 options: &Options,
57) -> usize
58where
59 <F as Float>::Unsigned: WriteInteger + FormattedSize,
60{
61 // PRECONDITIONS
62
63 // Assert no special cases remain, no negative numbers,
64 // and a valid format.
65 let format = NumberFormat::<{ FORMAT }> {};
66 assert!(format.is_valid());
67 debug_assert!(!float.is_special());
68 debug_assert!(float >= F::ZERO);
69 debug_assert!(matches!(
70 (format.radix(), format.exponent_base()),
71 (4, 2) | (8, 2) | (16, 2) | (32, 2) | (16, 4)
72 ));
73
74 // Quickly calculate the number of bits we would have written.
75 // This simulates writing the digits, so we can calculate the
76 // scientific exponent. Since this number is often constant
77 // (except for denormal values), and doesn't describe
78 // the actual shift or digits we use...
79 //
80 // Note:
81 // Except for denormal floats, this will always
82 // be `F::MANTISSA_SIZE`, unless we have special
83 // formatting write control.
84 let mantissa = float.mantissa();
85 let radix = format.mantissa_radix();
86 let (mantissa, mantissa_bits) = truncate_and_round(mantissa, radix, options);
87
88 // See if we should use an exponent if the number was represented
89 // in scientific notation, AKA, `I.FFFF^EEE`. If the exponent is above
90 // a certain value, then use scientific notation. We therefore have
91 // to adjust the exponent by the number of mantissa bits, and shift
92 // by 1 (since a scientific exponent of 0 should have 1 digit ahead).
93 // This is a binary exp, so we need to how large our
94 // adjusted exp to the radix is.
95 //
96 // The scientific exponent is always this way: it's the float exponent
97 // (binary) + mantissa bits (binary) - 1 (for the first bit, binary),
98 // since 1.0 is scientific exponent 0. We need to check the scientific
99 // exponent relative to the number of leading or trailing 0s
100 // it would introduce, that is, scaled to bits/digit. The min exp must
101 // be less than, and the max must be above 0.
102 let exp = float.exponent();
103 let mut sci_exp = exp + mantissa_bits as i32 - 1;
104
105 // Normalize the exponent if we have an actual zero.
106 if mantissa == <F as Float>::Unsigned::ZERO {
107 sci_exp = 0;
108 }
109
110 // SAFETY: Safe, just other API methods need to be migrated in.
111 write_float!(
112 float,
113 FORMAT,
114 sci_exp,
115 options,
116 write_float_scientific,
117 write_float_positive_exponent,
118 write_float_negative_exponent,
119 generic => _,
120 bytes => bytes,
121 args => mantissa, exp, sci_exp, options,
122 )
123}
124
125/// Write float to string in scientific notation.
126///
127/// # Safety
128///
129/// Safe as long as `bytes` is large enough to hold the number of digits
130/// and the scientific notation's exponent digits.
131///
132/// # Preconditions
133///
134/// The mantissa must be truncated and rounded, prior to calling this,
135/// based on the number of maximum digits.
136#[cfg_attr(not(feature = "compact"), inline(always))]
137pub fn write_float_scientific<M, const FORMAT: u128>(
138 bytes: &mut [u8],
139 mantissa: M,
140 exp: i32,
141 sci_exp: i32,
142 options: &Options,
143) -> usize
144where
145 M: WriteInteger + FormattedSize,
146{
147 // Just decent size bounds checks to ensure we have a lot of space.
148 assert!(M::FORMATTED_SIZE < BUFFER_SIZE - 2);
149 debug_assert!(bytes.len() >= BUFFER_SIZE);
150
151 // Config options
152 let format = NumberFormat::<{ FORMAT }> {};
153 let bits_per_digit = fast_log2(format.mantissa_radix());
154 let bits_per_base = fast_log2(format.exponent_base());
155 let decimal_point = options.decimal_point();
156
157 // Write our value, then trim trailing zeros, before we check the exact
158 // bounds of the digits, to avoid accidentally choosing too many digits.
159 // shl is the powers of two we have missing from our exponent that nee
160 // to be transferred to our significant digits. Since the all mantissa
161 // radix powers can be **exactly** represented by exponent bases,
162 // we can just shift this into the mantissa.
163 let shl = calculate_shl(exp, bits_per_digit);
164 let value = mantissa << shl;
165
166 let count = value.write_mantissa::<FORMAT>(&mut bytes[1..]);
167 bytes[0] = bytes[1];
168 bytes[1] = decimal_point;
169 let zeros = rtrim_char_count(&bytes[2..count + 1], b'0');
170 let digit_count = count - zeros;
171 // Extra 1 since we have the decimal point.
172 let mut cursor = digit_count + 1;
173
174 // Determine if we need to add more trailing zeros.
175 let exact_count = shared::min_exact_digits(digit_count, options);
176
177 // Write any trailing digits to the output.
178 // Won't panic safe if the buffer is large enough to hold the significant
179 // digits.
180 if !format.no_exponent_without_fraction() && cursor == 2 && options.trim_floats() {
181 // Need to trim floats from trailing zeros, and we have only a decimal.
182 cursor -= 1;
183 } else if exact_count < 2 {
184 // Need to have at least 1 digit, the trailing `.0`.
185 bytes[cursor] = b'0';
186 cursor += 1;
187 } else if exact_count > digit_count {
188 // NOTE: Neither `exact_count >= digit_count >= 2`.
189 // We need to write `exact_count - (cursor - 1)` digits, since
190 // cursor includes the decimal point.
191 let digits_end = exact_count + 1;
192 bytes[cursor..digits_end].fill(b'0');
193 cursor = digits_end;
194 }
195
196 // Now, write our scientific notation.
197 // Won't panic safe if bytes is large enough to store all digits.
198 let scaled_sci_exp = scale_sci_exp(sci_exp, bits_per_digit, bits_per_base);
199 shared::write_exponent::<FORMAT>(bytes, &mut cursor, scaled_sci_exp, options.exponent());
200
201 cursor
202}
203
204// ALGORITHM
205// ---------
206
207/// We need to scale the scientific exponent for writing.
208///
209/// This is similar to [`binary::scale_sci_exp`](crate::binary::scale_sci_exp),
210/// however, we need to effectively have the same algorithm with `bits_per_base`
211/// instead of `bits_per_digit`. However, `bits_per_base` is smaller, and
212/// will not properly floor the values, so we add in an extra step.
213#[inline(always)]
214pub fn scale_sci_exp(sci_exp: i32, bits_per_digit: i32, bits_per_base: i32) -> i32 {
215 if sci_exp < 0 {
216 let neg_sci_exp = sci_exp.wrapping_neg();
217 let floor = fast_ceildiv(neg_sci_exp, bits_per_digit);
218 (floor * bits_per_digit / bits_per_base).wrapping_neg()
219 } else {
220 let floor = sci_exp / bits_per_digit;
221 floor * bits_per_digit / bits_per_base
222 }
223}