use std::num::TryFromIntError;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[must_use]
pub struct ByteSize(u64);
impl ByteSize {
const BYTES_PER_MIB: u64 = 1024 * 1024;
const BYTES_PER_GIB: u64 = 1024 * Self::BYTES_PER_MIB;
pub const fn from_bytes(bytes: u64) -> Self {
Self(bytes)
}
#[must_use]
pub const fn checked_from_mib(mib: u64) -> Option<Self> {
match mib.checked_mul(Self::BYTES_PER_MIB) {
Some(bytes) => Some(Self(bytes)),
None => None,
}
}
#[must_use]
pub const fn checked_from_gib(gib: u64) -> Option<Self> {
match gib.checked_mul(Self::BYTES_PER_GIB) {
Some(bytes) => Some(Self(bytes)),
None => None,
}
}
#[must_use]
pub const fn as_bytes(self) -> u64 {
self.0
}
#[must_use]
pub const fn as_mib(self) -> u64 {
self.0 / Self::BYTES_PER_MIB
}
#[must_use]
pub const fn as_gib(self) -> u64 {
self.0 / Self::BYTES_PER_GIB
}
pub fn checked_div(self, divisor: u64) -> Option<Self> {
self.0.checked_div(divisor).map(Self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(super) struct Mib(u32);
impl Mib {
pub(super) const fn new(mib: u32) -> Self {
Self(mib)
}
}
impl TryFrom<ByteSize> for Mib {
type Error = TryFromIntError;
fn try_from(size: ByteSize) -> Result<Self, Self::Error> {
u32::try_from(size.as_mib()).map(Self::new)
}
}
impl From<Mib> for u32 {
fn from(mib: Mib) -> Self {
mib.0
}
}
#[cfg(test)]
mod tests {
use super::ByteSize;
use super::Mib;
#[test]
fn binary_units_share_a_canonical_byte_count() {
let gib = ByteSize::checked_from_gib(1).unwrap();
assert_eq!(gib.as_bytes(), 1_073_741_824);
assert_eq!(gib.as_mib(), 1024);
assert_eq!(gib.as_gib(), 1);
assert_eq!(ByteSize::checked_from_mib(1024), Some(gib));
assert_eq!(ByteSize::checked_from_mib(1).unwrap().as_bytes(), 1_048_576);
}
#[test]
fn checked_unit_constructors_work_in_constants() {
const MIB: ByteSize = ByteSize::checked_from_mib(1).unwrap();
const GIB: ByteSize = ByteSize::checked_from_gib(1).unwrap();
const MIB_OVERFLOW: Option<ByteSize> = ByteSize::checked_from_mib(u64::MAX);
const GIB_OVERFLOW: Option<ByteSize> = ByteSize::checked_from_gib(u64::MAX);
assert_eq!(MIB.as_bytes(), 1_048_576);
assert_eq!(GIB.as_bytes(), 1_073_741_824);
assert_eq!(MIB_OVERFLOW, None);
assert_eq!(GIB_OVERFLOW, None);
}
#[test]
fn zero_is_valid_in_every_unit() {
let zero = ByteSize::default();
assert_eq!(ByteSize::from_bytes(0), zero);
assert_eq!(ByteSize::checked_from_mib(0), Some(zero));
assert_eq!(ByteSize::checked_from_gib(0), Some(zero));
assert_eq!(zero.as_bytes(), 0);
assert_eq!(zero.as_mib(), 0);
assert_eq!(zero.as_gib(), 0);
}
#[test]
fn conversions_preserve_bytes_and_round_down_larger_units() {
for (bytes, mib, gib) in [
(1, 0, 0),
(1_048_575, 0, 0),
(1_048_576, 1, 0),
(1_048_577, 1, 0),
(1_073_741_823, 1023, 0),
(1_073_741_824, 1024, 1),
(1_073_741_825, 1024, 1),
(u64::MAX, 17_592_186_044_415, 17_179_869_183),
] {
let size = ByteSize::from_bytes(bytes);
assert_eq!(size.as_bytes(), bytes);
assert_eq!(size.as_mib(), mib);
assert_eq!(size.as_gib(), gib);
}
}
#[test]
fn checked_constructors_accept_the_largest_fitting_value_but_not_more() {
let max = ByteSize::from_bytes(u64::MAX);
let mib = max.as_mib();
assert_eq!(ByteSize::checked_from_mib(mib).unwrap().as_mib(), mib);
assert_eq!(ByteSize::checked_from_mib(mib + 1), None);
assert_eq!(ByteSize::checked_from_mib(u64::MAX), None);
let gib = max.as_gib();
assert_eq!(ByteSize::checked_from_gib(gib).unwrap().as_gib(), gib);
assert_eq!(ByteSize::checked_from_gib(gib + 1), None);
assert_eq!(ByteSize::checked_from_gib(u64::MAX), None);
}
#[test]
fn scalar_division_returns_a_size_and_rounds_down_to_bytes() {
let memory = ByteSize::checked_from_gib(16).unwrap();
assert_eq!(memory.checked_div(4).unwrap().as_gib(), 4);
for (bytes, divisor, expected_bytes) in [
(11, 4, 2),
(1, 4, 0),
(0, 4, 0),
(u64::MAX, 1, u64::MAX),
(u64::MAX, u64::MAX, 1),
] {
let size = ByteSize::from_bytes(bytes);
let expected = ByteSize::from_bytes(expected_bytes);
assert_eq!(size.checked_div(divisor), Some(expected));
}
}
#[test]
fn checked_division_rejects_zero_divisors() {
assert_eq!(ByteSize::from_bytes(0).checked_div(0), None);
assert_eq!(ByteSize::from_bytes(u64::MAX).checked_div(0), None);
}
#[test]
fn byte_size_to_mib_rounds_down() {
for (bytes, expected_mib) in [
(0, 0),
(1, 0),
(1_048_575, 0),
(1_048_576, 1),
(1_048_577, 1),
] {
assert_eq!(
Mib::try_from(ByteSize::from_bytes(bytes)).unwrap(),
Mib::new(expected_mib)
);
}
}
#[test]
fn byte_size_to_mib_rejects_overflow_without_panicking() {
let largest = ByteSize::checked_from_mib(u64::from(u32::MAX)).unwrap();
assert_eq!(Mib::try_from(largest).unwrap(), Mib::new(u32::MAX));
let first_overflow = ByteSize::checked_from_mib(u64::from(u32::MAX) + 1).unwrap();
let last_fitting = ByteSize::from_bytes(first_overflow.as_bytes() - 1);
assert_eq!(Mib::try_from(last_fitting).unwrap(), Mib::new(u32::MAX));
assert!(Mib::try_from(first_overflow).is_err());
assert!(Mib::try_from(ByteSize::from_bytes(u64::MAX)).is_err());
}
#[test]
fn conversion_preserves_mib_counts_without_rescaling() {
for count in [0, 1, 512, 8192, u32::MAX] {
assert_eq!(u32::from(Mib::new(count)), count);
}
}
}