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/// Appends a distance the way the geo commands write one, which is four digits
422/// after the point and no exponent ever.
423///
424/// `GEODIST` and the `WITHDIST` half of a search go through Redis's
425/// `fixedpoint_d2string` rather than through `d2string`, because "166.2742 km
426/// away" reads better than "166.27415156960033 km away" and four places is
427/// still a tenth of a metre when the unit is the kilometre. The trailing zeros
428/// stay, so a whole number of metres comes back as `5.0000` and a distance of
429/// nothing comes back as `0.0000`.
430///
431/// The scaled value is rounded to the nearest, ties to even, which is what
432/// `llrint` does in the default rounding mode and therefore what a real server
433/// answers. Ties are not reachable in practice, since the value being rounded
434/// came out of a square root, but rounding the other way would still be a
435/// divergence that only showed up in somebody's test suite.
436///
437/// A distance too large to scale into an integer writes nothing, which is what
438/// Redis does too: its formatter fails and hands the reply an empty string.
439/// Nothing reaches that from a real search, because the far side of the world
440/// is twenty thousand kilometres away and the scaled form of that is twelve
441/// digits.
442pub fn push_fixed4(out: &mut Vec<u8>, d: f64) {
443 let scaled = (d * 10_000.0).round_ties_even();
444 if !scaled.is_finite() || scaled.abs() >= DOUBLE_INT_LIMIT {
445 return;
446 }
447 let mut whole = scaled as i64;
448 if whole < 0 {
449 out.push(b'-');
450 whole = -whole;
451 }
452 let mut buf = [0u8; DIGITS_MAX];
453 let digits = u64_digits(&mut buf, whole as u64);
454 // Four digits or fewer means there is no integer part, and Redis writes a
455 // zero in front rather than leaving a reply that starts with a point. The
456 // padding is what is left of the four places once the digits are in.
457 if let Some(padding) = 4usize.checked_sub(digits.len()) {
458 out.extend_from_slice(b"0.");
459 out.extend_from_slice(&b"0000"[..padding]);
460 out.extend_from_slice(digits);
461 } else {
462 let (front, back) = digits.split_at(digits.len() - 4);
463 out.extend_from_slice(front);
464 out.push(b'.');
465 out.extend_from_slice(back);
466 }
467}
468
469/// Writes a double into a fixed buffer, byte for byte what [`push_double`]
470/// would append.
471///
472/// The two are the same code now, and this is the one that does the work,
473/// because the digit generator wants somewhere to put eighteen digits before it
474/// knows how many of them it is going to keep. The caller that needs it as a
475/// buffer rather than as a reply is the array type, which stores a value as a
476/// double only when the double prints back as the exact bytes the client sent,
477/// so it formats a candidate, compares, and usually throws it away.
478pub fn write_double(buf: &mut [u8; DOUBLE_MAX], d: f64) -> &[u8] {
479 // Zero first, so that the sign of a negative zero survives. The integer
480 // path below would lose it and Redis checks in this order for that reason.
481 if d == 0.0 {
482 let n = if d.is_sign_negative() {
483 buf[..2].copy_from_slice(b"-0");
484 2
485 } else {
486 buf[0] = b'0';
487 1
488 };
489 return &buf[..n];
490 }
491 if d.is_nan() {
492 buf[..3].copy_from_slice(b"nan");
493 return &buf[..3];
494 }
495 if d.is_infinite() {
496 let text: &[u8] = if d > 0.0 { b"inf" } else { b"-inf" };
497 buf[..text.len()].copy_from_slice(text);
498 return &buf[..text.len()];
499 }
500 if d.fract() == 0.0 && d.abs() <= DOUBLE_INT_LIMIT {
501 let mut digits = [0u8; DIGITS_MAX];
502 let text = i64_digits(&mut digits, d as i64);
503 let n = text.len();
504 buf[..n].copy_from_slice(text);
505 return &buf[..n];
506 }
507 let n = crate::dtoa::dtoa(d, buf);
508 &buf[..n]
509}
510
511/// Writes a double the way C's `%.17g` writes one, which is what `AROP`
512/// replies with.
513///
514/// Redis formats an aggregate through `ld2string` in its automatic mode, and
515/// that mode is a plain `%.17Lg`, so this is the one reply in the whole server
516/// that is not a shortest round trip printer. The difference is visible: three
517/// tenths comes back as `0.29999999999999999` here and as `0.3` from `ZSCORE`,
518/// because seventeen significant digits of the nearest double to three tenths
519/// really are those.
520///
521/// `%g` picks between the two forms the way C says: the exponent form when the
522/// decimal exponent is below minus four or at least the precision, the plain
523/// form otherwise, and trailing zeros come off either way.
524pub fn write_g17(buf: &mut [u8; DOUBLE_MAX], d: f64) -> &[u8] {
525 /// Seventeen significant digits is sixteen after the point.
526 const AFTER: usize = 16;
527 if d.is_nan() {
528 buf[..3].copy_from_slice(b"nan");
529 return &buf[..3];
530 }
531 if d.is_infinite() {
532 let word: &[u8] = if d > 0.0 { b"inf" } else { b"-inf" };
533 buf[..word.len()].copy_from_slice(word);
534 return &buf[..word.len()];
535 }
536 // The exponent C would use is the one the value has after it has been
537 // rounded to seventeen digits, so it has to come from the rounding and not
538 // from a logarithm: 9.9999999999999999e-5 rounds up into the next decade.
539 let mut scratch = [0u8; DOUBLE_MAX];
540 let mut sink = SliceSink {
541 buf: &mut scratch,
542 at: 0,
543 };
544 let _ = write!(sink, "{d:.AFTER$e}");
545 let end = sink.at;
546 let split = scratch[..end]
547 .iter()
548 .position(|&c| c == b'e')
549 .expect("the exponent form always has one");
550 let exp = parse_i64(&scratch[split + 1..end]).expect("a written exponent parses") as i32;
551
552 if !(-4..17).contains(&exp) {
553 // The exponent form, and C writes at least two exponent digits where
554 // Rust writes as few as one.
555 let mantissa = trim_zeros(&scratch[..split]);
556 let n = mantissa.len();
557 buf[..n].copy_from_slice(mantissa);
558 let mut sink = SliceSink { buf, at: n };
559 let sign = if exp < 0 { '-' } else { '+' };
560 let _ = write!(sink, "e{sign}{:02}", exp.unsigned_abs());
561 let at = sink.at;
562 return &buf[..at];
563 }
564 // The plain form, whose precision is what is left of the seventeen digits
565 // once the integer part has had its share.
566 let places = usize::try_from(AFTER as i32 - exp).unwrap_or(0);
567 let mut sink = SliceSink { buf, at: 0 };
568 let _ = write!(sink, "{d:.places$}");
569 let at = sink.at;
570 let n = trim_zeros(&buf[..at]).len();
571 &buf[..n]
572}
573
574/// The same digits [`write_g17`] would write, appended.
575pub fn push_g17(out: &mut Vec<u8>, d: f64) {
576 let mut buf = [0u8; DOUBLE_MAX];
577 out.extend_from_slice(write_g17(&mut buf, d));
578}
579
580/// Takes the trailing zeros off a fixed point number, and the point with them
581/// when nothing is left after it.
582///
583/// A number with no point in it is left alone, because the zeros in `1700` are
584/// not trailing anything.
585fn trim_zeros(text: &[u8]) -> &[u8] {
586 if !text.contains(&b'.') {
587 return text;
588 }
589 let end = text.iter().rposition(|&c| c != b'0').unwrap_or(0);
590 if text[end] == b'.' {
591 &text[..end]
592 } else {
593 &text[..=end]
594 }
595}
596
597/// A `core::fmt::Write` that fills a fixed buffer and stops when it is full.
598///
599/// Running out of room cannot happen here, because [`DOUBLE_MAX`] is sized for
600/// the widest double there is, and it is handled rather than asserted so that a
601/// mistake in that reasoning truncates a number instead of killing a shard.
602struct SliceSink<'a> {
603 buf: &'a mut [u8; DOUBLE_MAX],
604 at: usize,
605}
606
607impl core::fmt::Write for SliceSink<'_> {
608 fn write_str(&mut self, s: &str) -> core::fmt::Result {
609 let n = s.len().min(self.buf.len() - self.at);
610 self.buf[self.at..self.at + n].copy_from_slice(&s.as_bytes()[..n]);
611 self.at += n;
612 Ok(())
613 }
614}
615
616/// A `core::fmt::Write` that appends UTF-8 to a byte buffer.
617///
618/// The float printer only speaks `fmt::Write` and the reply buffer is bytes.
619/// This is the whole adapter, and it exists so that no reply path anywhere ever
620/// builds a `String` it immediately throws away.
621struct Utf8Sink<'a>(&'a mut Vec<u8>);
622
623impl core::fmt::Write for Utf8Sink<'_> {
624 fn write_str(&mut self, s: &str) -> core::fmt::Result {
625 self.0.extend_from_slice(s.as_bytes());
626 Ok(())
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633
634 fn text(n: i64) -> String {
635 let mut v = Vec::new();
636 push_i64(&mut v, n);
637 String::from_utf8(v).unwrap()
638 }
639
640 #[test]
641 fn integers_round_trip_through_text() {
642 for n in [
643 0,
644 1,
645 9,
646 10,
647 99,
648 100,
649 -1,
650 -9,
651 -10,
652 12345,
653 -12345,
654 i64::MAX,
655 i64::MIN,
656 ] {
657 assert_eq!(text(n), n.to_string(), "writing {n}");
658 assert_eq!(parse_i64(text(n).as_bytes()), Some(n), "reading {n}");
659 }
660 }
661
662 #[test]
663 fn the_length_is_known_before_the_digits_are_written() {
664 for n in [0, 5, 42, -42, 999, 1000, i64::MAX, i64::MIN] {
665 assert_eq!(i64_len(n), text(n).len(), "length of {n}");
666 }
667 }
668
669 /// Every boundary of the two digit loop, since an off by one there is a
670 /// wrong length header rather than a crash and would be found by a client.
671 #[test]
672 fn every_length_of_number_is_written_correctly() {
673 let mut n: u64 = 0;
674 for _ in 0..20 {
675 for probe in [n, n + 1, n.saturating_sub(1)] {
676 let mut v = Vec::new();
677 push_u64(&mut v, probe);
678 assert_eq!(v, probe.to_string().as_bytes(), "writing {probe}");
679 }
680 n = n.saturating_mul(10).max(9);
681 if n == u64::MAX {
682 break;
683 }
684 }
685 }
686
687 #[test]
688 fn the_stack_form_writes_what_the_vec_form_writes() {
689 // Two implementations of the same digits is the shape of bug that only
690 // shows at one boundary, so this checks them against each other rather
691 // than against a literal.
692 let mut buf = [0u8; DIGITS_MAX];
693 let mut n: i64 = 0;
694 for _ in 0..19 {
695 for probe in [n, -n, n + 1, n - 1] {
696 assert_eq!(i64_digits(&mut buf, probe), text(probe).as_bytes());
697 }
698 n = n.saturating_mul(10).max(9);
699 }
700 assert_eq!(i64_digits(&mut buf, i64::MIN), text(i64::MIN).as_bytes());
701 assert_eq!(i64_digits(&mut buf, i64::MAX), text(i64::MAX).as_bytes());
702 assert_eq!(i64_digits(&mut buf, 0), b"0", "and zero is one digit");
703
704 // And the unsigned form past where the signed one stops, which is the
705 // whole reason it is there.
706 for probe in [0, 1, u64::MAX, 1 << 63, i64::MAX as u64 + 1] {
707 let mut v = Vec::new();
708 push_u64(&mut v, probe);
709 assert_eq!(u64_digits(&mut buf, probe), v.as_slice(), "{probe}");
710 }
711 }
712
713 #[test]
714 fn the_parser_refuses_what_redis_refuses() {
715 for bad in [
716 &b""[..],
717 b"-",
718 b"+1",
719 b"01",
720 b"-01",
721 b" 1",
722 b"1 ",
723 b"1a",
724 b"a",
725 b"1.0",
726 b"-0",
727 b"-00",
728 b"9223372036854775808",
729 b"-9223372036854775809",
730 b"99999999999999999999999",
731 ] {
732 assert_eq!(parse_i64(bad), None, "{:?} should not parse", bad);
733 }
734 // The one leading zero that is a number, and the one negative that only
735 // exists going downwards.
736 assert_eq!(parse_i64(b"0"), Some(0));
737 assert_eq!(parse_i64(b"-9223372036854775808"), Some(i64::MIN));
738 }
739
740 #[test]
741 fn the_float_parser_refuses_what_redis_refuses() {
742 assert_eq!(parse_f64(b"3.5"), Some(3.5));
743 assert_eq!(parse_f64(b"-0"), Some(-0.0));
744 assert_eq!(parse_f64(b"3.0e3"), Some(3000.0));
745 assert_eq!(parse_f64(b"inf"), Some(f64::INFINITY));
746 assert_eq!(parse_f64(b"-inf"), Some(f64::NEG_INFINITY));
747 // No whitespace anywhere, nothing trailing, and no NaN, because a
748 // stored NaN compares false against itself for the rest of time.
749 assert_eq!(parse_f64(b" 3.5"), None);
750 assert_eq!(parse_f64(b"3.5 "), None);
751 assert_eq!(parse_f64(b"3.5x"), None);
752 assert_eq!(parse_f64(b""), None);
753 assert_eq!(parse_f64(b"nan"), None);
754 }
755
756 #[test]
757 fn the_float_parser_takes_hexadecimal_because_strtold_does() {
758 // Every one of these was read off a real 8.10.1 before it was written
759 // down here.
760 assert_eq!(parse_f64(b"0x10"), Some(16.0));
761 assert_eq!(parse_f64(b"0X10"), Some(16.0));
762 assert_eq!(parse_f64(b"0X1p4"), Some(16.0));
763 assert_eq!(parse_f64(b"0x1.8p1"), Some(3.0));
764 assert_eq!(parse_f64(b"-0x1.8p1"), Some(-3.0));
765 assert_eq!(parse_f64(b"+0x10"), Some(16.0));
766 assert_eq!(parse_f64(b"0x1p-1"), Some(0.5));
767 assert_eq!(parse_f64(b"0xff"), Some(255.0));
768
769 // A string that starts like a hexadecimal number and then stops being
770 // one is refused rather than falling through to the decimal parser,
771 // which would read the leading zero and call it a day.
772 assert_eq!(parse_f64(b"0x"), None);
773 assert_eq!(parse_f64(b"0xzz"), None);
774 assert_eq!(parse_f64(b"0x1p"), None);
775 assert_eq!(parse_f64(b"0x1.2.3"), None);
776 assert_eq!(parse_f64(b"0x10x"), None);
777 assert_eq!(parse_f64(b"0x1p99999"), None);
778 }
779
780 #[test]
781 fn a_mantissa_longer_than_a_double_still_lands_in_the_right_place() {
782 // Seventeen hex digits, one more than a u64 holds. The digits past the
783 // end are worth four binary places each and nothing else, which is all
784 // a double can use them for anyway.
785 assert_eq!(
786 parse_f64(b"0x10000000000000000"),
787 Some(18446744073709551616.0)
788 );
789 assert_eq!(parse_f64(b"0x1p1024"), None);
790 }
791
792 #[test]
793 fn doubles_are_written_the_way_redis_writes_them() {
794 let cases: &[(f64, &str)] = &[
795 (0.0, "0"),
796 // Redis checks for zero before it checks for an integer, so this
797 // keeps its sign where the integer printer would have dropped it.
798 (-0.0, "-0"),
799 (3.0, "3"),
800 (-3.0, "-3"),
801 (3.5, "3.5"),
802 (0.1, "0.1"),
803 // The integer printer reaches two to the sixty second, and past it
804 // the digit generator takes over and switches to an exponent.
805 (4.611686018427388e18, "4611686018427387904"),
806 (1e19, "1e+19"),
807 (1e30, "1e+30"),
808 (1e-7, "1e-7"),
809 (1e-6, "0.000001"),
810 (5e-324, "5e-324"),
811 (f64::INFINITY, "inf"),
812 (f64::NEG_INFINITY, "-inf"),
813 (f64::NAN, "nan"),
814 ];
815 for &(d, want) in cases {
816 let mut v = Vec::new();
817 push_double(&mut v, d);
818 assert_eq!(String::from_utf8(v).unwrap(), want, "writing {d}");
819 }
820 }
821
822 /// The human printer is the other one, and the difference is the exponent.
823 ///
824 /// `INCRBYFLOAT` and `HINCRBYFLOAT` are the only two commands that use it,
825 /// and the reason it exists as a separate thing is the last four rows: a
826 /// fixed point conversion has no exponent form to switch to, so a magnitude
827 /// that comes back as `1e+30` from a score comes back written out in full
828 /// from an increment.
829 #[test]
830 fn the_increment_printer_never_writes_an_exponent() {
831 let cases: &[(f64, &str)] = &[
832 (0.0, "0"),
833 // The human mode says so explicitly, where `d2string` keeps it.
834 (-0.0, "0"),
835 (3.0, "3"),
836 (3.5, "3.5"),
837 (0.1, "0.1"),
838 (10.5, "10.5"),
839 (0.30000000000000004, "0.30000000000000004"),
840 (1e30, "1000000000000000000000000000000"),
841 (1e19, "10000000000000000000"),
842 (1e-7, "0.0000001"),
843 (f64::INFINITY, "inf"),
844 (f64::NEG_INFINITY, "-inf"),
845 (f64::NAN, "nan"),
846 ];
847 for &(d, want) in cases {
848 let mut v = Vec::new();
849 push_human(&mut v, d);
850 assert_eq!(String::from_utf8(v).unwrap(), want, "writing {d}");
851 }
852 // The smallest subnormal, which is where the lack of an exponent form
853 // costs the most: `0.` and then three hundred and twenty four places.
854 let mut v = Vec::new();
855 push_human(&mut v, 5e-324);
856 assert_eq!(v.len(), 326);
857 assert!(v.starts_with(b"0.0") && v.ends_with(b"5"));
858 }
859
860 #[test]
861 fn a_distance_always_has_four_places_after_the_point() {
862 let cases: &[(f64, &str)] = &[
863 // Every one of these came off a running 8.10.1, through GEODIST and
864 // through WITHDIST, in all four units.
865 (0.0, "0.0000"),
866 (-0.0, "0.0000"),
867 (166_274.151_561_39, "166274.1516"),
868 (166.274_151_561_39, "166.2742"),
869 (103.318_154_263_49, "103.3182"),
870 (545_518.869_950_1, "545518.8700"),
871 (5.0, "5.0000"),
872 // Under one, where the integer part is a zero that is written rather
873 // than counted, and under a ten thousandth, where every digit of the
874 // answer is a leading zero.
875 (0.5, "0.5000"),
876 (0.05, "0.0500"),
877 (0.005, "0.0050"),
878 (0.0005, "0.0005"),
879 (0.000_04, "0.0000"),
880 // The tie goes to the even digit, which is what llrint does.
881 (0.000_25, "0.0002"),
882 (0.000_35, "0.0004"),
883 (-1.5, "-1.5000"),
884 ];
885 for &(d, want) in cases {
886 let mut v = Vec::new();
887 push_fixed4(&mut v, d);
888 assert_eq!(String::from_utf8(v).unwrap(), want, "writing {d}");
889 }
890 // Nothing at all for the values a real server's formatter refuses,
891 // which no search can produce and a client can still ask for.
892 for d in [f64::INFINITY, f64::NAN, 1e30] {
893 let mut v = Vec::new();
894 push_fixed4(&mut v, d);
895 assert!(v.is_empty(), "writing {d}");
896 }
897 }
898
899 /// The two double writers have to agree, because one is used to predict the
900 /// other.
901 ///
902 /// The array type decides whether a value can be stored as a double by
903 /// formatting it with `write_double` and checking the bytes against what the
904 /// client sent, and then the reply comes out of `push_double`. If they ever
905 /// disagreed, a value would go in as a number and come back out as
906 /// different text.
907 #[test]
908 fn the_two_double_writers_agree() {
909 let mut cases = vec![
910 0.0,
911 -0.0,
912 1.0,
913 -1.0,
914 3.5,
915 0.1,
916 -0.1,
917 1e-320,
918 f64::MIN_POSITIVE,
919 f64::MAX,
920 f64::MIN,
921 DOUBLE_INT_LIMIT,
922 -DOUBLE_INT_LIMIT,
923 DOUBLE_INT_LIMIT + 2.0,
924 f64::INFINITY,
925 f64::NEG_INFINITY,
926 f64::NAN,
927 ];
928 // A spread of ordinary values, so the agreement is not only about the
929 // corners that were thought of in advance.
930 for i in -400..400 {
931 cases.push(f64::from(i) / 7.0);
932 cases.push(f64::from(i) * 1e12);
933 }
934 for d in cases {
935 let mut v = Vec::new();
936 push_double(&mut v, d);
937 let mut buf = [0u8; DOUBLE_MAX];
938 assert_eq!(write_double(&mut buf, d), &v[..], "writing {d}");
939 }
940 }
941
942 /// The fixed buffer is big enough for the widest double there is.
943 ///
944 /// `write_double` truncates rather than panicking if it is not, so a bad
945 /// constant would show up as a wrong answer somewhere far away instead of
946 /// here.
947 #[test]
948 fn the_fixed_buffer_holds_the_widest_double() {
949 let mut widest = 0;
950 for d in [f64::MIN, f64::MAX, f64::from_bits(1), -f64::from_bits(1)] {
951 let mut v = Vec::new();
952 push_double(&mut v, d);
953 widest = widest.max(v.len());
954 }
955 assert!(widest <= DOUBLE_MAX, "{widest} bytes needs more than room");
956 }
957
958 /// Seventeen significant digits, the two forms, and the trailing zeros off
959 /// both of them.
960 ///
961 /// The expected bytes here are what C's `%.17g` prints, which is what Redis
962 /// replies to `AROP` with, and it is not what the rest of the server writes
963 /// for a double: three tenths is `0.29999999999999999` in this printer and
964 /// `0.3` in the other one.
965 #[test]
966 fn the_aggregate_printer_writes_seventeen_significant_digits() {
967 let cases: &[(f64, &str)] = &[
968 (0.0, "0"),
969 (-0.0, "-0"),
970 (1.0, "1"),
971 (-1.0, "-1"),
972 (0.5, "0.5"),
973 (0.1, "0.10000000000000001"),
974 (0.3, "0.29999999999999999"),
975 (0.1 + 0.2, "0.30000000000000004"),
976 (1.0 / 3.0, "0.33333333333333331"),
977 (0.0001, "0.0001"),
978 // Below a ten thousandth is where the exponent form starts, and C
979 // writes two exponent digits where Rust would write one.
980 (1.5e-5, "1.5e-05"),
981 (1e-5, "1.0000000000000001e-05"),
982 (1e16, "10000000000000000"),
983 // And it starts again once the digits run out at seventeen.
984 (1e17, "1e+17"),
985 (1e30, "1e+30"),
986 (-1e30, "-1e+30"),
987 (1e100, "1e+100"),
988 (f64::MAX, "1.7976931348623157e+308"),
989 (f64::from_bits(1), "4.9406564584124654e-324"),
990 (12345678901234567.0, "12345678901234568"),
991 (f64::INFINITY, "inf"),
992 (f64::NEG_INFINITY, "-inf"),
993 (f64::NAN, "nan"),
994 ];
995 for &(d, want) in cases {
996 let mut buf = [0u8; DOUBLE_MAX];
997 assert_eq!(
998 core::str::from_utf8(write_g17(&mut buf, d)).unwrap(),
999 want,
1000 "writing {d}"
1001 );
1002 let mut v = Vec::new();
1003 push_g17(&mut v, d);
1004 assert_eq!(String::from_utf8(v).unwrap(), want, "appending {d}");
1005 }
1006 }
1007}