# 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.
```rust
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`:
| `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