Skip to main content

common/serde/
sortable.rs

1//! Sortable numeric encoding for lexicographic key ordering.
2//!
3//! This module provides utilities to encode signed integers and floating-point
4//! numbers into byte sequences that sort correctly when compared lexicographically
5//! (byte-by-byte). This is essential for key-value stores where keys are compared
6//! as raw bytes.
7//!
8//! ## Why Sortable Encoding?
9//!
10//! Standard binary representations don't sort correctly:
11//!
12//! - **Signed integers**: Two's complement puts negative numbers after positive
13//!   (e.g., -1 = 0xFF...FF sorts after 1 = 0x00...01)
14//! - **Floating point**: IEEE 754 has complex sign/exponent/mantissa layout that
15//!   doesn't match numeric order (e.g., -0.5 sorts after 1.0 in raw bytes)
16//!
17//! ## Encoding Schemes
18//!
19//! ### Signed Integers (i64)
20//!
21//! XOR with the sign bit mask (`0x8000_0000_0000_0000`):
22//! - Flips the sign bit, making negative numbers have 0 in the high bit
23//! - Negative numbers (originally 1xxx...) become (0xxx...) and sort first
24//! - Positive numbers (originally 0xxx...) become (1xxx...) and sort second
25//! - Preserves relative ordering within each group
26//!
27//! ### Floating Point (f64)
28//!
29//! IEEE 754 sortable encoding:
30//! - If negative (sign bit set): flip ALL bits
31//! - If positive (sign bit clear): flip only sign bit
32//!
33//! This works because:
34//! - Positive floats: flipping sign bit puts them in the 1xxx range
35//! - Negative floats: flipping all bits reverses their order (more negative → smaller)
36//! - Special values (NaN, infinity) are handled correctly
37//!
38//! ## Usage
39//!
40//! Encode values before writing to key bytes (big-endian for lexicographic order):
41//!
42//! ```
43//! use common::serde::sortable::{encode_i64_sortable, decode_i64_sortable};
44//!
45//! let value: i64 = -42;
46//! let sortable = encode_i64_sortable(value);
47//! let bytes = sortable.to_be_bytes(); // Use big-endian for keys
48//!
49//! // Later, decode:
50//! let recovered = decode_i64_sortable(u64::from_be_bytes(bytes));
51//! assert_eq!(recovered, value);
52//! ```
53
54/// Encode an i64 value for sortable byte comparison.
55///
56/// XORs with the sign bit to ensure negative numbers sort before positive.
57/// The result should be written in big-endian format for correct lexicographic ordering.
58#[inline]
59pub const fn encode_i64_sortable(value: i64) -> u64 {
60    (value as u64) ^ 0x8000_0000_0000_0000
61}
62
63/// Decode a sortable-encoded u64 back to the original i64 value.
64#[inline]
65pub const fn decode_i64_sortable(sortable: u64) -> i64 {
66    (sortable ^ 0x8000_0000_0000_0000) as i64
67}
68
69/// Encode an f64 value for sortable byte comparison.
70///
71/// Uses IEEE 754 sortable encoding:
72/// - Negative floats: flip all bits
73/// - Positive floats: flip only sign bit
74///
75/// The result should be written in big-endian format for correct lexicographic ordering.
76#[inline]
77pub const fn encode_f64_sortable(value: f64) -> u64 {
78    let bits = value.to_bits();
79    if bits & 0x8000_0000_0000_0000 != 0 {
80        // Negative: flip all bits
81        !bits
82    } else {
83        // Positive: flip only sign bit
84        bits ^ 0x8000_0000_0000_0000
85    }
86}
87
88/// Decode a sortable-encoded u64 back to the original f64 value.
89#[inline]
90pub const fn decode_f64_sortable(sortable: u64) -> f64 {
91    let bits = if sortable & 0x8000_0000_0000_0000 != 0 {
92        // Was positive (high bit now set): flip sign bit
93        sortable ^ 0x8000_0000_0000_0000
94    } else {
95        // Was negative (high bit now clear): flip all bits
96        !sortable
97    };
98    f64::from_bits(bits)
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn should_roundtrip_i64_values() {
107        let values = [
108            i64::MIN,
109            i64::MIN + 1,
110            -1000,
111            -1,
112            0,
113            1,
114            1000,
115            i64::MAX - 1,
116            i64::MAX,
117        ];
118
119        for value in values {
120            let encoded = encode_i64_sortable(value);
121            let decoded = decode_i64_sortable(encoded);
122            assert_eq!(decoded, value, "Roundtrip failed for {}", value);
123        }
124    }
125
126    #[test]
127    fn should_preserve_i64_ordering() {
128        let values = [i64::MIN, -1000, -1, 0, 1, 1000, i64::MAX];
129
130        for window in values.windows(2) {
131            let (a, b) = (window[0], window[1]);
132            let encoded_a = encode_i64_sortable(a);
133            let encoded_b = encode_i64_sortable(b);
134            assert!(
135                encoded_a < encoded_b,
136                "Ordering violated: {} (0x{:016x}) should be < {} (0x{:016x})",
137                a,
138                encoded_a,
139                b,
140                encoded_b
141            );
142        }
143    }
144
145    #[test]
146    fn should_roundtrip_f64_values() {
147        let values = [
148            f64::NEG_INFINITY,
149            f64::MIN,
150            -1000.5,
151            -1.0,
152            -f64::MIN_POSITIVE,
153            -0.0,
154            0.0,
155            f64::MIN_POSITIVE,
156            1.0,
157            1000.5,
158            f64::MAX,
159            f64::INFINITY,
160        ];
161
162        for value in values {
163            let encoded = encode_f64_sortable(value);
164            let decoded = decode_f64_sortable(encoded);
165            assert_eq!(
166                decoded.to_bits(),
167                value.to_bits(),
168                "Roundtrip failed for {}",
169                value
170            );
171        }
172    }
173
174    #[test]
175    fn should_preserve_f64_ordering() {
176        let values = [
177            f64::NEG_INFINITY,
178            f64::MIN,
179            -1000.5,
180            -1.0,
181            -0.0, // -0.0 and 0.0 have same numeric value but different bits
182            0.0,
183            1.0,
184            1000.5,
185            f64::MAX,
186            f64::INFINITY,
187        ];
188
189        for window in values.windows(2) {
190            let (a, b) = (window[0], window[1]);
191            // Skip -0.0 vs 0.0 comparison (they're equal numerically)
192            if a == b {
193                continue;
194            }
195            let encoded_a = encode_f64_sortable(a);
196            let encoded_b = encode_f64_sortable(b);
197            assert!(
198                encoded_a < encoded_b,
199                "Ordering violated: {} (0x{:016x}) should be < {} (0x{:016x})",
200                a,
201                encoded_a,
202                b,
203                encoded_b
204            );
205        }
206    }
207
208    #[test]
209    fn should_handle_f64_special_values() {
210        // NaN roundtrips (though ordering of NaN is undefined)
211        let nan = f64::NAN;
212        let encoded = encode_f64_sortable(nan);
213        let decoded = decode_f64_sortable(encoded);
214        assert!(decoded.is_nan());
215    }
216}