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}