Skip to main content

ps_pint16/
lib.rs

1//! Packs unsigned integers into a `u16` via variable precision.
2//!
3//! A [`PackedInt`] keeps nine significant bits and a scale, so any value up to
4//! `511 × 2²⁵⁴` fits in two bytes. Packing is lossy above 255: the value is
5//! rounded **up** to the nearest representable one, never by more than one part
6//! in 256.
7//!
8//! ```
9//! use ps_pint16::PackedInt;
10//!
11//! // Values below 256 survive exactly.
12//! assert_eq!(PackedInt::from_u64(255).to_u64(), 255);
13//!
14//! // Larger ones round up to the nearest representable value.
15//! assert_eq!(PackedInt::from_u64(1_000_000).to_u64(), 1_001_472);
16//!
17//! // Two bytes, whatever the width of the input.
18//! assert_eq!(core::mem::size_of::<PackedInt>(), 2);
19//! ```
20//!
21//! # Encoding
22//!
23//! The high byte of the representation is an exponent `e`, the low byte a
24//! mantissa `m`:
25//!
26//! | exponent | value       | range                | step    |
27//! |----------|-------------|----------------------|---------|
28//! | `0`      | `m`         | `0 ..= 255`          | `1`     |
29//! | `e ≥ 1`  | `2ᵉ⁺⁷ + m × 2ᵉ⁻¹` | `2ᵉ⁺⁷ ..= 511 × 2ᵉ⁻¹` | `2ᵉ⁻¹` |
30//!
31//! Consecutive exponents meet exactly one step apart, so the 65 536
32//! representations form a strictly increasing sequence with no gaps and no
33//! duplicates. Two consequences follow:
34//!
35//! * Every `u16` is a valid [`PackedInt`], so [`from_inner_u16`] cannot fail.
36//! * The derived [`Ord`] agrees with the order of the values represented, so
37//!   packed integers can be sorted and compared without unpacking.
38//!
39//! # Rounding and saturation
40//!
41//! Packing rounds up, so unpacking never returns less than was packed. When a
42//! packed value exceeds the target type, unpacking saturates at that type's
43//! maximum instead of wrapping. Both directions are total: no input panics.
44//!
45//! [`from_inner_u16`]: PackedInt::from_inner_u16
46#![cfg_attr(not(test), no_std)]
47#![deny(missing_docs)]
48
49/// Runs the examples in `README.md` as doctests. Not part of the public API.
50#[cfg(doctest)]
51#[doc = include_str!("../README.md")]
52struct Readme;
53
54/// An unsigned integer packed into 16 bits with variable precision.
55///
56/// See the [crate-level documentation](crate) for the encoding, the rounding
57/// behaviour, and the ordering guarantee.
58#[repr(transparent)]
59#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
60pub struct PackedInt {
61    inner: u16,
62}
63
64macro_rules! impl_from_type {
65    ($type:ty, $name:ident) => {
66        #[doc = concat!("Packs a [`", stringify!($type), "`], rounding up.")]
67        ///
68        /// Returns the smallest representable value greater than or equal to
69        /// `value`, which is `value` itself whenever it is below 256.
70        pub const fn $name(mut value: $type) -> Self {
71            let mut prefix = 0u16;
72
73            // Halve, rounding up, until the value fits in nine bits. Written
74            // this way rather than as `(value + 1) >> 1` so that `<$type>::MAX`
75            // does not overflow.
76            while value > 0x1ff {
77                prefix += 1;
78                value = (value >> 1) + (value & 1);
79            }
80
81            // The ninth bit of a normalized value is always set, so it carries
82            // into the exponent and need not be stored.
83            Self {
84                inner: (prefix << 8) + (value as u16),
85            }
86        }
87    };
88}
89
90macro_rules! impl_into_type {
91    ($type:ty, $name:ident) => {
92        #[doc = concat!("Unpacks into a [`", stringify!($type), "`], saturating at [`", stringify!($type), "::MAX`].")]
93        ///
94        /// Values that do not fit in the target type saturate rather than wrap.
95        pub const fn $name(self) -> $type {
96            let prefix = (self.inner >> 8) as $type;
97            let suffix = (self.inner & 0xff) as $type;
98
99            if prefix == 0 {
100                suffix
101            } else if 7 + prefix >= <$type>::BITS as $type {
102                <$type>::MAX
103            } else {
104                (1 << (7 + prefix)) | (suffix << (prefix - 1))
105            }
106        }
107    };
108}
109
110macro_rules! impl_traits {
111    ($type:ty, $from:ident, $into:ident) => {
112        impl From<$type> for PackedInt {
113            fn from(value: $type) -> Self {
114                Self::$from(value)
115            }
116        }
117
118        impl From<PackedInt> for $type {
119            fn from(packed: PackedInt) -> $type {
120                packed.$into()
121            }
122        }
123    };
124}
125
126impl PackedInt {
127    /// Reads a packed value from 12 bits: all of `bits[0]`, and the high
128    /// nibble of `bits[1]`.
129    ///
130    /// The low nibble of `bits[1]` is reserved for the caller and is ignored.
131    pub const fn from_12_bits(bits: &[u8; 2]) -> Self {
132        Self {
133            inner: (((bits[1] & 0xf0) as u16) << 4) | (bits[0] as u16),
134        }
135    }
136
137    /// Writes the packed value into 12 bits: all of `bits[0]`, and the high
138    /// nibble of `bits[1]`.
139    ///
140    /// The low nibble of `bits[1]` is left zero for the caller to use.
141    ///
142    /// Twelve bits hold only the low nibble of the exponent, so this is
143    /// lossless for values up to `511 × 2¹⁴` (8 372 224) and no further.
144    /// Beyond that the exponent is truncated and the value reads back as an
145    /// unrelated number.
146    pub const fn to_12_bits(self) -> [u8; 2] {
147        [self.inner as u8, 0xF0 & (self.inner >> 4) as u8]
148    }
149
150    /// Reads a packed value from two little-endian bytes.
151    pub const fn from_16_bits(bits: &[u8; 2]) -> Self {
152        Self {
153            inner: u16::from_le_bytes(*bits),
154        }
155    }
156
157    /// Writes the packed value as two little-endian bytes.
158    pub const fn to_16_bits(self) -> [u8; 2] {
159        self.inner.to_le_bytes()
160    }
161
162    /// Reinterprets a `u16` as a packed value.
163    ///
164    /// Every `u16` is a valid representation, so this cannot fail. It is the
165    /// inverse of [`to_inner_u16`](Self::to_inner_u16).
166    pub const fn from_inner_u16(inner: u16) -> Self {
167        Self { inner }
168    }
169
170    /// Returns the underlying representation.
171    pub const fn to_inner_u16(self) -> u16 {
172        self.inner
173    }
174
175    impl_from_type!(usize, from_usize);
176    impl_from_type!(u128, from_u128);
177    impl_from_type!(u64, from_u64);
178    impl_from_type!(u32, from_u32);
179    impl_from_type!(u16, from_u16);
180
181    impl_into_type!(usize, to_usize);
182    impl_into_type!(u128, to_u128);
183    impl_into_type!(u64, to_u64);
184    impl_into_type!(u32, to_u32);
185    impl_into_type!(u16, to_u16);
186}
187
188impl_traits!(usize, from_usize, to_usize);
189impl_traits!(u128, from_u128, to_u128);
190impl_traits!(u64, from_u64, to_u64);
191impl_traits!(u32, from_u32, to_u32);
192
193#[cfg(test)]
194mod tests {
195    use crate::PackedInt;
196
197    /// The first representation whose value exceeds [`u128::MAX`].
198    const U128_SATURATION: u16 = 0x7900;
199
200    /// Independent implementation of the encoding documented at the crate
201    /// root, against which the crate's own arithmetic is checked.
202    ///
203    /// Returns [`None`] if the represented value exceeds [`u128::MAX`].
204    fn reference_value(inner: u16) -> Option<u128> {
205        let exponent = u32::from(inner >> 8);
206        let mantissa = u128::from(inner & 0xff);
207
208        if exponent == 0 {
209            return Some(mantissa);
210        }
211
212        if exponent + 7 >= u128::BITS {
213            return None;
214        }
215
216        Some((1 << (exponent + 7)) + (mantissa << (exponent - 1)))
217    }
218
219    macro_rules! assert_unpacks_or_saturates {
220        ($packed:expr, $expected:expr, $type:ty, $to:ident) => {
221            let actual = $packed.$to();
222
223            match $expected {
224                Some(value) if value <= <$type>::MAX as u128 => assert_eq!(
225                    actual as u128, value,
226                    concat!(stringify!($to), " of {:?} should be {}"),
227                    $packed, value
228                ),
229                _ => assert_eq!(
230                    actual,
231                    <$type>::MAX,
232                    concat!(stringify!($to), " of {:?} should saturate"),
233                    $packed
234                ),
235            }
236        };
237    }
238
239    #[test]
240    fn every_representation_decodes_per_the_specification() {
241        for inner in 0..=u16::MAX {
242            let packed = PackedInt::from_inner_u16(inner);
243            let expected = reference_value(inner);
244
245            assert_unpacks_or_saturates!(packed, expected, u16, to_u16);
246            assert_unpacks_or_saturates!(packed, expected, u32, to_u32);
247            assert_unpacks_or_saturates!(packed, expected, u64, to_u64);
248            assert_unpacks_or_saturates!(packed, expected, usize, to_usize);
249
250            assert_eq!(packed.to_u128(), expected.unwrap_or(u128::MAX));
251        }
252    }
253
254    #[test]
255    fn values_increase_strictly_and_without_gaps() {
256        for inner in 0..U128_SATURATION - 1 {
257            let lower = reference_value(inner).expect("below the saturation point");
258            let upper = reference_value(inner + 1).expect("below the saturation point");
259
260            let exponent = u32::from(inner >> 8);
261            let step = 1u128 << exponent.saturating_sub(1);
262
263            assert_eq!(
264                upper - lower,
265                step,
266                "{inner:#06x} and its successor are not one step apart"
267            );
268
269            assert!(
270                PackedInt::from_inner_u16(inner) < PackedInt::from_inner_u16(inner + 1),
271                "Ord disagrees with the value order at {inner:#06x}"
272            );
273        }
274    }
275
276    #[test]
277    fn packing_returns_the_least_representable_upper_bound() {
278        assert_eq!(PackedInt::from_u128(0).to_inner_u16(), 0);
279
280        for inner in 1..U128_SATURATION {
281            let value = reference_value(inner).expect("below the saturation point");
282            let previous = reference_value(inner - 1).expect("below the saturation point");
283
284            assert_eq!(
285                PackedInt::from_u128(value).to_inner_u16(),
286                inner,
287                "{value} is representable and should pack to {inner:#06x}"
288            );
289
290            assert_eq!(
291                PackedInt::from_u128(previous + 1).to_inner_u16(),
292                inner,
293                "{} should round up to {inner:#06x}",
294                previous + 1
295            );
296        }
297    }
298
299    #[test]
300    fn packing_is_exact_below_256_and_rounds_up_above() {
301        for value in 0..256u128 {
302            assert_eq!(PackedInt::from_u128(value).to_u128(), value);
303        }
304
305        for inner in 1..U128_SATURATION {
306            let representable = reference_value(inner).expect("below the saturation point");
307
308            for value in [representable - 1, representable] {
309                let rounded = PackedInt::from_u128(value).to_u128();
310
311                assert!(rounded >= value, "{value} rounded down to {rounded}");
312
313                assert!(
314                    rounded - value <= value >> 8,
315                    "{value} rounded to {rounded}, further than one part in 256"
316                );
317            }
318        }
319    }
320
321    #[test]
322    fn packing_is_independent_of_the_input_width() {
323        for value in 0..=u16::MAX {
324            let packed = PackedInt::from_u16(value);
325
326            assert_eq!(PackedInt::from_u32(u32::from(value)), packed);
327            assert_eq!(PackedInt::from_u64(u64::from(value)), packed);
328            assert_eq!(PackedInt::from_u128(u128::from(value)), packed);
329            assert_eq!(PackedInt::from_usize(usize::from(value)), packed);
330        }
331    }
332
333    #[test]
334    fn packing_the_type_maximum_round_trips() {
335        assert_eq!(PackedInt::from_u16(u16::MAX).to_u16(), u16::MAX);
336        assert_eq!(PackedInt::from_u32(u32::MAX).to_u32(), u32::MAX);
337        assert_eq!(PackedInt::from_u64(u64::MAX).to_u64(), u64::MAX);
338        assert_eq!(PackedInt::from_u128(u128::MAX).to_u128(), u128::MAX);
339        assert_eq!(PackedInt::from_usize(usize::MAX).to_usize(), usize::MAX);
340    }
341
342    #[test]
343    fn powers_of_two_survive_packing() {
344        for shift in 0..u128::BITS {
345            let value = 1u128 << shift;
346
347            assert_eq!(PackedInt::from_u128(value).to_u128(), value);
348        }
349    }
350
351    #[test]
352    fn sixteen_bit_round_trip_is_lossless() {
353        for inner in 0..=u16::MAX {
354            let packed = PackedInt::from_inner_u16(inner);
355
356            assert_eq!(PackedInt::from_16_bits(&packed.to_16_bits()), packed);
357        }
358    }
359
360    #[test]
361    fn twelve_bit_round_trip_ignores_the_reserved_nibble() {
362        for inner in 0..0x1000 {
363            let packed = PackedInt::from_inner_u16(inner);
364            let bits = packed.to_12_bits();
365
366            assert_eq!(bits[1] & 0x0f, 0, "{inner:#06x} wrote the reserved nibble");
367
368            for reserved in 0..0x10 {
369                let dirty = [bits[0], bits[1] | reserved];
370
371                assert_eq!(
372                    PackedInt::from_12_bits(&dirty),
373                    packed,
374                    "{inner:#06x} was corrupted by reserved nibble {reserved:#03x}"
375                );
376            }
377        }
378    }
379
380    #[test]
381    fn twelve_bits_hold_values_up_to_the_documented_bound() {
382        let largest = PackedInt::from_inner_u16(0x0fff);
383
384        assert_eq!(largest.to_u128(), 511 * (1 << 14));
385        assert_eq!(largest.to_u128(), 8_372_224);
386        assert_eq!(PackedInt::from_12_bits(&largest.to_12_bits()), largest);
387    }
388
389    #[test]
390    fn conversions_are_usable_in_const_context() {
391        const PACKED: PackedInt = PackedInt::from_u64(1_000_000);
392        const UNPACKED: u64 = PACKED.to_u64();
393        const BYTES: [u8; 2] = PACKED.to_16_bits();
394
395        assert_eq!(UNPACKED, 1_001_472);
396        assert_eq!(PackedInt::from_16_bits(&BYTES), PACKED);
397    }
398
399    #[test]
400    fn from_impls_agree_with_the_inherent_methods() {
401        for shift in 0..u128::BITS {
402            let value = 1u128 << shift;
403            let packed = PackedInt::from(value);
404
405            assert_eq!(packed, PackedInt::from_u128(value));
406            assert_eq!(u128::from(packed), packed.to_u128());
407            assert_eq!(u64::from(packed), packed.to_u64());
408            assert_eq!(u32::from(packed), packed.to_u32());
409            assert_eq!(usize::from(packed), packed.to_usize());
410        }
411    }
412}