Skip to main content

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}