ps-pint16 0.1.0-5

Packs integers into a u16 via variable precision
Documentation

ps-pint16

Packs unsigned integers into a u16 via variable precision.

A PackedInt keeps nine significant bits and a scale, so any value up to 511 × 2²⁵⁴ fits in two bytes. Packing is lossy above 255: the value is rounded up to the nearest representable one, never by more than one part in 256.

use ps_pint16::PackedInt;

// Values below 256 survive exactly.
assert_eq!(PackedInt::from_u64(255).to_u64(), 255);

// Larger ones round up to the nearest representable value.
assert_eq!(PackedInt::from_u64(1_000_000).to_u64(), 1_001_472);

// Two bytes, whatever the width of the input.
assert_eq!(core::mem::size_of::<PackedInt>(), 2);

Encoding

The high byte of the representation is an exponent e, the low byte a mantissa m:

exponent value range step
0 m 0 ..= 255 1
e ≥ 1 2ᵉ⁺⁷ + m × 2ᵉ⁻¹ 2ᵉ⁺⁷ ..= 511 × 2ᵉ⁻¹ 2ᵉ⁻¹

Consecutive exponents meet exactly one step apart, so the 65 536 representations form a strictly increasing sequence with no gaps and no duplicates. Two consequences follow:

  • Every u16 is a valid PackedInt, so from_inner_u16 cannot fail.
  • The derived Ord agrees with the order of the values represented, so packed integers can be sorted and compared without unpacking.

Rounding and saturation

Packing rounds up, so unpacking never returns less than was packed. When a packed value exceeds the target type, unpacking saturates at that type's maximum instead of wrapping. Both directions are total: no input panics.

Storage

  • to_16_bits / from_16_bits store the full range as two little-endian bytes.
  • to_12_bits / from_12_bits use all of the first byte and the high nibble of the second, leaving the low nibble free for the caller. Twelve bits hold only the low nibble of the exponent, so this form is lossless for values up to 511 × 2¹⁴ (8 372 224) and no further.

Compatibility

#![no_std], no dependencies, every conversion is a const fn. Builds on Rust 1.58 and later.

License

GPL-3.0-or-later