Skip to main content

decimal_bytes/
encoding.rs

1//! Byte encoding for decimal values.
2//!
3//! This module implements a lexicographically sortable encoding for decimal numbers.
4//! The encoding ensures that byte-wise comparison yields the same result as numerical comparison.
5//!
6//! ## Encoding Format
7//!
8//! ```text
9//! [sign byte] [exponent bytes] [mantissa bytes]
10//! ```
11//!
12//! - **Sign byte**: 0x00 for negative, 0x80 for zero, 0xFF for positive
13//! - **Exponent**: Variable-length, biased encoding (inverted for negative numbers)
14//! - **Mantissa**: BCD-encoded digits, 2 per byte (inverted for negative numbers)
15//!
16//! ## Special Values (PostgreSQL compatible)
17//!
18//! - **-Infinity**: Sorts less than all negative numbers
19//! - **+Infinity**: Sorts greater than all positive numbers
20//! - **NaN**: Sorts greater than +Infinity (per PostgreSQL semantics)
21//!
22//! ## Sort Order
23//!
24//! ```text
25//! -Infinity < negatives < zero < positives < +Infinity < NaN
26//! ```
27
28use thiserror::Error;
29
30/// Sign byte values for regular numbers
31pub(crate) const SIGN_NEGATIVE: u8 = 0x00;
32pub(crate) const SIGN_ZERO: u8 = 0x80;
33pub(crate) const SIGN_POSITIVE: u8 = 0xFF;
34
35/// Special value encodings (designed for correct lexicographic ordering)
36/// -Infinity: [0x00, 0x00, 0x00] - sorts before all negative numbers
37pub const ENCODING_NEG_INFINITY: [u8; 3] = [0x00, 0x00, 0x00];
38/// +Infinity: [0xFF, 0xFF, 0xFE] - sorts after all positive numbers
39pub const ENCODING_POS_INFINITY: [u8; 3] = [0xFF, 0xFF, 0xFE];
40/// NaN: [0xFF, 0xFF, 0xFF] - sorts after +Infinity (PostgreSQL semantics)
41pub const ENCODING_NAN: [u8; 3] = [0xFF, 0xFF, 0xFF];
42
43/// Reserved exponent values (to distinguish special values from regular numbers)
44const RESERVED_NEG_INFINITY_EXP: u16 = 0x0000; // For negative sign byte
45const RESERVED_POS_INFINITY_EXP: u16 = 0xFFFE; // For positive sign byte
46const RESERVED_NAN_EXP: u16 = 0xFFFF; // For positive sign byte
47
48/// Exponent bias to make all exponents positive for encoding
49const EXPONENT_BIAS: i32 = 16384;
50const MAX_EXPONENT: i32 = 32767 - EXPONENT_BIAS - 2; // Reserve top 2 values for Infinity/NaN
51const MIN_EXPONENT: i32 = -EXPONENT_BIAS + 1; // Reserve 0x0000 for -Infinity
52
53/// Errors that can occur during decimal encoding/decoding.
54#[derive(Error, Debug, Clone, PartialEq)]
55pub enum DecimalError {
56    /// The input string format is invalid.
57    #[error("Invalid format: {0}")]
58    InvalidFormat(String),
59
60    /// The number exceeds the supported precision range.
61    #[error("Precision overflow: exponent out of range")]
62    PrecisionOverflow,
63
64    /// The encoded bytes are invalid.
65    #[error("Invalid encoding")]
66    InvalidEncoding,
67}
68
69/// Special decimal values (IEEE 754 / PostgreSQL compatible)
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum SpecialValue {
72    /// Positive infinity
73    Infinity,
74    /// Negative infinity
75    NegInfinity,
76    /// Not a Number
77    NaN,
78}
79
80/// Encodes a decimal string to sortable bytes.
81pub fn encode_decimal(value: &str) -> Result<Vec<u8>, DecimalError> {
82    // Check for special values first
83    if let Some(special) = parse_special_value(value) {
84        return Ok(encode_special_value(special));
85    }
86
87    let (is_negative, digits, exponent) = parse_decimal(value)?;
88
89    // Handle zero
90    if digits.is_empty() {
91        return Ok(vec![SIGN_ZERO]);
92    }
93
94    let mut result = Vec::with_capacity(1 + 2 + digits.len().div_ceil(2));
95
96    // Sign byte
97    result.push(if is_negative {
98        SIGN_NEGATIVE
99    } else {
100        SIGN_POSITIVE
101    });
102
103    // Encode exponent
104    encode_exponent(&mut result, exponent, is_negative);
105
106    // Encode mantissa (BCD, 2 digits per byte)
107    encode_mantissa(&mut result, &digits, is_negative);
108
109    Ok(result)
110}
111
112/// Parses special value strings (case-insensitive).
113fn parse_special_value(value: &str) -> Option<SpecialValue> {
114    let trimmed = value.trim();
115    let lower = trimmed.to_lowercase();
116
117    match lower.as_str() {
118        "infinity" | "inf" | "+infinity" | "+inf" => Some(SpecialValue::Infinity),
119        "-infinity" | "-inf" => Some(SpecialValue::NegInfinity),
120        "nan" | "-nan" | "+nan" => Some(SpecialValue::NaN), // PostgreSQL treats all NaN as equal
121        _ => None,
122    }
123}
124
125/// Encodes a special value to bytes.
126pub fn encode_special_value(special: SpecialValue) -> Vec<u8> {
127    match special {
128        SpecialValue::NegInfinity => ENCODING_NEG_INFINITY.to_vec(),
129        SpecialValue::Infinity => ENCODING_POS_INFINITY.to_vec(),
130        SpecialValue::NaN => ENCODING_NAN.to_vec(),
131    }
132}
133
134/// Checks if bytes represent a special value.
135pub fn decode_special_value(bytes: &[u8]) -> Option<SpecialValue> {
136    if bytes.len() == 3 {
137        if bytes == ENCODING_NEG_INFINITY {
138            return Some(SpecialValue::NegInfinity);
139        }
140        if bytes == ENCODING_POS_INFINITY {
141            return Some(SpecialValue::Infinity);
142        }
143        if bytes == ENCODING_NAN {
144            return Some(SpecialValue::NaN);
145        }
146    }
147    None
148}
149
150/// Encodes a decimal string with precision and scale constraints.
151///
152/// # Arguments
153/// * `value` - The decimal string to encode
154/// * `precision` - Maximum total significant digits (None = unlimited)
155/// * `scale` - Number of digits after decimal point; negative values round to left of decimal
156///
157/// # PostgreSQL Compatibility
158/// Supports negative scale (rounds to powers of 10):
159/// - scale = -3 rounds to nearest 1000
160/// - NUMERIC(2, -3) allows values like -99000 to 99000
161pub fn encode_decimal_with_constraints(
162    value: &str,
163    precision: Option<u32>,
164    scale: Option<i32>,
165) -> Result<Vec<u8>, DecimalError> {
166    // Handle special values - they ignore precision/scale
167    if parse_special_value(value).is_some() {
168        return encode_decimal(value);
169    }
170
171    let truncated = truncate_decimal(value, precision, scale)?;
172    encode_decimal(&truncated)
173}
174
175/// Decodes bytes back to a decimal string.
176pub fn decode_to_string(bytes: &[u8]) -> Result<String, DecimalError> {
177    if bytes.is_empty() {
178        return Err(DecimalError::InvalidEncoding);
179    }
180
181    // Check for special values first
182    if let Some(special) = decode_special_value(bytes) {
183        return Ok(match special {
184            SpecialValue::NegInfinity => "-Infinity".to_string(),
185            SpecialValue::Infinity => "Infinity".to_string(),
186            SpecialValue::NaN => "NaN".to_string(),
187        });
188    }
189
190    let sign_byte = bytes[0];
191
192    // Handle zero
193    if sign_byte == SIGN_ZERO {
194        return Ok("0".to_string());
195    }
196
197    let is_negative = sign_byte == SIGN_NEGATIVE;
198
199    if sign_byte != SIGN_NEGATIVE && sign_byte != SIGN_POSITIVE {
200        return Err(DecimalError::InvalidEncoding);
201    }
202
203    // Decode exponent (also validates it's not a reserved value)
204    let (exponent, mantissa_start) = decode_exponent(&bytes[1..], is_negative)?;
205
206    // Decode mantissa
207    let mantissa_bytes = &bytes[1 + mantissa_start..];
208    let digits = decode_mantissa(mantissa_bytes, is_negative)?;
209
210    // Format as string
211    format_decimal(is_negative, &digits, exponent)
212}
213
214/// Decodes bytes back to a decimal string with a specific scale.
215///
216/// This ensures the output has exactly `scale` decimal places, adding trailing
217/// zeros if needed. This is useful for PostgreSQL NUMERIC display formatting.
218///
219/// # Arguments
220/// * `bytes` - The encoded decimal bytes
221/// * `scale` - Number of decimal places to ensure in the output
222///
223/// # Examples
224/// ```ignore
225/// // If bytes represent "1", with scale 18, returns "1.000000000000000000"
226/// // If bytes represent "1.5", with scale 18, returns "1.500000000000000000"
227/// ```
228pub fn decode_to_string_with_scale(bytes: &[u8], scale: i32) -> Result<String, DecimalError> {
229    // First decode to normalized string
230    let normalized = decode_to_string(bytes)?;
231
232    // Handle special values - they don't get scale formatting
233    if normalized == "NaN" || normalized == "Infinity" || normalized == "-Infinity" {
234        return Ok(normalized);
235    }
236
237    // If scale <= 0, no decimal places needed
238    if scale <= 0 {
239        return Ok(normalized);
240    }
241
242    let scale = scale as usize;
243
244    // Find the decimal point position
245    if let Some(dot_pos) = normalized.find('.') {
246        let current_decimals = normalized.len() - dot_pos - 1;
247        if current_decimals >= scale {
248            // Already has enough decimal places
249            Ok(normalized)
250        } else {
251            // Need to add trailing zeros
252            let zeros_needed = scale - current_decimals;
253            Ok(format!("{}{}", normalized, "0".repeat(zeros_needed)))
254        }
255    } else {
256        // No decimal point - add one with the required zeros
257        Ok(format!("{}.{}", normalized, "0".repeat(scale)))
258    }
259}
260
261/// Parses a decimal string into sign, digits, and exponent.
262fn parse_decimal(value: &str) -> Result<(bool, Vec<u8>, i32), DecimalError> {
263    let value = value.trim();
264    let mut chars = value.chars().peekable();
265
266    // Handle sign
267    let is_negative = if chars.peek() == Some(&'-') {
268        chars.next();
269        true
270    } else if chars.peek() == Some(&'+') {
271        chars.next();
272        false
273    } else {
274        false
275    };
276
277    // Collect the numeric part (before 'e' or 'E')
278    let mut integer_part = String::new();
279    let mut fractional_part = String::new();
280    let mut seen_decimal = false;
281    let mut seen_exponent_marker = false;
282
283    while let Some(&c) = chars.peek() {
284        if c == '.' {
285            if seen_decimal {
286                return Err(DecimalError::InvalidFormat(
287                    "Multiple decimal points".to_string(),
288                ));
289            }
290            seen_decimal = true;
291            chars.next();
292        } else if c.is_ascii_digit() {
293            if seen_decimal {
294                fractional_part.push(c);
295            } else {
296                integer_part.push(c);
297            }
298            chars.next();
299        } else if c == 'e' || c == 'E' {
300            seen_exponent_marker = true;
301            chars.next();
302            break;
303        } else {
304            return Err(DecimalError::InvalidFormat(format!(
305                "Invalid character: {}",
306                c
307            )));
308        }
309    }
310
311    // Parse exponent (required if 'e' or 'E' was seen)
312    let mut exp_offset: i32 = 0;
313    if seen_exponent_marker {
314        if chars.peek().is_none() {
315            return Err(DecimalError::InvalidFormat(
316                "Missing exponent after 'e'".to_string(),
317            ));
318        }
319        let exp_str: String = chars.collect();
320        exp_offset = exp_str
321            .parse()
322            .map_err(|_| DecimalError::InvalidFormat(format!("Invalid exponent: {}", exp_str)))?;
323    }
324
325    // Handle empty input
326    if integer_part.is_empty() && fractional_part.is_empty() {
327        return Ok((false, vec![], 0));
328    }
329
330    // If only fractional part, integer part is "0"
331    if integer_part.is_empty() {
332        integer_part.push('0');
333    }
334
335    // Remember where the decimal point was before combining
336    let decimal_position = integer_part.len();
337
338    // Combine all digits by appending fractional part (avoids extra allocation)
339    integer_part.push_str(&fractional_part);
340    let all_digits = integer_part;
341
342    // Find the first and last non-zero digit positions
343    let first_nonzero = all_digits.chars().position(|c| c != '0');
344    let last_nonzero = all_digits.chars().rev().position(|c| c != '0');
345
346    // If all zeros, return zero
347    if first_nonzero.is_none() {
348        return Ok((false, vec![], 0));
349    }
350
351    let first_nonzero = first_nonzero.unwrap();
352    let last_nonzero = all_digits.len() - 1 - last_nonzero.unwrap();
353
354    // Extract the significant digits
355    let significant = &all_digits[first_nonzero..=last_nonzero];
356
357    // Calculate the exponent
358    let exponent = (decimal_position as i32) - (first_nonzero as i32) + exp_offset;
359
360    // Convert significant digits to bytes
361    let digits: Vec<u8> = significant
362        .chars()
363        .map(|c| c.to_digit(10).unwrap() as u8)
364        .collect();
365
366    // Validate exponent range
367    if !(MIN_EXPONENT..=MAX_EXPONENT).contains(&exponent) {
368        return Err(DecimalError::PrecisionOverflow);
369    }
370
371    Ok((is_negative, digits, exponent))
372}
373
374/// Encodes the exponent as variable-length bytes.
375fn encode_exponent(result: &mut Vec<u8>, exponent: i32, is_negative: bool) {
376    // Bias the exponent to make it always positive
377    // Note: We add 1 to reserve 0x0000 for -Infinity on negative side
378    let biased = (exponent + EXPONENT_BIAS) as u16;
379
380    // For negative numbers, invert the exponent so larger negative numbers sort first
381    let encoded = if is_negative { !biased } else { biased };
382
383    // Use 2 bytes for the exponent (big-endian)
384    result.push((encoded >> 8) as u8);
385    result.push((encoded & 0xFF) as u8);
386}
387
388/// Decodes the exponent from bytes.
389fn decode_exponent(bytes: &[u8], is_negative: bool) -> Result<(i32, usize), DecimalError> {
390    if bytes.len() < 2 {
391        return Err(DecimalError::InvalidEncoding);
392    }
393
394    let encoded = ((bytes[0] as u16) << 8) | (bytes[1] as u16);
395
396    // Check for reserved values (should have been caught by decode_special_value)
397    if is_negative && encoded == RESERVED_NEG_INFINITY_EXP {
398        return Err(DecimalError::InvalidEncoding);
399    }
400    if !is_negative && (encoded == RESERVED_POS_INFINITY_EXP || encoded == RESERVED_NAN_EXP) {
401        return Err(DecimalError::InvalidEncoding);
402    }
403
404    let biased = if is_negative { !encoded } else { encoded };
405    let exponent = (biased as i32) - EXPONENT_BIAS;
406
407    Ok((exponent, 2))
408}
409
410/// Encodes the mantissa as BCD (2 digits per byte).
411fn encode_mantissa(result: &mut Vec<u8>, digits: &[u8], is_negative: bool) {
412    // Pack 2 digits per byte
413    let mut i = 0;
414    while i < digits.len() {
415        let high = digits[i];
416        let low = if i + 1 < digits.len() {
417            digits[i + 1]
418        } else {
419            0 // Pad with 0 if odd number of digits
420        };
421
422        let byte = (high << 4) | low;
423
424        // For negative numbers, invert to reverse the sort order
425        result.push(if is_negative { !byte } else { byte });
426
427        i += 2;
428    }
429}
430
431/// Decodes the mantissa from BCD bytes.
432fn decode_mantissa(bytes: &[u8], is_negative: bool) -> Result<Vec<u8>, DecimalError> {
433    let mut digits = Vec::with_capacity(bytes.len() * 2);
434
435    for &byte in bytes {
436        let byte = if is_negative { !byte } else { byte };
437        let high = (byte >> 4) & 0x0F;
438        let low = byte & 0x0F;
439
440        if high > 9 || low > 9 {
441            return Err(DecimalError::InvalidEncoding);
442        }
443
444        digits.push(high);
445        digits.push(low);
446    }
447
448    // Remove trailing zeros (padding)
449    while digits.last() == Some(&0) && digits.len() > 1 {
450        digits.pop();
451    }
452
453    Ok(digits)
454}
455
456/// Formats digits and exponent back to a decimal string.
457fn format_decimal(is_negative: bool, digits: &[u8], exponent: i32) -> Result<String, DecimalError> {
458    if digits.is_empty() {
459        return Ok("0".to_string());
460    }
461
462    let mut result = String::new();
463
464    if is_negative {
465        result.push('-');
466    }
467
468    let num_digits = digits.len() as i32;
469
470    if exponent >= num_digits {
471        // All digits are before the decimal point (integer part)
472        for d in digits {
473            result.push(char::from_digit(*d as u32, 10).unwrap());
474        }
475        // Add trailing zeros if needed
476        for _ in 0..(exponent - num_digits) {
477            result.push('0');
478        }
479    } else if exponent <= 0 {
480        // All digits are after the decimal point
481        result.push('0');
482        result.push('.');
483        for _ in 0..(-exponent) {
484            result.push('0');
485        }
486        for d in digits {
487            result.push(char::from_digit(*d as u32, 10).unwrap());
488        }
489    } else {
490        // Some digits before decimal, some after
491        let decimal_pos = exponent as usize;
492        for (i, d) in digits.iter().enumerate() {
493            if i == decimal_pos {
494                result.push('.');
495            }
496            result.push(char::from_digit(*d as u32, 10).unwrap());
497        }
498    }
499
500    Ok(result)
501}
502
503/// Truncates a decimal string to fit precision and scale constraints.
504///
505/// # PostgreSQL Compatibility
506/// - Positive scale: digits after decimal point
507/// - Negative scale: rounds to left of decimal (e.g., -3 rounds to nearest 1000)
508/// - Precision: total significant (non-rounded) digits
509fn truncate_decimal(
510    value: &str,
511    precision: Option<u32>,
512    scale: Option<i32>,
513) -> Result<String, DecimalError> {
514    // Parse to get sign and parts
515    let value = value.trim();
516    let is_negative = value.starts_with('-');
517    let value = value.trim_start_matches(['-', '+']);
518
519    // Split into integer and fractional parts
520    let (integer_part, fractional_part) = if let Some(dot_pos) = value.find('.') {
521        (&value[..dot_pos], &value[dot_pos + 1..])
522    } else {
523        (value, "")
524    };
525
526    // Trim leading zeros from integer part (but keep at least one digit)
527    let integer_part = integer_part.trim_start_matches('0');
528    let integer_part = if integer_part.is_empty() {
529        "0"
530    } else {
531        integer_part
532    };
533
534    let scale_val = scale.unwrap_or(0);
535
536    // Handle negative scale (round to left of decimal point)
537    if scale_val < 0 {
538        let round_digits = (-scale_val) as usize;
539
540        // Remove all fractional digits when scale is negative
541        let mut int_str = integer_part.to_string();
542
543        if int_str.len() <= round_digits {
544            // Number is smaller than the rounding unit
545            // Round: if the number >= half the rounding unit, round up to the unit
546            let num_val: u64 = int_str.parse().unwrap_or(0);
547            let rounding_unit = 10u64.pow(round_digits as u32);
548            let half_unit = rounding_unit / 2;
549
550            let result = if num_val >= half_unit {
551                rounding_unit.to_string()
552            } else {
553                "0".to_string()
554            };
555
556            return if is_negative && result != "0" {
557                Ok(format!("-{}", result))
558            } else {
559                Ok(result)
560            };
561        }
562
563        // Round the integer part
564        let keep_len = int_str.len() - round_digits;
565        let keep_part = &int_str[..keep_len];
566        let round_part = &int_str[keep_len..];
567
568        // Check if we need to round up
569        let first_rounded_digit = round_part.chars().next().unwrap_or('0');
570        let mut result_int = keep_part.to_string();
571
572        if first_rounded_digit >= '5' {
573            result_int = add_one_to_integer(&result_int);
574        }
575
576        // Add trailing zeros
577        int_str = format!("{}{}", result_int, "0".repeat(round_digits));
578
579        // Apply precision constraint for negative scale
580        if let Some(p) = precision {
581            let max_significant = p as usize;
582            let significant_len = result_int.trim_start_matches('0').len();
583            if significant_len > max_significant && max_significant > 0 {
584                // Truncate from left (keep least significant digits)
585                let trimmed = &result_int[result_int.len().saturating_sub(max_significant)..];
586                int_str = format!("{}{}", trimmed, "0".repeat(round_digits));
587            }
588        }
589
590        return if is_negative && int_str != "0" {
591            Ok(format!("-{}", int_str))
592        } else {
593            Ok(int_str)
594        };
595    }
596
597    // Handle positive scale (normal case - digits after decimal)
598    let scale_usize = scale_val as usize;
599
600    // Apply scale constraint (truncate/round fractional part)
601    let (mut integer_part, fractional_part) = if fractional_part.len() > scale_usize {
602        // Round the last digit
603        let truncated = &fractional_part[..scale_usize];
604        let next_digit = fractional_part.chars().nth(scale_usize).unwrap_or('0');
605
606        if next_digit >= '5' {
607            // Round up - this may carry into integer part
608            if scale_usize == 0 {
609                // Rounding to integer
610                (add_one_to_integer(integer_part), String::new())
611            } else {
612                let rounded = round_up(truncated);
613                if rounded.len() > scale_usize {
614                    // Carry into integer part
615                    let new_int = add_one_to_integer(integer_part);
616                    (new_int, "0".repeat(scale_usize))
617                } else {
618                    (integer_part.to_string(), rounded)
619                }
620            }
621        } else {
622            (integer_part.to_string(), truncated.to_string())
623        }
624    } else {
625        (integer_part.to_string(), fractional_part.to_string())
626    };
627
628    // Apply precision constraint
629    if let Some(p) = precision {
630        let max_integer_digits = if (p as i32) > scale_val {
631            (p as i32 - scale_val) as usize
632        } else {
633            0
634        };
635
636        if integer_part.len() > max_integer_digits && max_integer_digits > 0 {
637            // Truncate from the left (keep least significant digits)
638            integer_part = integer_part[integer_part.len() - max_integer_digits..].to_string();
639        } else if max_integer_digits == 0 {
640            integer_part = "0".to_string();
641        }
642    }
643
644    // Reconstruct
645    let result = if fractional_part.is_empty() || fractional_part.chars().all(|c| c == '0') {
646        integer_part
647    } else {
648        format!("{}.{}", integer_part, fractional_part.trim_end_matches('0'))
649    };
650
651    if is_negative && result != "0" {
652        Ok(format!("-{}", result))
653    } else {
654        Ok(result)
655    }
656}
657
658/// Adds 1 to an integer string.
659fn add_one_to_integer(s: &str) -> String {
660    let mut chars: Vec<char> = s.chars().collect();
661    let mut carry = true;
662
663    for c in chars.iter_mut().rev() {
664        if carry {
665            if *c == '9' {
666                *c = '0';
667            } else {
668                *c = char::from_digit(c.to_digit(10).unwrap() + 1, 10).unwrap();
669                carry = false;
670            }
671        }
672    }
673
674    if carry {
675        format!("1{}", chars.iter().collect::<String>())
676    } else {
677        chars.iter().collect()
678    }
679}
680
681/// Rounds up a digit string by adding 1 to the last digit.
682fn round_up(s: &str) -> String {
683    let mut chars: Vec<char> = s.chars().collect();
684    let mut carry = true;
685
686    for c in chars.iter_mut().rev() {
687        if carry {
688            if *c == '9' {
689                *c = '0';
690            } else {
691                *c = char::from_digit(c.to_digit(10).unwrap() + 1, 10).unwrap();
692                carry = false;
693            }
694        }
695    }
696
697    if carry {
698        // All 9s became 0s, prepend 1
699        format!("1{}", chars.iter().collect::<String>())
700    } else {
701        chars.iter().collect()
702    }
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708
709    #[test]
710    fn test_encode_decode_roundtrip() {
711        let values = vec![
712            "0",
713            "1",
714            "-1",
715            "123.456",
716            "-123.456",
717            "0.001",
718            "0.1",
719            "10",
720            "100",
721            "1000",
722            "-0.001",
723            "999999999999999999",
724        ];
725
726        for s in values {
727            let encoded = encode_decimal(s).unwrap();
728            let decoded = decode_to_string(&encoded).unwrap();
729            // Re-encode to normalize
730            let re_encoded = encode_decimal(&decoded).unwrap();
731            assert_eq!(encoded, re_encoded, "Roundtrip failed for {}", s);
732        }
733    }
734
735    #[test]
736    fn test_lexicographic_ordering() {
737        let values = vec![
738            "-1000", "-100", "-10", "-1", "-0.1", "-0.01", "0", "0.01", "0.1", "1", "10", "100",
739            "1000",
740        ];
741
742        let encoded: Vec<Vec<u8>> = values.iter().map(|s| encode_decimal(s).unwrap()).collect();
743
744        // Verify encoding preserves order
745        for i in 0..encoded.len() - 1 {
746            assert!(
747                encoded[i] < encoded[i + 1],
748                "Ordering failed: {} should be < {}",
749                values[i],
750                values[i + 1]
751            );
752        }
753    }
754
755    #[test]
756    fn test_zero_encoding() {
757        let encoded = encode_decimal("0").unwrap();
758        assert_eq!(encoded, vec![SIGN_ZERO]);
759
760        let encoded = encode_decimal("0.0").unwrap();
761        assert_eq!(encoded, vec![SIGN_ZERO]);
762
763        let encoded = encode_decimal("-0").unwrap();
764        assert_eq!(encoded, vec![SIGN_ZERO]);
765    }
766
767    #[test]
768    fn test_truncate_scale() {
769        assert_eq!(
770            truncate_decimal("123.456", None, Some(2)).unwrap(),
771            "123.46"
772        );
773        assert_eq!(
774            truncate_decimal("123.454", None, Some(2)).unwrap(),
775            "123.45"
776        );
777        assert_eq!(truncate_decimal("123.995", None, Some(2)).unwrap(), "124");
778        assert_eq!(truncate_decimal("9.999", None, Some(2)).unwrap(), "10");
779    }
780
781    #[test]
782    fn test_storage_efficiency() {
783        // 9 digit number: should be ~1 sign + 2 exp + 5 mantissa = 8 bytes
784        let encoded = encode_decimal("123456789").unwrap();
785        assert!(
786            encoded.len() <= 8,
787            "Expected <= 8 bytes, got {}",
788            encoded.len()
789        );
790
791        // Small decimal
792        let encoded = encode_decimal("0.1").unwrap();
793        assert!(
794            encoded.len() <= 4,
795            "Expected <= 4 bytes, got {}",
796            encoded.len()
797        );
798    }
799
800    // ==================== Special Values Tests ====================
801
802    #[test]
803    fn test_special_value_encoding() {
804        // Test encoding special values
805        let pos_inf = encode_decimal("Infinity").unwrap();
806        assert_eq!(pos_inf, ENCODING_POS_INFINITY.to_vec());
807
808        let neg_inf = encode_decimal("-Infinity").unwrap();
809        assert_eq!(neg_inf, ENCODING_NEG_INFINITY.to_vec());
810
811        let nan = encode_decimal("NaN").unwrap();
812        assert_eq!(nan, ENCODING_NAN.to_vec());
813    }
814
815    #[test]
816    fn test_special_value_decoding() {
817        // Test decoding special values
818        assert_eq!(
819            decode_to_string(&ENCODING_POS_INFINITY).unwrap(),
820            "Infinity"
821        );
822        assert_eq!(
823            decode_to_string(&ENCODING_NEG_INFINITY).unwrap(),
824            "-Infinity"
825        );
826        assert_eq!(decode_to_string(&ENCODING_NAN).unwrap(), "NaN");
827    }
828
829    #[test]
830    fn test_special_value_parsing_variants() {
831        // Test various ways to write special values (case-insensitive)
832        let variants = vec![
833            ("infinity", "Infinity"),
834            ("Infinity", "Infinity"),
835            ("INFINITY", "Infinity"),
836            ("inf", "Infinity"),
837            ("Inf", "Infinity"),
838            ("+infinity", "Infinity"),
839            ("+inf", "Infinity"),
840            ("-infinity", "-Infinity"),
841            ("-inf", "-Infinity"),
842            ("-Infinity", "-Infinity"),
843            ("nan", "NaN"),
844            ("NaN", "NaN"),
845            ("NAN", "NaN"),
846            ("-nan", "NaN"), // PostgreSQL treats -NaN as NaN
847            ("+nan", "NaN"),
848        ];
849
850        for (input, expected) in variants {
851            let encoded = encode_decimal(input).unwrap();
852            let decoded = decode_to_string(&encoded).unwrap();
853            assert_eq!(decoded, expected, "Failed for input: {}", input);
854        }
855    }
856
857    #[test]
858    fn test_special_value_ordering() {
859        // PostgreSQL order: -Infinity < negatives < zero < positives < Infinity < NaN
860        let values = vec![
861            "-Infinity",
862            "-1000000",
863            "-1",
864            "-0.001",
865            "0",
866            "0.001",
867            "1",
868            "1000000",
869            "Infinity",
870            "NaN",
871        ];
872
873        let encoded: Vec<Vec<u8>> = values.iter().map(|s| encode_decimal(s).unwrap()).collect();
874
875        // Verify ordering
876        for i in 0..encoded.len() - 1 {
877            assert!(
878                encoded[i] < encoded[i + 1],
879                "Special value ordering failed: {} should be < {} (bytes: {:?} < {:?})",
880                values[i],
881                values[i + 1],
882                encoded[i],
883                encoded[i + 1]
884            );
885        }
886    }
887
888    #[test]
889    fn test_special_value_roundtrip() {
890        let values = vec!["Infinity", "-Infinity", "NaN"];
891
892        for s in values {
893            let encoded = encode_decimal(s).unwrap();
894            let decoded = decode_to_string(&encoded).unwrap();
895            let re_encoded = encode_decimal(&decoded).unwrap();
896            assert_eq!(
897                encoded, re_encoded,
898                "Special value roundtrip failed for {}",
899                s
900            );
901        }
902    }
903
904    #[test]
905    fn test_decode_special_value_helper() {
906        assert_eq!(
907            decode_special_value(&ENCODING_POS_INFINITY),
908            Some(SpecialValue::Infinity)
909        );
910        assert_eq!(
911            decode_special_value(&ENCODING_NEG_INFINITY),
912            Some(SpecialValue::NegInfinity)
913        );
914        assert_eq!(decode_special_value(&ENCODING_NAN), Some(SpecialValue::NaN));
915
916        // Regular values should return None
917        let regular = encode_decimal("123.456").unwrap();
918        assert_eq!(decode_special_value(&regular), None);
919
920        let zero = encode_decimal("0").unwrap();
921        assert_eq!(decode_special_value(&zero), None);
922    }
923
924    // ==================== Negative Scale Tests ====================
925
926    #[test]
927    fn test_negative_scale_basic() {
928        // Round to nearest 10
929        assert_eq!(truncate_decimal("123", None, Some(-1)).unwrap(), "120");
930        assert_eq!(truncate_decimal("125", None, Some(-1)).unwrap(), "130");
931        assert_eq!(truncate_decimal("124", None, Some(-1)).unwrap(), "120");
932
933        // Round to nearest 100
934        assert_eq!(truncate_decimal("1234", None, Some(-2)).unwrap(), "1200");
935        assert_eq!(truncate_decimal("1250", None, Some(-2)).unwrap(), "1300");
936        assert_eq!(truncate_decimal("1249", None, Some(-2)).unwrap(), "1200");
937
938        // Round to nearest 1000
939        assert_eq!(truncate_decimal("12345", None, Some(-3)).unwrap(), "12000");
940        assert_eq!(truncate_decimal("12500", None, Some(-3)).unwrap(), "13000");
941    }
942
943    #[test]
944    fn test_negative_scale_small_numbers() {
945        // When number is smaller than rounding unit
946        assert_eq!(truncate_decimal("499", None, Some(-3)).unwrap(), "0");
947        assert_eq!(truncate_decimal("500", None, Some(-3)).unwrap(), "1000");
948        assert_eq!(truncate_decimal("999", None, Some(-3)).unwrap(), "1000");
949
950        assert_eq!(truncate_decimal("49", None, Some(-2)).unwrap(), "0");
951        assert_eq!(truncate_decimal("50", None, Some(-2)).unwrap(), "100");
952    }
953
954    #[test]
955    fn test_negative_scale_with_precision() {
956        // NUMERIC(2, -3): max 2 significant digits, round to nearest 1000
957        assert_eq!(
958            truncate_decimal("12345", Some(2), Some(-3)).unwrap(),
959            "12000"
960        );
961        // 99999 rounded to nearest 1000 = 100000
962        // "100" significant part exceeds precision 2, truncated from left to "00"
963        // Final: "00" + "000" trailing zeros = "00000"
964        // Note: PostgreSQL would error here; we truncate instead
965        assert_eq!(
966            truncate_decimal("99999", Some(2), Some(-3)).unwrap(),
967            "00000"
968        );
969    }
970
971    #[test]
972    fn test_negative_scale_negative_numbers() {
973        assert_eq!(truncate_decimal("-123", None, Some(-1)).unwrap(), "-120");
974        assert_eq!(truncate_decimal("-125", None, Some(-1)).unwrap(), "-130");
975        assert_eq!(truncate_decimal("-1234", None, Some(-2)).unwrap(), "-1200");
976    }
977
978    #[test]
979    fn test_negative_scale_with_decimal_input() {
980        // Fractional part is ignored with negative scale
981        assert_eq!(truncate_decimal("123.456", None, Some(-1)).unwrap(), "120");
982        assert_eq!(
983            truncate_decimal("1234.999", None, Some(-2)).unwrap(),
984            "1200"
985        );
986    }
987
988    #[test]
989    fn test_negative_scale_encoding_ordering() {
990        // Verify ordering is preserved with negative scale rounding
991        let values = vec!["-1000", "-100", "0", "100", "1000"];
992
993        let encoded: Vec<Vec<u8>> = values
994            .iter()
995            .map(|s| encode_decimal_with_constraints(s, None, Some(-2)).unwrap())
996            .collect();
997
998        for i in 0..encoded.len() - 1 {
999            assert!(
1000                encoded[i] < encoded[i + 1],
1001                "Negative scale ordering failed: {} should be < {}",
1002                values[i],
1003                values[i + 1]
1004            );
1005        }
1006    }
1007
1008    #[test]
1009    fn test_special_values_ignore_precision_scale() {
1010        // Special values should pass through unchanged regardless of precision/scale
1011        let inf = encode_decimal_with_constraints("Infinity", Some(5), Some(2)).unwrap();
1012        assert_eq!(inf, ENCODING_POS_INFINITY.to_vec());
1013
1014        let neg_inf = encode_decimal_with_constraints("-Infinity", Some(5), Some(2)).unwrap();
1015        assert_eq!(neg_inf, ENCODING_NEG_INFINITY.to_vec());
1016
1017        let nan = encode_decimal_with_constraints("NaN", Some(5), Some(2)).unwrap();
1018        assert_eq!(nan, ENCODING_NAN.to_vec());
1019    }
1020}