yo_common/num.rs
1//! Numbers to text and back, written by hand.
2//!
3//! Every reply that carries a length or an integer goes through here, which is
4//! every reply, so this is as hot as anything in the codec. The formatting
5//! machinery in `core::fmt` would produce the same bytes and would be several
6//! times slower for the two or three digits a bulk header usually needs, so it
7//! is not used for integers.
8//!
9//! Parsing is deliberately strict and matches Redis's `string2ll` byte for
10//! byte, including its refusal of leading zeros and of a leading `+`. The
11//! protocol's own lengths are parsed with it, so a stricter or looser reading
12//! here is a real difference in what the two servers accept.
13
14use core::fmt::Write as _;
15
16/// Every two digit pair, `00` through `99`, laid out end to end.
17///
18/// Two digits per pass rather than one halves the number of divisions, which is
19/// the whole cost of this loop. Built at compile time rather than typed out,
20/// because a two hundred character literal is a typo waiting to happen and the
21/// compiler will do it for free.
22const PAIRS: [u8; 200] = {
23 let mut t = [0u8; 200];
24 let mut i = 0;
25 while i < 100 {
26 t[i * 2] = b'0' + (i / 10) as u8;
27 t[i * 2 + 1] = b'0' + (i % 10) as u8;
28 i += 1;
29 }
30 t
31};
32
33/// The most digits a `u64` can have, which is what `18446744073709551615` needs.
34const U64_DIGITS: usize = 20;
35
36/// Appends the decimal digits of `n`.
37///
38/// The digits go into a fixed twenty byte buffer that is then copied whole,
39/// with the length cut back afterwards to the digits that are actually there.
40/// A copy of a length the compiler can see is a couple of stores it writes
41/// inline; a copy of a length only known at run time is a call into the
42/// platform's `memmove`, and getting into that call costs more than moving one
43/// digit. It showed up as eleven percent of `SADD` on the wire, where the whole
44/// reply is `:0`.
45pub fn push_u64(out: &mut Vec<u8>, n: u64) {
46 let len = u64_len(n);
47 let mut buf = [0u8; U64_DIGITS];
48 let mut i = len;
49 let mut n = n;
50 while n >= 100 {
51 let p = ((n % 100) as usize) * 2;
52 n /= 100;
53 i -= 2;
54 buf[i] = PAIRS[p];
55 buf[i + 1] = PAIRS[p + 1];
56 }
57 if n >= 10 {
58 let p = (n as usize) * 2;
59 buf[0] = PAIRS[p];
60 buf[1] = PAIRS[p + 1];
61 } else {
62 buf[0] = b'0' + n as u8;
63 }
64 let at = out.len();
65 out.extend_from_slice(&buf);
66 out.truncate(at + len);
67}
68
69/// Appends the decimal digits of `n`, with a minus sign if it needs one.
70pub fn push_i64(out: &mut Vec<u8>, n: i64) {
71 if n < 0 {
72 out.push(b'-');
73 }
74 // `unsigned_abs` rather than `-n`, which overflows on `i64::MIN`.
75 push_u64(out, n.unsigned_abs());
76}
77
78/// The number of bytes [`push_i64`] would append.
79///
80/// Used to presize a reply buffer before anything is written to it, which is
81/// the whole point of Y18: the buffer is sized once from what is about to go
82/// into it rather than grown while it is being filled.
83pub const fn i64_len(n: i64) -> usize {
84 (if n < 0 { 1 } else { 0 }) + u64_len(n.unsigned_abs())
85}
86
87/// How many digits `n` has.
88///
89/// `ilog10` and not a loop of divides, because this runs in front of every
90/// integer reply to size the buffer and a divide by ten is twenty cycles the
91/// hardware's leading zero count answers in one.
92#[must_use]
93pub const fn u64_len(n: u64) -> usize {
94 match n.checked_ilog10() {
95 Some(log) => log as usize + 1,
96 // `ilog10` has no answer for zero, which still takes one digit to say.
97 None => 1,
98 }
99}
100
101/// A buffer big enough for the digits of any `i64` or `u64`, sign included.
102///
103/// Twenty digits for `18446744073709551615` and one more for the minus sign
104/// that `-9223372036854775808` needs.
105pub const DIGITS_MAX: usize = U64_DIGITS + 1;
106
107/// The digits of `n`, written backwards into `buf`, and where they start.
108///
109/// One digit a pass rather than the two [`push_u64`] does, because these two
110/// are not on the reply path and the pair table is only worth its branch when
111/// it is.
112fn fill_back(buf: &mut [u8; DIGITS_MAX], n: u64) -> usize {
113 let mut at = DIGITS_MAX;
114 let mut v = n;
115 loop {
116 at -= 1;
117 buf[at] = b'0' + (v % 10) as u8;
118 v /= 10;
119 if v == 0 {
120 return at;
121 }
122 }
123}
124
125/// The decimal digits of `n`, written into the back of `buf`.
126///
127/// The same answer [`push_i64`] gives, for a caller that has nowhere to put a
128/// `Vec`. `SSCAN key 0 MATCH 1*` has to run a glob over a member that is stored
129/// as a number and has no digits anywhere, and doing that through a `Vec` would
130/// be an allocation per member on a thread that must not allocate.
131pub fn i64_digits(buf: &mut [u8; DIGITS_MAX], n: i64) -> &[u8] {
132 let mut at = fill_back(buf, n.unsigned_abs());
133 if n < 0 {
134 at -= 1;
135 buf[at] = b'-';
136 }
137 &buf[at..]
138}
139
140/// The decimal digits of `n`, written into the back of `buf`.
141///
142/// The unsigned form, for the numbers that genuinely do not fit in an `i64`. A
143/// scan cursor is one: ours packs a partition count into the top bits, so a
144/// large enough collection hands the client a number with bit 63 set and
145/// reporting it as a signed integer would report it as negative.
146pub fn u64_digits(buf: &mut [u8; DIGITS_MAX], n: u64) -> &[u8] {
147 let at = fill_back(buf, n);
148 &buf[at..]
149}
150
151/// Parses a signed decimal integer the way Redis's `string2ll` does.
152///
153/// Returns `None` for anything it would reject, which includes an empty slice,
154/// a leading `+`, a leading zero on a non zero number, any non digit anywhere,
155/// and anything that does not fit in an `i64`. The protocol's array and bulk
156/// lengths are parsed with this, so being looser here would mean accepting
157/// frames that Redis rejects, and being stricter would mean the reverse.
158pub fn parse_i64(s: &[u8]) -> Option<i64> {
159 // The longest thing that can parse is `-9223372036854775808`, at twenty.
160 if s.is_empty() || s.len() > 20 {
161 return None;
162 }
163 let (negative, digits) = if s[0] == b'-' {
164 (true, &s[1..])
165 } else {
166 (false, s)
167 };
168 if digits.is_empty() {
169 return None;
170 }
171 // A leading zero is only ever a whole number zero, and only a positive one.
172 // `007` is not seven here and it is not seven in Redis either, and `-0` is
173 // not a number in either: `string2ll` tests its zero case against the length
174 // of the whole string, so the minus sign puts `-0` past it and into the one
175 // to nine gate, which it fails. That matters beyond parsing, because this is
176 // also what decides whether a string is stored int encoded. Accepting `-0`
177 // would store it as the integer zero, and `GET` would then hand the client
178 // back `0` for a value it wrote as `-0`.
179 if digits[0] == b'0' {
180 return if digits.len() == 1 && !negative {
181 Some(0)
182 } else {
183 None
184 };
185 }
186 let mut v: u64 = 0;
187 for &c in digits {
188 if !c.is_ascii_digit() {
189 return None;
190 }
191 v = v.checked_mul(10)?.checked_add(u64::from(c - b'0'))?;
192 }
193 if negative {
194 // One more magnitude is available going negative, and `i64::MIN`
195 // reached through `wrapping_neg` is the one value that cannot be
196 // written as a positive `i64` first.
197 if v > (i64::MAX as u64) + 1 {
198 None
199 } else {
200 Some((v as i64).wrapping_neg())
201 }
202 } else if v > i64::MAX as u64 {
203 None
204 } else {
205 Some(v as i64)
206 }
207}
208
209/// The largest magnitude Redis's `double2ll` will turn into an integer.
210///
211/// `double2ll` refuses anything outside `LLONG_MAX / 2`, which as a double is
212/// exactly two to the sixty second, and then checks that the value survives a
213/// round trip through a `long long`. Everything inside that range and integral
214/// is written with the integer printer rather than the digit generator, and
215/// that decision is visible: at this magnitude the digit generator would switch
216/// to an exponent.
217const DOUBLE_INT_LIMIT: f64 = 4_611_686_018_427_387_904.0; // 2^62
218
219/// Redis's `getLongDoubleFromObject`, as far as the difference is observable.
220///
221/// A float argument and a float value are parsed by the same rules, and the
222/// rules are stricter than Rust's `str::parse`: no leading or trailing
223/// whitespace at all, and `nan` is refused where the infinities are not. Redis
224/// refuses NaN because every command that takes a float goes on to store the
225/// result, and a stored NaN compares false against itself forever after.
226///
227/// This lives here for the same reason [`parse_i64`] does. It is not a codec
228/// question, it is the same question the string type asks of a stored value,
229/// and the storage layer cannot reach into the wire layer to ask it.
230///
231/// It also takes hexadecimal, because `strtold` does and Redis inherits every
232/// bit of that. `INCRBYFLOAT` on a key holding `0x10` counts from sixteen on a
233/// real server, and `INCRBYFLOAT key 0x10` adds sixteen. Nobody designed that
234/// and it is unlikely anyone relies on it, but a client that sends it gets an
235/// answer from Redis and an error from us, and telling a client its value is
236/// not a valid float when the server next door accepts it is the kind of
237/// difference that gets found in production rather than in a test.
238pub fn parse_f64(s: &[u8]) -> Option<f64> {
239 if s.is_empty() || s[0].is_ascii_whitespace() {
240 return None;
241 }
242 let text = core::str::from_utf8(s).ok()?;
243 if text.trim() != text {
244 return None;
245 }
246 let v = if is_hex(text) {
247 parse_hex_f64(text)?
248 } else {
249 text.parse().ok()?
250 };
251 if v.is_nan() { None } else { Some(v) }
252}
253
254/// Does this start the way a C hexadecimal float does?
255///
256/// Only the prefix is checked here. Whether the rest of it is a number at all
257/// is [`parse_hex_f64`]'s problem, and a string that starts `0x` and continues
258/// badly has to be refused rather than falling back to the decimal parser,
259/// which would read `0xzz` as a plain zero.
260fn is_hex(text: &str) -> bool {
261 let body = text.strip_prefix(['+', '-']).unwrap_or(text).as_bytes();
262 body.len() > 2 && body[0] == b'0' && (body[1] | 0x20) == b'x'
263}
264
265/// `0x1.8p1` and the rest of C's hexadecimal float syntax.
266///
267/// The binary exponent is optional, which it is not in a C source literal but
268/// is in `strtod`, so `0x10` on its own is sixteen. The mantissa is gathered
269/// into a `u64` until it is full and after that the digits only move the
270/// exponent, which costs nothing anyone will see: sixteen hex digits is more
271/// precision than a double has to give back.
272fn parse_hex_f64(text: &str) -> Option<f64> {
273 let (negative, rest) = match text.as_bytes()[0] {
274 b'-' => (true, &text[1..]),
275 b'+' => (false, &text[1..]),
276 _ => (false, text),
277 };
278 let body = &rest[2..]; // `is_hex` already checked the `0x`.
279
280 let mut mantissa: u64 = 0;
281 let mut exponent: i32 = 0;
282 let mut digits = 0usize;
283 let mut seen_point = false;
284 let mut at = 0usize;
285 let bytes = body.as_bytes();
286
287 while at < bytes.len() {
288 let c = bytes[at];
289 if c == b'.' {
290 if seen_point {
291 return None;
292 }
293 seen_point = true;
294 at += 1;
295 continue;
296 }
297 let Some(value) = (c as char).to_digit(16) else {
298 break;
299 };
300 digits += 1;
301 if mantissa <= u64::MAX >> 4 {
302 mantissa = (mantissa << 4) | u64::from(value);
303 if seen_point {
304 exponent -= 4;
305 }
306 } else if !seen_point {
307 // Past what a `u64` holds, a digit before the point is worth four
308 // more binary places and nothing else.
309 exponent += 4;
310 }
311 at += 1;
312 }
313 if digits == 0 {
314 return None;
315 }
316
317 if at < bytes.len() {
318 // A binary exponent, and it is the only thing allowed to be here.
319 if (bytes[at] | 0x20) != b'p' {
320 return None;
321 }
322 let written: i32 = rest[2 + at + 1..].parse().ok()?;
323 exponent = exponent.checked_add(written)?;
324 }
325
326 let value = (mantissa as f64) * exp2(exponent);
327 // An overflow to infinity is refused rather than stored. Redis refuses it
328 // too, at a much higher ceiling, and that gap is in the divergence register
329 // rather than pretended away here.
330 if !value.is_finite() {
331 return None;
332 }
333 Some(if negative { -value } else { value })
334}
335
336/// Two to the power of a whole number, without `std`.
337///
338/// `powi` is not in core, and the exponent can be far enough out that squaring
339/// up from one would take a while, so this walks the bits. A power that is out
340/// of range comes back as an infinity and the caller refuses it.
341fn exp2(mut n: i32) -> f64 {
342 let mut base = if n < 0 { 0.5 } else { 2.0 };
343 n = n.abs();
344 let mut out = 1.0f64;
345 while n > 0 {
346 if n & 1 == 1 {
347 out *= base;
348 }
349 base *= base;
350 n >>= 1;
351 }
352 out
353}
354
355/// Room for the longest thing [`write_double`] or [`write_g17`] can write.
356///
357/// Both of them are bounded by the same thing, a mantissa of seventeen digits
358/// with a handful of zeros or a `.` and an exponent around it, and neither can
359/// reach thirty two bytes. It is worth saying out loud that this used to be
360/// three hundred and fifty two, because Rust's printer writes every leading
361/// zero of a subnormal, and the port of Redis's own printer is what shrank it.
362pub const DOUBLE_MAX: usize = 32;
363
364const _: () = assert!(DOUBLE_MAX >= crate::dtoa::MAX);
365
366/// Appends a double the way Redis 8 writes one.
367///
368/// This is `d2string`. Redis stopped using `%.17g` in 7.0 and now writes a
369/// double in two cases: a value that is exactly an integer inside two to the
370/// sixty second is written with the integer printer, and everything else goes
371/// through the Grisu2 in [`crate::dtoa`]. Zero is checked before either of
372/// them, which is the only reason negative zero comes back as `-0` rather than
373/// as `0`.
374///
375/// The infinities and NaN are written as bare words because that is what RESP3
376/// says and what RESP2 clients have always been given.
377pub fn push_double(out: &mut Vec<u8>, d: f64) {
378 let mut buf = [0u8; DOUBLE_MAX];
379 out.extend_from_slice(write_double(&mut buf, d));
380}
381
382/// Appends a double the way `INCRBYFLOAT` and `HINCRBYFLOAT` write one, which
383/// is not the way everything else does.
384///
385/// Those two go through `ld2string` in its human mode rather than through
386/// `d2string`, and the human mode is `%.17Lf` with the trailing zeros taken off
387/// and a lone `-0` turned back into `0`. Being a fixed point conversion it never
388/// writes an exponent, so `INCRBYFLOAT key 1e30` answers a one and thirty zeros
389/// where `ZSCORE` would answer `1e+30` for the same number.
390///
391/// The digits are the shortest ones rather than seventeen decimal places of the
392/// `f64`, and that is the closer answer rather than the lazier one. Redis holds
393/// the value in a long double, so `%.17Lf` of one tenth is `0.10000000000000000`
394/// and comes back as `0.1` once the zeros are stripped. Seventeen decimal places
395/// of the `f64` would be `0.10000000000000001`, which is a worse match for the
396/// same reason D-11 gives: the extra width is what makes the long double print
397/// cleanly, and shortest digits land on the same text without pretending to have
398/// it.
399pub fn push_human(out: &mut Vec<u8>, d: f64) {
400 if d.is_nan() {
401 out.extend_from_slice(b"nan");
402 return;
403 }
404 if d.is_infinite() {
405 out.extend_from_slice(if d > 0.0 { b"inf" } else { b"-inf" });
406 return;
407 }
408 // Negative zero loses its sign here, which is the one thing the human mode
409 // says out loud and `d2string` does the other way round.
410 if d.fract() == 0.0 && d.abs() <= DOUBLE_INT_LIMIT {
411 push_i64(out, d as i64);
412 return;
413 }
414 // Writing through the sink puts the digits straight into the reply buffer.
415 // `format!` would produce the same bytes and one throwaway allocation, and
416 // a shard thread that allocates aborts.
417 let mut sink = Utf8Sink(out);
418 let _ = write!(sink, "{d}");
419}
420
421/// Writes a double into a fixed buffer, byte for byte what [`push_double`]
422/// would append.
423///
424/// The two are the same code now, and this is the one that does the work,
425/// because the digit generator wants somewhere to put eighteen digits before it
426/// knows how many of them it is going to keep. The caller that needs it as a
427/// buffer rather than as a reply is the array type, which stores a value as a
428/// double only when the double prints back as the exact bytes the client sent,
429/// so it formats a candidate, compares, and usually throws it away.
430pub fn write_double(buf: &mut [u8; DOUBLE_MAX], d: f64) -> &[u8] {
431 // Zero first, so that the sign of a negative zero survives. The integer
432 // path below would lose it and Redis checks in this order for that reason.
433 if d == 0.0 {
434 let n = if d.is_sign_negative() {
435 buf[..2].copy_from_slice(b"-0");
436 2
437 } else {
438 buf[0] = b'0';
439 1
440 };
441 return &buf[..n];
442 }
443 if d.is_nan() {
444 buf[..3].copy_from_slice(b"nan");
445 return &buf[..3];
446 }
447 if d.is_infinite() {
448 let text: &[u8] = if d > 0.0 { b"inf" } else { b"-inf" };
449 buf[..text.len()].copy_from_slice(text);
450 return &buf[..text.len()];
451 }
452 if d.fract() == 0.0 && d.abs() <= DOUBLE_INT_LIMIT {
453 let mut digits = [0u8; DIGITS_MAX];
454 let text = i64_digits(&mut digits, d as i64);
455 let n = text.len();
456 buf[..n].copy_from_slice(text);
457 return &buf[..n];
458 }
459 let n = crate::dtoa::dtoa(d, buf);
460 &buf[..n]
461}
462
463/// Writes a double the way C's `%.17g` writes one, which is what `AROP`
464/// replies with.
465///
466/// Redis formats an aggregate through `ld2string` in its automatic mode, and
467/// that mode is a plain `%.17Lg`, so this is the one reply in the whole server
468/// that is not a shortest round trip printer. The difference is visible: three
469/// tenths comes back as `0.29999999999999999` here and as `0.3` from `ZSCORE`,
470/// because seventeen significant digits of the nearest double to three tenths
471/// really are those.
472///
473/// `%g` picks between the two forms the way C says: the exponent form when the
474/// decimal exponent is below minus four or at least the precision, the plain
475/// form otherwise, and trailing zeros come off either way.
476pub fn write_g17(buf: &mut [u8; DOUBLE_MAX], d: f64) -> &[u8] {
477 /// Seventeen significant digits is sixteen after the point.
478 const AFTER: usize = 16;
479 if d.is_nan() {
480 buf[..3].copy_from_slice(b"nan");
481 return &buf[..3];
482 }
483 if d.is_infinite() {
484 let word: &[u8] = if d > 0.0 { b"inf" } else { b"-inf" };
485 buf[..word.len()].copy_from_slice(word);
486 return &buf[..word.len()];
487 }
488 // The exponent C would use is the one the value has after it has been
489 // rounded to seventeen digits, so it has to come from the rounding and not
490 // from a logarithm: 9.9999999999999999e-5 rounds up into the next decade.
491 let mut scratch = [0u8; DOUBLE_MAX];
492 let mut sink = SliceSink {
493 buf: &mut scratch,
494 at: 0,
495 };
496 let _ = write!(sink, "{d:.AFTER$e}");
497 let end = sink.at;
498 let split = scratch[..end]
499 .iter()
500 .position(|&c| c == b'e')
501 .expect("the exponent form always has one");
502 let exp = parse_i64(&scratch[split + 1..end]).expect("a written exponent parses") as i32;
503
504 if !(-4..17).contains(&exp) {
505 // The exponent form, and C writes at least two exponent digits where
506 // Rust writes as few as one.
507 let mantissa = trim_zeros(&scratch[..split]);
508 let n = mantissa.len();
509 buf[..n].copy_from_slice(mantissa);
510 let mut sink = SliceSink { buf, at: n };
511 let sign = if exp < 0 { '-' } else { '+' };
512 let _ = write!(sink, "e{sign}{:02}", exp.unsigned_abs());
513 let at = sink.at;
514 return &buf[..at];
515 }
516 // The plain form, whose precision is what is left of the seventeen digits
517 // once the integer part has had its share.
518 let places = usize::try_from(AFTER as i32 - exp).unwrap_or(0);
519 let mut sink = SliceSink { buf, at: 0 };
520 let _ = write!(sink, "{d:.places$}");
521 let at = sink.at;
522 let n = trim_zeros(&buf[..at]).len();
523 &buf[..n]
524}
525
526/// The same digits [`write_g17`] would write, appended.
527pub fn push_g17(out: &mut Vec<u8>, d: f64) {
528 let mut buf = [0u8; DOUBLE_MAX];
529 out.extend_from_slice(write_g17(&mut buf, d));
530}
531
532/// Takes the trailing zeros off a fixed point number, and the point with them
533/// when nothing is left after it.
534///
535/// A number with no point in it is left alone, because the zeros in `1700` are
536/// not trailing anything.
537fn trim_zeros(text: &[u8]) -> &[u8] {
538 if !text.contains(&b'.') {
539 return text;
540 }
541 let end = text.iter().rposition(|&c| c != b'0').unwrap_or(0);
542 if text[end] == b'.' {
543 &text[..end]
544 } else {
545 &text[..=end]
546 }
547}
548
549/// A `core::fmt::Write` that fills a fixed buffer and stops when it is full.
550///
551/// Running out of room cannot happen here, because [`DOUBLE_MAX`] is sized for
552/// the widest double there is, and it is handled rather than asserted so that a
553/// mistake in that reasoning truncates a number instead of killing a shard.
554struct SliceSink<'a> {
555 buf: &'a mut [u8; DOUBLE_MAX],
556 at: usize,
557}
558
559impl core::fmt::Write for SliceSink<'_> {
560 fn write_str(&mut self, s: &str) -> core::fmt::Result {
561 let n = s.len().min(self.buf.len() - self.at);
562 self.buf[self.at..self.at + n].copy_from_slice(&s.as_bytes()[..n]);
563 self.at += n;
564 Ok(())
565 }
566}
567
568/// A `core::fmt::Write` that appends UTF-8 to a byte buffer.
569///
570/// The float printer only speaks `fmt::Write` and the reply buffer is bytes.
571/// This is the whole adapter, and it exists so that no reply path anywhere ever
572/// builds a `String` it immediately throws away.
573struct Utf8Sink<'a>(&'a mut Vec<u8>);
574
575impl core::fmt::Write for Utf8Sink<'_> {
576 fn write_str(&mut self, s: &str) -> core::fmt::Result {
577 self.0.extend_from_slice(s.as_bytes());
578 Ok(())
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 fn text(n: i64) -> String {
587 let mut v = Vec::new();
588 push_i64(&mut v, n);
589 String::from_utf8(v).unwrap()
590 }
591
592 #[test]
593 fn integers_round_trip_through_text() {
594 for n in [
595 0,
596 1,
597 9,
598 10,
599 99,
600 100,
601 -1,
602 -9,
603 -10,
604 12345,
605 -12345,
606 i64::MAX,
607 i64::MIN,
608 ] {
609 assert_eq!(text(n), n.to_string(), "writing {n}");
610 assert_eq!(parse_i64(text(n).as_bytes()), Some(n), "reading {n}");
611 }
612 }
613
614 #[test]
615 fn the_length_is_known_before_the_digits_are_written() {
616 for n in [0, 5, 42, -42, 999, 1000, i64::MAX, i64::MIN] {
617 assert_eq!(i64_len(n), text(n).len(), "length of {n}");
618 }
619 }
620
621 /// Every boundary of the two digit loop, since an off by one there is a
622 /// wrong length header rather than a crash and would be found by a client.
623 #[test]
624 fn every_length_of_number_is_written_correctly() {
625 let mut n: u64 = 0;
626 for _ in 0..20 {
627 for probe in [n, n + 1, n.saturating_sub(1)] {
628 let mut v = Vec::new();
629 push_u64(&mut v, probe);
630 assert_eq!(v, probe.to_string().as_bytes(), "writing {probe}");
631 }
632 n = n.saturating_mul(10).max(9);
633 if n == u64::MAX {
634 break;
635 }
636 }
637 }
638
639 #[test]
640 fn the_stack_form_writes_what_the_vec_form_writes() {
641 // Two implementations of the same digits is the shape of bug that only
642 // shows at one boundary, so this checks them against each other rather
643 // than against a literal.
644 let mut buf = [0u8; DIGITS_MAX];
645 let mut n: i64 = 0;
646 for _ in 0..19 {
647 for probe in [n, -n, n + 1, n - 1] {
648 assert_eq!(i64_digits(&mut buf, probe), text(probe).as_bytes());
649 }
650 n = n.saturating_mul(10).max(9);
651 }
652 assert_eq!(i64_digits(&mut buf, i64::MIN), text(i64::MIN).as_bytes());
653 assert_eq!(i64_digits(&mut buf, i64::MAX), text(i64::MAX).as_bytes());
654 assert_eq!(i64_digits(&mut buf, 0), b"0", "and zero is one digit");
655
656 // And the unsigned form past where the signed one stops, which is the
657 // whole reason it is there.
658 for probe in [0, 1, u64::MAX, 1 << 63, i64::MAX as u64 + 1] {
659 let mut v = Vec::new();
660 push_u64(&mut v, probe);
661 assert_eq!(u64_digits(&mut buf, probe), v.as_slice(), "{probe}");
662 }
663 }
664
665 #[test]
666 fn the_parser_refuses_what_redis_refuses() {
667 for bad in [
668 &b""[..],
669 b"-",
670 b"+1",
671 b"01",
672 b"-01",
673 b" 1",
674 b"1 ",
675 b"1a",
676 b"a",
677 b"1.0",
678 b"-0",
679 b"-00",
680 b"9223372036854775808",
681 b"-9223372036854775809",
682 b"99999999999999999999999",
683 ] {
684 assert_eq!(parse_i64(bad), None, "{:?} should not parse", bad);
685 }
686 // The one leading zero that is a number, and the one negative that only
687 // exists going downwards.
688 assert_eq!(parse_i64(b"0"), Some(0));
689 assert_eq!(parse_i64(b"-9223372036854775808"), Some(i64::MIN));
690 }
691
692 #[test]
693 fn the_float_parser_refuses_what_redis_refuses() {
694 assert_eq!(parse_f64(b"3.5"), Some(3.5));
695 assert_eq!(parse_f64(b"-0"), Some(-0.0));
696 assert_eq!(parse_f64(b"3.0e3"), Some(3000.0));
697 assert_eq!(parse_f64(b"inf"), Some(f64::INFINITY));
698 assert_eq!(parse_f64(b"-inf"), Some(f64::NEG_INFINITY));
699 // No whitespace anywhere, nothing trailing, and no NaN, because a
700 // stored NaN compares false against itself for the rest of time.
701 assert_eq!(parse_f64(b" 3.5"), None);
702 assert_eq!(parse_f64(b"3.5 "), None);
703 assert_eq!(parse_f64(b"3.5x"), None);
704 assert_eq!(parse_f64(b""), None);
705 assert_eq!(parse_f64(b"nan"), None);
706 }
707
708 #[test]
709 fn the_float_parser_takes_hexadecimal_because_strtold_does() {
710 // Every one of these was read off a real 8.10.1 before it was written
711 // down here.
712 assert_eq!(parse_f64(b"0x10"), Some(16.0));
713 assert_eq!(parse_f64(b"0X10"), Some(16.0));
714 assert_eq!(parse_f64(b"0X1p4"), Some(16.0));
715 assert_eq!(parse_f64(b"0x1.8p1"), Some(3.0));
716 assert_eq!(parse_f64(b"-0x1.8p1"), Some(-3.0));
717 assert_eq!(parse_f64(b"+0x10"), Some(16.0));
718 assert_eq!(parse_f64(b"0x1p-1"), Some(0.5));
719 assert_eq!(parse_f64(b"0xff"), Some(255.0));
720
721 // A string that starts like a hexadecimal number and then stops being
722 // one is refused rather than falling through to the decimal parser,
723 // which would read the leading zero and call it a day.
724 assert_eq!(parse_f64(b"0x"), None);
725 assert_eq!(parse_f64(b"0xzz"), None);
726 assert_eq!(parse_f64(b"0x1p"), None);
727 assert_eq!(parse_f64(b"0x1.2.3"), None);
728 assert_eq!(parse_f64(b"0x10x"), None);
729 assert_eq!(parse_f64(b"0x1p99999"), None);
730 }
731
732 #[test]
733 fn a_mantissa_longer_than_a_double_still_lands_in_the_right_place() {
734 // Seventeen hex digits, one more than a u64 holds. The digits past the
735 // end are worth four binary places each and nothing else, which is all
736 // a double can use them for anyway.
737 assert_eq!(
738 parse_f64(b"0x10000000000000000"),
739 Some(18446744073709551616.0)
740 );
741 assert_eq!(parse_f64(b"0x1p1024"), None);
742 }
743
744 #[test]
745 fn doubles_are_written_the_way_redis_writes_them() {
746 let cases: &[(f64, &str)] = &[
747 (0.0, "0"),
748 // Redis checks for zero before it checks for an integer, so this
749 // keeps its sign where the integer printer would have dropped it.
750 (-0.0, "-0"),
751 (3.0, "3"),
752 (-3.0, "-3"),
753 (3.5, "3.5"),
754 (0.1, "0.1"),
755 // The integer printer reaches two to the sixty second, and past it
756 // the digit generator takes over and switches to an exponent.
757 (4.611686018427388e18, "4611686018427387904"),
758 (1e19, "1e+19"),
759 (1e30, "1e+30"),
760 (1e-7, "1e-7"),
761 (1e-6, "0.000001"),
762 (5e-324, "5e-324"),
763 (f64::INFINITY, "inf"),
764 (f64::NEG_INFINITY, "-inf"),
765 (f64::NAN, "nan"),
766 ];
767 for &(d, want) in cases {
768 let mut v = Vec::new();
769 push_double(&mut v, d);
770 assert_eq!(String::from_utf8(v).unwrap(), want, "writing {d}");
771 }
772 }
773
774 /// The human printer is the other one, and the difference is the exponent.
775 ///
776 /// `INCRBYFLOAT` and `HINCRBYFLOAT` are the only two commands that use it,
777 /// and the reason it exists as a separate thing is the last four rows: a
778 /// fixed point conversion has no exponent form to switch to, so a magnitude
779 /// that comes back as `1e+30` from a score comes back written out in full
780 /// from an increment.
781 #[test]
782 fn the_increment_printer_never_writes_an_exponent() {
783 let cases: &[(f64, &str)] = &[
784 (0.0, "0"),
785 // The human mode says so explicitly, where `d2string` keeps it.
786 (-0.0, "0"),
787 (3.0, "3"),
788 (3.5, "3.5"),
789 (0.1, "0.1"),
790 (10.5, "10.5"),
791 (0.30000000000000004, "0.30000000000000004"),
792 (1e30, "1000000000000000000000000000000"),
793 (1e19, "10000000000000000000"),
794 (1e-7, "0.0000001"),
795 (f64::INFINITY, "inf"),
796 (f64::NEG_INFINITY, "-inf"),
797 (f64::NAN, "nan"),
798 ];
799 for &(d, want) in cases {
800 let mut v = Vec::new();
801 push_human(&mut v, d);
802 assert_eq!(String::from_utf8(v).unwrap(), want, "writing {d}");
803 }
804 // The smallest subnormal, which is where the lack of an exponent form
805 // costs the most: `0.` and then three hundred and twenty four places.
806 let mut v = Vec::new();
807 push_human(&mut v, 5e-324);
808 assert_eq!(v.len(), 326);
809 assert!(v.starts_with(b"0.0") && v.ends_with(b"5"));
810 }
811
812 /// The two double writers have to agree, because one is used to predict the
813 /// other.
814 ///
815 /// The array type decides whether a value can be stored as a double by
816 /// formatting it with `write_double` and checking the bytes against what the
817 /// client sent, and then the reply comes out of `push_double`. If they ever
818 /// disagreed, a value would go in as a number and come back out as
819 /// different text.
820 #[test]
821 fn the_two_double_writers_agree() {
822 let mut cases = vec![
823 0.0,
824 -0.0,
825 1.0,
826 -1.0,
827 3.5,
828 0.1,
829 -0.1,
830 1e-320,
831 f64::MIN_POSITIVE,
832 f64::MAX,
833 f64::MIN,
834 DOUBLE_INT_LIMIT,
835 -DOUBLE_INT_LIMIT,
836 DOUBLE_INT_LIMIT + 2.0,
837 f64::INFINITY,
838 f64::NEG_INFINITY,
839 f64::NAN,
840 ];
841 // A spread of ordinary values, so the agreement is not only about the
842 // corners that were thought of in advance.
843 for i in -400..400 {
844 cases.push(f64::from(i) / 7.0);
845 cases.push(f64::from(i) * 1e12);
846 }
847 for d in cases {
848 let mut v = Vec::new();
849 push_double(&mut v, d);
850 let mut buf = [0u8; DOUBLE_MAX];
851 assert_eq!(write_double(&mut buf, d), &v[..], "writing {d}");
852 }
853 }
854
855 /// The fixed buffer is big enough for the widest double there is.
856 ///
857 /// `write_double` truncates rather than panicking if it is not, so a bad
858 /// constant would show up as a wrong answer somewhere far away instead of
859 /// here.
860 #[test]
861 fn the_fixed_buffer_holds_the_widest_double() {
862 let mut widest = 0;
863 for d in [f64::MIN, f64::MAX, f64::from_bits(1), -f64::from_bits(1)] {
864 let mut v = Vec::new();
865 push_double(&mut v, d);
866 widest = widest.max(v.len());
867 }
868 assert!(widest <= DOUBLE_MAX, "{widest} bytes needs more than room");
869 }
870
871 /// Seventeen significant digits, the two forms, and the trailing zeros off
872 /// both of them.
873 ///
874 /// The expected bytes here are what C's `%.17g` prints, which is what Redis
875 /// replies to `AROP` with, and it is not what the rest of the server writes
876 /// for a double: three tenths is `0.29999999999999999` in this printer and
877 /// `0.3` in the other one.
878 #[test]
879 fn the_aggregate_printer_writes_seventeen_significant_digits() {
880 let cases: &[(f64, &str)] = &[
881 (0.0, "0"),
882 (-0.0, "-0"),
883 (1.0, "1"),
884 (-1.0, "-1"),
885 (0.5, "0.5"),
886 (0.1, "0.10000000000000001"),
887 (0.3, "0.29999999999999999"),
888 (0.1 + 0.2, "0.30000000000000004"),
889 (1.0 / 3.0, "0.33333333333333331"),
890 (0.0001, "0.0001"),
891 // Below a ten thousandth is where the exponent form starts, and C
892 // writes two exponent digits where Rust would write one.
893 (1.5e-5, "1.5e-05"),
894 (1e-5, "1.0000000000000001e-05"),
895 (1e16, "10000000000000000"),
896 // And it starts again once the digits run out at seventeen.
897 (1e17, "1e+17"),
898 (1e30, "1e+30"),
899 (-1e30, "-1e+30"),
900 (1e100, "1e+100"),
901 (f64::MAX, "1.7976931348623157e+308"),
902 (f64::from_bits(1), "4.9406564584124654e-324"),
903 (12345678901234567.0, "12345678901234568"),
904 (f64::INFINITY, "inf"),
905 (f64::NEG_INFINITY, "-inf"),
906 (f64::NAN, "nan"),
907 ];
908 for &(d, want) in cases {
909 let mut buf = [0u8; DOUBLE_MAX];
910 assert_eq!(
911 core::str::from_utf8(write_g17(&mut buf, d)).unwrap(),
912 want,
913 "writing {d}"
914 );
915 let mut v = Vec::new();
916 push_g17(&mut v, d);
917 assert_eq!(String::from_utf8(v).unwrap(), want, "appending {d}");
918 }
919 }
920}