Skip to main content

rupg_types/
numeric_layout.rs

1//! The values of `NUMERIC`, the decimal number of PostgreSQL with no fixed width and no fixed scale, as the bytes that a vector holds.
2//!
3//! The number is the one `NumericVar` holds in `src/backend/utils/adt/numeric.c`: a sign, a weight, base-10000 digits and a display scale. [`crate::Numeric`] does the arithmetic, the input and the output on that form, and this module only packs it.
4//!
5//! The first byte is the class of the value, in the order of the values: negative infinity, a negative number, zero, a positive number, positive infinity and `NaN`, as PostgreSQL sorts them. A number then has its weight and its digits, with no zero digit at either end. A negative number has them flipped and a last word of `0xffff`, so that a longer negative number sorts first. The last two bytes are the display scale. Thus the bytes before the last two order as the numbers do, and `1.0` and `1.00` have the same [`key`] and different bytes.
6//!
7//! Lifted from `crates/rudb-common/src/numeric.rs` of tamnd/rudb at f5f7065a (spec/04 section 4.9).
8
9/// `NUMERIC_POS`, `NUMERIC_NEG`, `NUMERIC_NAN`, `NUMERIC_PINF` and `NUMERIC_NINF`, the sign codes of PostgreSQL.
10pub const POSITIVE: u16 = 0x0000;
11pub const NEGATIVE: u16 = 0x4000;
12pub const NAN: u16 = 0xc000;
13pub const INFINITY: u16 = 0xd000;
14pub const NEGATIVE_INFINITY: u16 = 0xf000;
15
16const CLASS_NEGATIVE_INFINITY: u8 = 1;
17const CLASS_NEGATIVE: u8 = 2;
18const CLASS_ZERO: u8 = 3;
19const CLASS_POSITIVE: u8 = 4;
20const CLASS_INFINITY: u8 = 5;
21const CLASS_NAN: u8 = 6;
22
23/// A value taken apart. A special value has no digits, a weight of 0 and a display scale of 0.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct Unpacked {
26    /// One of the sign codes above.
27    pub sign: u16,
28    /// The power of 10000 of the first digit.
29    pub weight: i16,
30    /// The number of decimal digits that the text shows after the point.
31    pub dscale: u16,
32    /// The base-10000 digits, the most significant first, with no zero at either end.
33    pub digits: Vec<i16>,
34}
35
36/// The bytes of a value. Zero digits at either end are dropped, and a number with no digits left is zero.
37#[must_use]
38pub fn encode(sign: u16, weight: i16, dscale: u16, digits: &[i16]) -> Vec<u8> {
39    let lead = digits.iter().take_while(|&&digit| digit == 0).count();
40    let digits = &digits[lead..];
41    let tail = digits.iter().rev().take_while(|&&digit| digit == 0).count();
42    let digits = &digits[..digits.len() - tail];
43    #[expect(clippy::cast_possible_truncation, reason = "a weight is far below 2^15 digits")]
44    let weight = weight.wrapping_sub(lead as i16);
45    let class = match sign {
46        NAN => CLASS_NAN,
47        INFINITY => CLASS_INFINITY,
48        NEGATIVE_INFINITY => CLASS_NEGATIVE_INFINITY,
49        _ if digits.is_empty() => CLASS_ZERO,
50        NEGATIVE => CLASS_NEGATIVE,
51        _ => CLASS_POSITIVE,
52    };
53    let mut bytes = Vec::with_capacity(5 + 2 * digits.len() + 2);
54    bytes.push(class);
55    let words = std::iter::once(weight as u16 ^ 0x8000).chain(digits.iter().map(|&d| d as u16));
56    match class {
57        CLASS_POSITIVE => words.for_each(|word| bytes.extend_from_slice(&word.to_be_bytes())),
58        CLASS_NEGATIVE => {
59            // The weight flips, and each digit is stored as 0xfffe less the digit so that the end word 0xffff is above every digit.
60            let mut words = words;
61            let weight = words.next().unwrap_or_default();
62            bytes.extend_from_slice(&(!weight).to_be_bytes());
63            for digit in words {
64                bytes.extend_from_slice(&(0xfffe - digit).to_be_bytes());
65            }
66            bytes.extend_from_slice(&0xffffu16.to_be_bytes());
67        }
68        _ => {}
69    }
70    let dscale =
71        if matches!(class, CLASS_POSITIVE | CLASS_NEGATIVE | CLASS_ZERO) { dscale } else { 0 };
72    bytes.extend_from_slice(&dscale.to_be_bytes());
73    bytes
74}
75
76/// The value of bytes that [`encode`] wrote. Bytes that it cannot have written read as `NaN`.
77#[must_use]
78pub fn decode(bytes: &[u8]) -> Unpacked {
79    let special = |sign| Unpacked { sign, weight: 0, dscale: 0, digits: Vec::new() };
80    let Some((&class, rest)) = bytes.split_first() else {
81        return special(NAN);
82    };
83    let dscale = match rest.len().checked_sub(2) {
84        Some(at) => u16::from_be_bytes([rest[at], rest[at + 1]]),
85        None => return special(NAN),
86    };
87    let body = &rest[..rest.len() - 2];
88    let words = || body.as_chunks::<2>().0.iter().map(|&pair| u16::from_be_bytes(pair));
89    match class {
90        CLASS_ZERO => Unpacked { sign: POSITIVE, weight: 0, dscale, digits: Vec::new() },
91        CLASS_POSITIVE => {
92            let mut words = words();
93            let weight = (words.next().unwrap_or(0x8000) ^ 0x8000) as i16;
94            let digits = words.map(|word| word as i16).collect();
95            Unpacked { sign: POSITIVE, weight, dscale, digits }
96        }
97        CLASS_NEGATIVE => {
98            let mut words = words();
99            let weight = (!words.next().unwrap_or(0x7fff) ^ 0x8000) as i16;
100            let digits =
101                words.take_while(|&word| word != 0xffff).map(|word| (0xfffe - word) as i16);
102            Unpacked { sign: NEGATIVE, weight, dscale, digits: digits.collect() }
103        }
104        CLASS_INFINITY => special(INFINITY),
105        CLASS_NEGATIVE_INFINITY => special(NEGATIVE_INFINITY),
106        _ => special(NAN),
107    }
108}
109
110/// The bytes that order and compare as the number does, which leave out the display scale.
111#[must_use]
112pub fn key(bytes: &[u8]) -> &[u8] {
113    &bytes[..bytes.len().saturating_sub(2)]
114}
115
116/// The text of a value, as `numeric_out` writes it: the digits with exactly the display scale.
117#[must_use]
118pub fn to_text(bytes: &[u8]) -> String {
119    let value = decode(bytes);
120    match value.sign {
121        NAN => return "NaN".to_owned(),
122        INFINITY => return "Infinity".to_owned(),
123        NEGATIVE_INFINITY => return "-Infinity".to_owned(),
124        _ => {}
125    }
126    let mut out = String::new();
127    if value.sign == NEGATIVE {
128        out.push('-');
129    }
130    let digit = |weight: i32| -> i32 {
131        let at = i32::from(value.weight) - weight;
132        usize::try_from(at).ok().and_then(|at| value.digits.get(at)).map_or(0, |&d| i32::from(d))
133    };
134    // The digits before the point, the first with no leading zeros.
135    if value.weight < 0 {
136        out.push('0');
137    } else {
138        for weight in (0..=i32::from(value.weight)).rev() {
139            let d = digit(weight);
140            if weight == i32::from(value.weight) {
141                out.push_str(&d.to_string());
142            } else {
143                out.push_str(&format!("{d:04}"));
144            }
145        }
146    }
147    if value.dscale > 0 {
148        out.push('.');
149        let mut written = 0;
150        let mut weight = -1;
151        while written < usize::from(value.dscale) {
152            let group = format!("{:04}", digit(weight));
153            let take = (usize::from(value.dscale) - written).min(4);
154            out.push_str(&group[..take]);
155            written += take;
156            weight -= 1;
157        }
158    }
159    out
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    fn bytes(sign: u16, weight: i16, dscale: u16, digits: &[i16]) -> Vec<u8> {
167        encode(sign, weight, dscale, digits)
168    }
169
170    #[test]
171    fn the_bytes_order_as_the_numbers_do() {
172        let values = [
173            bytes(NEGATIVE_INFINITY, 0, 0, &[]),
174            bytes(NEGATIVE, 1, 0, &[1, 0]),
175            bytes(NEGATIVE, 0, 1, &[1, 5000]),
176            bytes(NEGATIVE, 0, 0, &[1]),
177            bytes(NEGATIVE, -1, 4, &[1]),
178            bytes(POSITIVE, 0, 2, &[]),
179            bytes(POSITIVE, -1, 4, &[1]),
180            bytes(POSITIVE, 0, 0, &[1]),
181            bytes(POSITIVE, 0, 1, &[1, 5000]),
182            bytes(POSITIVE, 1, 0, &[1, 0]),
183            bytes(INFINITY, 0, 0, &[]),
184            bytes(NAN, 0, 0, &[]),
185        ];
186        for pair in values.windows(2) {
187            assert!(key(&pair[0]) < key(&pair[1]), "{} < {}", to_text(&pair[0]), to_text(&pair[1]));
188        }
189        assert_eq!(key(&bytes(POSITIVE, 0, 1, &[1])), key(&bytes(POSITIVE, 0, 2, &[1, 0])));
190    }
191
192    #[test]
193    fn a_value_reads_back_and_prints_with_its_scale() {
194        for (sign, weight, dscale, digits, text) in [
195            (POSITIVE, 0, 20, &[0, 3333, 3333, 3333, 3333, 3333][..], "0.33333333333333333333"),
196            (NEGATIVE, 1, 2, &[12, 3456, 7800][..], "-123456.78"),
197            (POSITIVE, 0, 3, &[][..], "0.000"),
198            (POSITIVE, -2, 6, &[33][..], "0.000000"),
199            (POSITIVE, -2, 8, &[33][..], "0.00000033"),
200            (NAN, 0, 0, &[][..], "NaN"),
201            (NEGATIVE_INFINITY, 0, 0, &[][..], "-Infinity"),
202        ] {
203            let packed = bytes(sign, weight, dscale, digits);
204            assert_eq!(to_text(&packed), text);
205            let back = decode(&packed);
206            assert_eq!(encode(back.sign, back.weight, back.dscale, &back.digits), packed);
207        }
208    }
209}