surrealdb_expr/expr/decimal.rs
1//! Decimal functionality and extension traits.
2//!
3//! This module provides DecimalLexEncoder, a variable-length, lexicographic
4//! byte encoding for fastnum::D128 values. The encoding ensures that
5//! byte-wise ordering preserves numeric ordering, which is critical for key
6//! construction in indexes. The encoder is stream-friendly: it guarantees an
7//! in-band terminator within the mantissa encoding and appends a trailing 0x00
8//! so decoders can read until the first zero when values are concatenated in
9//! composite keys.
10//!
11//! Where it’s used:
12//! - val::number::Number::{as_decimal_buf, from_decimal_buf}
13//! - key::value::StoreKeyNumber serde impls used in index key material
14//!
15//! Ordering overview:
16//! - Finite negatives < zero < finite positives
17//! - −∞ < all finite < +∞ < NaN
18//!
19//! See the struct-level documentation below for the precise byte format.
20
21use anyhow::Result;
22use fastnum::decimal::{Context, Sign};
23use fastnum::{D128, U128};
24use rust_decimal::Decimal;
25
26use crate::expr::Error;
27
28/// Variable-length lexicographic encoding for D128 values that preserves sort
29/// order.
30///
31/// This encoder converts D128 values into byte sequences that maintain the same
32/// lexicographic ordering as the original decimal values. This is crucial for
33/// database indexing where byte-level comparison must match numeric comparison.
34///
35/// ## Encoding Format
36///
37/// The encoding uses a variable-length format optimized for lexicographic
38/// ordering:
39///
40/// ### Zero Values
41/// - Zero is encoded as a single byte: `0x80`
42/// - This ensures zero sorts between negative and positive numbers
43///
44/// ### Non-Zero Values
45/// The format consists of:
46/// 1. **Class/marker byte** (1 byte):
47/// - `0x80` Zero
48/// - `0x40` Finite negative
49/// - `0xA0` Finite positive
50/// - `0x20` Negative infinity
51/// - `0xC0` Positive infinity
52/// - `0xFF` NaN
53///
54/// 2. **Biased scale** (2 bytes, big-endian):
55/// - We bias the "scale" (not the raw exponent). Scale is defined as: `scale = exponent +
56/// (digit_count - 1)`, i.e., the position of the most-significant digit in a
57/// scientific-notation sense.
58/// - Stored as: `biased = scale + EXP_BIAS` (unsigned 16-bit)
59/// - For negative numbers: stored as `0xFFFF - biased` (one's complement) to reverse order
60/// - EXP_BIAS = 6144. With D128, `exponent ∈ [-6143, +6144]` and `digit_count ∈ [1, 34]`, so
61/// `scale ∈ [-6143, 6177]`, which maps into `[1, 12321]` after biasing, well within `u16`.
62///
63/// 3. **Packed digit representation** (variable length):
64/// - Digits are taken from the absolute value's base-10 representation
65/// - Each pair of digits is packed into one byte (4 bits per digit)
66/// - For positive numbers: stored as-is
67/// - For negative numbers: all bytes are bitwise complemented to reverse ordering
68/// - Termination: encoding stops when a nibble equals `0x0`. This naturally handles both odd and
69/// even digit counts: • odd count: the last byte has a low nibble of 0 • even count: an extra
70/// full terminator byte is appended (0x00 for positives, 0xFF for negatives)
71///
72/// Because a terminator is always present within (or immediately after) the
73/// mantissa, any trailing type-marker byte appended by higher layers will never
74/// be consumed by the mantissa decoder.
75///
76/// ## Properties
77/// - Preserves lexicographic ordering: if `a < b` then `encode(a) < encode(b)`
78/// - Variable length encoding (3+ bytes typical: 1 sign + 2 scale + packed digits)
79/// - Handles full D128 range including extreme values
80/// - Uses packed digit encoding for efficient storage (2 digits per byte)
81pub struct DecimalLexEncoder;
82
83impl DecimalLexEncoder {
84 /// We use a 16-bit biased "scale" (not the raw exponent).
85 /// With D128: exponent ∈ [-6143, +6144] and digit_count ∈ [1, 34], so
86 /// scale = exponent + (digit_count - 1) ∈ [-6143, 6177]. Adding EXP_BIAS
87 /// maps this into [1, 12321], comfortably within u16.
88 const EXP_BIAS: i32 = 6144; // bias used for mapping signed scale into u16 space
89
90 const FINITE_NEGATIVE_MARKER: u8 = 0x40;
91 const FINITE_POSITIVE_MARKER: u8 = 0xA0;
92
93 // 0x80 = 128, middle value for proper ordering
94 const ZERO_MARKER: u8 = 0x80;
95
96 const INFINITE_NEGATIVE_MARKER: u8 = 0x20;
97 const INFINITE_POSITIVE_MAKER: u8 = 0xC0;
98 const NAN_MARKER: u8 = 0xFF;
99
100 /// Encodes a D128 value into a lexicographically ordered byte sequence.
101 ///
102 /// The encoding preserves sort order: if `a < b` then `encode(a) <
103 /// encode(b)`. This is essential for database indexing where byte-level
104 /// comparison must match numeric comparison.
105 pub fn encode(dec: D128) -> Vec<u8> {
106 if dec.is_nan() {
107 return vec![Self::NAN_MARKER, 0x00];
108 }
109
110 // Extract sign
111 let is_negative = dec.is_negative();
112
113 if dec.is_infinite() {
114 if is_negative {
115 return vec![Self::INFINITE_NEGATIVE_MARKER, 0x00];
116 } else {
117 return vec![Self::INFINITE_POSITIVE_MAKER, 0x00];
118 }
119 }
120 // Special case: zero gets a fixed encoding that sorts between negative and
121 // positive
122 if dec.is_zero() {
123 return vec![Self::ZERO_MARKER, 0x00];
124 }
125
126 // Work with absolute value
127 let normalized = dec.abs(); // Get absolute value
128 let e = -normalized.fractional_digits_count() as i32; // Exponent: negative of fractional digits
129 let digit_count = normalized.digits_count();
130
131 // Calculate the scale that positions the first digit as the most significant
132 // This normalizes the number to scientific notation form
133 let scale = e + (digit_count as i32 - 1); // Scale for scientific notation
134
135 // Apply bias to map the scale range to unsigned 16-bit space
136 // For D128, scale ∈ [-6143, 6177] → [1, 12321] after adding EXP_BIAS.
137 let biased_exponent = (scale + Self::EXP_BIAS) as u16;
138
139 let encode_exponent = |e: u16| {
140 let q = (e / 255) as u8;
141 let r = (e % 255) as u8;
142 [q + 1, r + 1]
143 };
144
145 // Build the final encoded result
146 // Capacity: 1 sign + 2 exponent + packed digits (2 digits per byte) + potential
147 // terminator
148 let mut result = Vec::with_capacity(5 + (digit_count + 1).div_ceil(2));
149
150 // Convert the mantissa to decimal string representation for digit packing
151 let radix10 = normalized.digits().to_str_radix(10);
152
153 // Encode sign marker and biased scale based on sign
154 if is_negative {
155 // Sign marker: 0x00 ensures negative numbers sort before positive ones
156 result.push(Self::FINITE_NEGATIVE_MARKER);
157 // Complement of biased scale: reverses ordering so that more negative values
158 // sort first This maintains total ordering for negatives when compared
159 // bytewise.
160 result.extend(encode_exponent(0xFFFF - biased_exponent));
161 // Complement all packed digit bytes to reverse their ordering for negative
162 // numbers
163 Self::pack_digits_negative(&radix10, &mut result);
164 } else {
165 // Sign marker: 0xFF ensures positive numbers sort after negative ones
166 result.push(Self::FINITE_POSITIVE_MARKER);
167 // Biased scale: larger scales (greater magnitude) sort later for positives
168 result.extend(encode_exponent(biased_exponent));
169 // Store packed digit bytes directly for positive numbers
170 Self::pack_digits_positive(&radix10, &mut result);
171 }
172 result.push(0x00);
173 //
174 result
175 }
176
177 /// Decodes a lexicographically encoded byte sequence back to a D128 value.
178 ///
179 /// This reverses the encoding process, reconstructing the original D128
180 /// from its byte representation while handling all the encoding
181 /// transformations.
182 pub fn decode(bytes: &[u8]) -> Result<D128> {
183 // Handle empty buffer
184 if bytes.is_empty() {
185 return Err(Error::Serialization("Cannot decode from empty buffer".to_string()).into());
186 }
187
188 // Special cases
189 let is_negative = match bytes[0] {
190 Self::ZERO_MARKER => {
191 return Ok(D128::ZERO);
192 }
193 Self::INFINITE_NEGATIVE_MARKER => return Ok(D128::NEG_INFINITY),
194 Self::INFINITE_POSITIVE_MAKER => return Ok(D128::INFINITY),
195 Self::NAN_MARKER => return Ok(D128::NAN),
196 Self::FINITE_NEGATIVE_MARKER => true,
197 Self::FINITE_POSITIVE_MARKER => false,
198 marker => {
199 return Err(Error::Serialization(format!("Invalid marker byte: {marker}")).into());
200 }
201 };
202
203 // Need at least 3 bytes: marker (1) + exponent (2)
204 if bytes.len() < 3 {
205 return Err(Error::Serialization(format!("Buffer too short: {}", bytes.len())).into());
206 }
207
208 // Extract biased exponent (2 bytes with shift to avoid 0x00)
209 let biased_exponent = (bytes[1] - 1) as u16 * 255 + (bytes[2] - 1) as u16;
210 // Unbias the scale, handling negative number complement
211 let biased_exponent = if is_negative {
212 // For negative numbers, undo the complement applied during encoding
213 0xFFFF - biased_exponent
214 } else {
215 biased_exponent
216 };
217 // Convert back to the original scale by removing the bias
218 let scale = biased_exponent as i32 - Self::EXP_BIAS;
219
220 // Unpack the digit bytes back to decimal string, handling sign-specific
221 // encoding
222 let (mantissa, digit_count) = if is_negative {
223 Self::unpack_digits_negative(&bytes[3..])?
224 } else {
225 Self::unpack_digits_positive(&bytes[3..])?
226 };
227 if digit_count == 0 {
228 return Err(Error::Serialization("Empty mantissa".to_string()).into());
229 }
230 // Calculate the final exponent: scale minus the position adjustment for
231 // scientific notation
232 let exponent = scale - (digit_count - 1);
233
234 Ok(D128::from_parts(
235 mantissa,
236 exponent,
237 if is_negative {
238 Sign::Minus
239 } else {
240 Sign::Plus
241 },
242 Context::default(),
243 ))
244 }
245
246 /// Packs decimal digits for negative numbers with bit inversion for
247 /// lexicographic ordering. Each pair of ASCII digits is packed into a
248 /// single byte (4 bits each) and then inverted. Mapping: '0'..'9' → 1..10
249 /// (we avoid 0 so that 0 nibbles can be used as terminators).
250 /// For odd digit counts, the last byte has a zero low nibble; for even
251 /// counts, an extra 0xFF terminator byte is appended after bit inversion.
252 fn pack_digits_negative(radix10: &str, buf: &mut Vec<u8>) {
253 let mut iter = radix10.as_bytes().chunks_exact(2);
254 for pair in &mut iter {
255 // pair is &[u8; 2]
256 // Convert ASCII digits to numeric values: '0' (48) -> 1, '1' (49) -> 2, etc.
257 // We subtract 47 instead of 48 to map '0'->1, '1'->2, ..., '9'->10
258 // This ensures no digit maps to 0, which we use as termination marker
259 let hi = pair[0] - 47;
260 let lo = pair[1] - 47;
261 let packed = (hi << 4) | lo;
262 buf.push(!packed); // Invert bits for negative number lexicographic ordering
263 }
264 // If the length is odd, the remainder (the last lone byte) is here:
265 if let Some(remainder) = iter.remainder().first() {
266 let hi = remainder - 47;
267 let packed = hi << 4;
268 buf.push(!packed);
269 } else {
270 // Set the termination byte (inverted)
271 buf.push(0xF0); // !0x0F
272 }
273 }
274
275 /// Packs decimal digits for positive numbers into bytes for lexicographic
276 /// ordering. Each pair of ASCII digits is packed into a single byte (4
277 /// bits each). Mapping: '0'..'9' → 1..10. For odd digit counts, the last
278 /// byte has a zero low nibble; for even counts, an extra 0x00 terminator
279 /// byte is appended. This ensures decode will stop before any trailing
280 /// type marker appended by higher layers.
281 fn pack_digits_positive(radix10: &str, buf: &mut Vec<u8>) {
282 let mut iter = radix10.as_bytes().chunks_exact(2);
283 for pair in &mut iter {
284 // pair is &[u8; 2]
285 // Convert ASCII digits to numeric values: '0' (48) -> 1, '1' (49) -> 2, etc.
286 // We subtract 47 instead of 48 to map '0'->1, '1'->2, ..., '9'->10
287 // This ensures no digit maps to 0, which we use as termination marker
288 let hi = pair[0] - 47;
289 let lo = pair[1] - 47;
290 let packed = (hi << 4) | lo;
291 buf.push(packed);
292 }
293
294 // If the length is odd, the remainder (the last lone byte) is here:
295 if let Some(remainder) = iter.remainder().first() {
296 let hi = remainder - 47;
297 let packed = hi << 4;
298 buf.push(packed);
299 } else {
300 // Set the termination byte
301 buf.push(0x0F);
302 }
303 }
304
305 /// Unpacks digits from bytes for positive numbers.
306 /// Reverses the packing process by extracting digit pairs from each byte.
307 /// Stops when a nibble equals 0 (terminator). Accumulates the mantissa
308 /// directly into U128 and returns the total number of decoded digits.
309 fn unpack_digits_positive(buf: &[u8]) -> Result<(U128, i32)> {
310 let mut m = U128::ZERO;
311 let mut l = 0;
312 for pack in buf {
313 let d = Self::unpack_digit(*pack, &mut m)?;
314 l += d as i32;
315 if d < 2 {
316 break;
317 }
318 }
319 Ok((m, l))
320 }
321
322 /// Unpacks digits from bytes for negative numbers.
323 /// First inverts each byte to undo the bit inversion, then extracts digit
324 /// pairs. Stops when a nibble equals 0 (after inversion). Accumulates into
325 /// U128 and returns the number of decoded digits.
326 fn unpack_digits_negative(buf: &[u8]) -> Result<(U128, i32)> {
327 let mut m = U128::ZERO;
328 let mut l = 0i32;
329 for pack in buf {
330 let d = Self::unpack_digit(!*pack, &mut m)?;
331 l += d as i32;
332 if d < 2 {
333 break;
334 }
335 }
336 Ok((m, l))
337 }
338
339 /// Unpacks a single packed byte into one or two digits.
340 /// Returns the number of digits appended (0, 1, or 2). A return of 0 or 1
341 /// indicates that a terminator nibble (0x0) was encountered and the caller
342 /// should stop.
343 ///
344 /// The byte contains two 4-bit values (nibbles): high nibble and low
345 /// nibble. Each nibble represents a digit value (1..=10 mapping to
346 /// '0'..'9'). Values outside 1..=10 are rejected as corrupted input.
347 fn unpack_digit(pack: u8, m: &mut U128) -> Result<u8> {
348 let hi = pack >> 4;
349 let lo = pack & 0x0F;
350 if hi == 0x0 {
351 return Ok(0);
352 }
353 if !(1..=10).contains(&hi) {
354 return Err(anyhow::Error::new(Error::Serialization(format!(
355 "Invalid high nibble: {hi}"
356 ))));
357 }
358 *m = *m * U128::TEN + U128::from(hi - 1);
359 if lo == 0 {
360 return Ok(1);
361 }
362 if !(1..=10).contains(&lo) {
363 return Err(anyhow::Error::new(Error::Serialization(format!(
364 "Invalid low nibble: {lo}"
365 ))));
366 }
367 *m = *m * U128::TEN + U128::from(lo - 1);
368 Ok(2)
369 }
370
371 /// Converts a rust_decimal::Decimal to a fastnum::D128.
372 ///
373 /// This conversion extracts the mantissa, scale, and sign from the Decimal
374 /// and reconstructs them as a D128 value.
375 pub fn to_d128(dec: Decimal) -> D128 {
376 let scale = dec.scale();
377 let mantissa = dec.mantissa(); // i128
378 let sign = if mantissa < 0 {
379 Sign::Minus
380 } else {
381 Sign::Plus
382 };
383 // Investigation of the code of the from_u128 at the time of adding the unwrap shows that
384 // this function cannot panic, weirdly it seems unable to ever return an error.
385 //
386 // The function returns an error when the number has more significant bits then can fit
387 // into the amount of bits of a u128 which seems impossible for a function which takes a
388 // u128.
389 let abs =
390 U128::from_u128(mantissa.unsigned_abs()).expect("u128 conversion should not fail");
391 D128::from_parts(abs, -(scale as i32), sign, Context::default())
392 }
393
394 /// Converts a fastnum::D128 to a rust_decimal::Decimal.
395 ///
396 /// This conversion uses string representation as an intermediate format
397 /// to ensure precision is maintained during the conversion.
398 pub fn to_decimal(d128: D128) -> Result<Decimal> {
399 Ok(Decimal::from_str_radix(&d128.to_string(), 10)?)
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 fn test_cases() -> [D128; 32] {
408 [
409 D128::from(f64::NEG_INFINITY),
410 D128::from(f64::MIN),
411 D128::from_i128(i128::MIN).unwrap(),
412 D128::from(i64::MIN),
413 D128::from(-1001),
414 D128::from(-1000),
415 D128::from(-999),
416 D128::from(-100),
417 -D128::TEN,
418 D128::from(-9),
419 D128::from(-3.15),
420 D128::from(-std::f64::consts::PI),
421 D128::from(-1.5f64),
422 -D128::ONE,
423 D128::ZERO,
424 D128::ONE,
425 D128::from(1.5f64),
426 D128::from(2),
427 D128::from(std::f64::consts::PI),
428 D128::from(3.15),
429 D128::from(9),
430 D128::TEN,
431 D128::from(100),
432 D128::from(999),
433 D128::from(1000),
434 D128::from(1001),
435 D128::from(i64::MAX),
436 D128::from_i128(i128::MAX).unwrap(),
437 D128::from_u128(u128::MAX).unwrap(),
438 D128::from(f64::MAX),
439 D128::from(f64::INFINITY),
440 D128::from(f64::NAN),
441 ]
442 }
443
444 #[test]
445 fn test_encode_decode_roundtrip() {
446 let cases = test_cases();
447 for (i, case) in cases.into_iter().enumerate() {
448 let encoded = DecimalLexEncoder::encode(case);
449 let decoded = DecimalLexEncoder::decode(&encoded).expect("Decode should succeed");
450 if case.is_nan() {
451 assert!(decoded.is_nan(), "Roundtrip failed for {i}: {case} != {decoded}");
452 } else {
453 assert_eq!(case, decoded, "Roundtrip failed for {i}: {case} != {decoded}");
454 }
455 }
456 }
457
458 #[test]
459 fn test_encode_terminate_with_zero() {
460 let cases = test_cases();
461 for (i, case) in cases.into_iter().enumerate() {
462 let encoded = DecimalLexEncoder::encode(case);
463 assert_eq!(
464 encoded.iter().filter(|&b| *b == 0x00).count(),
465 1,
466 "Encoded buffer should contains only one 0x00 - {i}: {case} {encoded:?}"
467 );
468 assert_eq!(
469 encoded.iter().position(|&b| b == 0x00).unwrap(),
470 encoded.len() - 1,
471 "Encoded buffer should terminate with 0x00 - {i}: {case} {encoded:?}"
472 );
473 }
474 }
475
476 #[test]
477 fn test_lexicographic_ordering() {
478 let cases = test_cases();
479 for (i, window) in cases.windows(2).enumerate() {
480 let n1 = &window[0];
481 let n2 = &window[1];
482 assert!(n1 < n2, "#{i} - {n1:?} < {n2:?} (before serialization)");
483 let b1 = DecimalLexEncoder::encode(*n1);
484 let b2 = DecimalLexEncoder::encode(*n2);
485 assert!(b1 < b2, "#{i} - {n1:?} < {n2:?} (after serialization) - {b1:?} < {b2:?}");
486 }
487 }
488
489 #[test]
490 fn test_decode_empty_buffer() {
491 let result = DecimalLexEncoder::decode(&[]);
492 assert!(result.is_err());
493 assert!(result.unwrap_err().to_string().contains("Cannot decode from empty buffer"));
494 }
495
496 #[test]
497 fn test_decode_buffer_too_short() {
498 let result = DecimalLexEncoder::decode(&[0xA0]);
499 assert!(result.is_err());
500 let err = result.unwrap_err();
501 assert!(err.to_string().contains("Buffer too short"), "{err:?}");
502 }
503
504 #[test]
505 fn test_decode_invalid_marker() {
506 let result = DecimalLexEncoder::decode(&[0x42, 0x00, 0x00, 0x00]);
507 assert!(result.is_err());
508 let err = result.unwrap_err();
509 assert_eq!(err.to_string(), "Serialization error: Invalid marker byte: 66", "{err:?}");
510 }
511
512 #[test]
513 fn test_decode_empty_mantissa() {
514 // Create a buffer that starts correctly but is truncated during mantissa
515 // decoding
516 let result = DecimalLexEncoder::decode(&[0xA0, 0x01, 0x01, 0x00]); // Missing mantissa data
517 assert!(result.is_err());
518 let err = result.unwrap_err();
519 assert!(err.to_string().contains("Empty mantissa"), "{err:?}");
520 }
521}