Skip to main content

reifydb_value/
byte_size.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{
5	fmt::{self, Display, Formatter},
6	ops::{Add, Mul},
7};
8
9use serde::{Deserialize, Serialize};
10
11const KIB: u64 = 1024;
12const MIB: u64 = 1024 * 1024;
13const GIB: u64 = 1024 * 1024 * 1024;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16#[repr(transparent)]
17pub struct ByteSize(u64);
18
19impl ByteSize {
20	pub const ZERO: Self = Self(0);
21
22	pub const fn from_bytes(bytes: u64) -> Self {
23		Self(bytes)
24	}
25
26	pub const fn from_kib(kib: u64) -> Self {
27		Self(kib * KIB)
28	}
29
30	pub const fn from_mib(mib: u64) -> Self {
31		Self(mib * MIB)
32	}
33
34	pub const fn from_gib(gib: u64) -> Self {
35		Self(gib * GIB)
36	}
37
38	pub const fn as_bytes(self) -> u64 {
39		self.0
40	}
41
42	pub const fn as_kib(self) -> u64 {
43		self.0 / KIB
44	}
45
46	pub const fn saturating_add(self, other: Self) -> Self {
47		Self(self.0.saturating_add(other.0))
48	}
49
50	pub const fn saturating_sub(self, other: Self) -> Self {
51		Self(self.0.saturating_sub(other.0))
52	}
53}
54
55impl Add for ByteSize {
56	type Output = Self;
57
58	fn add(self, rhs: Self) -> Self {
59		Self(self.0 + rhs.0)
60	}
61}
62
63impl Mul<u64> for ByteSize {
64	type Output = Self;
65
66	fn mul(self, rhs: u64) -> Self {
67		Self(self.0 * rhs)
68	}
69}
70
71impl Display for ByteSize {
72	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
73		let bytes = self.0;
74		if bytes >= GIB && bytes.is_multiple_of(GIB) {
75			write!(f, "{} GiB", bytes / GIB)
76		} else if bytes >= MIB && bytes.is_multiple_of(MIB) {
77			write!(f, "{} MiB", bytes / MIB)
78		} else if bytes >= KIB && bytes.is_multiple_of(KIB) {
79			write!(f, "{} KiB", bytes / KIB)
80		} else {
81			write!(f, "{} B", bytes)
82		}
83	}
84}
85
86impl From<ByteSize> for u64 {
87	fn from(size: ByteSize) -> Self {
88		size.0
89	}
90}
91
92impl From<u64> for ByteSize {
93	fn from(bytes: u64) -> Self {
94		Self(bytes)
95	}
96}
97
98#[cfg(test)]
99mod tests {
100	use super::*;
101
102	#[test]
103	fn test_from_bytes_round_trips() {
104		assert_eq!(ByteSize::from_bytes(4096).as_bytes(), 4096);
105	}
106
107	#[test]
108	fn test_unit_constructors_use_binary_base() {
109		assert_eq!(ByteSize::from_kib(1).as_bytes(), 1024);
110		assert_eq!(ByteSize::from_mib(64).as_bytes(), 64 * 1024 * 1024);
111		assert_eq!(ByteSize::from_gib(1).as_bytes(), 1024 * 1024 * 1024);
112	}
113
114	#[test]
115	fn test_as_kib_matches_sqlite_cache_semantics() {
116		// SQLite negative cache_size is interpreted in KiB; from_kib(2000) must read back as 2000.
117		assert_eq!(ByteSize::from_kib(2000).as_kib(), 2000);
118	}
119
120	#[test]
121	fn test_as_kib_truncates_sub_kib_remainder() {
122		assert_eq!(ByteSize::from_bytes(2047).as_kib(), 1);
123		assert_eq!(ByteSize::from_bytes(1023).as_kib(), 0);
124	}
125
126	#[test]
127	fn test_zero() {
128		assert_eq!(ByteSize::ZERO.as_bytes(), 0);
129		assert_eq!(ByteSize::ZERO, ByteSize::from_bytes(0));
130	}
131
132	#[test]
133	fn test_mul_scales_a_fixed_record_width_by_a_count() {
134		// A fixed-width record times a row count is the whole reason Mul exists; scaling must stay in bytes.
135		assert_eq!(ByteSize::from_bytes(33) * 4, ByteSize::from_bytes(132));
136		assert_eq!(ByteSize::from_kib(1) * 1024, ByteSize::from_mib(1));
137		assert_eq!(ByteSize::from_bytes(33) * 0, ByteSize::ZERO);
138		assert_eq!(ByteSize::ZERO * 99, ByteSize::ZERO);
139	}
140
141	#[test]
142	fn test_ordering_compares_by_byte_count() {
143		assert!(ByteSize::from_kib(1) < ByteSize::from_mib(1));
144		assert!(ByteSize::from_mib(256) > ByteSize::from_mib(64));
145		assert_eq!(ByteSize::from_kib(1024), ByteSize::from_mib(1));
146	}
147
148	#[test]
149	fn test_display_picks_largest_exact_unit() {
150		assert_eq!(ByteSize::from_gib(2).to_string(), "2 GiB");
151		assert_eq!(ByteSize::from_mib(64).to_string(), "64 MiB");
152		assert_eq!(ByteSize::from_kib(2000).to_string(), "2000 KiB");
153		assert_eq!(ByteSize::from_bytes(4096).to_string(), "4 KiB");
154		assert_eq!(ByteSize::from_bytes(1500).to_string(), "1500 B");
155		assert_eq!(ByteSize::ZERO.to_string(), "0 B");
156	}
157
158	#[test]
159	fn test_add_sums_bytes() {
160		assert_eq!(ByteSize::from_kib(1) + ByteSize::from_kib(3), ByteSize::from_kib(4));
161		assert_eq!(ByteSize::from_mib(1) + ByteSize::from_bytes(512), ByteSize::from_bytes(1024 * 1024 + 512));
162		assert_eq!(ByteSize::ZERO + ByteSize::from_bytes(7), ByteSize::from_bytes(7));
163	}
164
165	#[test]
166	fn test_saturating_add_clamps_at_max() {
167		assert_eq!(ByteSize::from_bytes(10).saturating_add(ByteSize::from_bytes(5)), ByteSize::from_bytes(15));
168		assert_eq!(
169			ByteSize::from_bytes(u64::MAX).saturating_add(ByteSize::from_bytes(1)),
170			ByteSize::from_bytes(u64::MAX)
171		);
172	}
173
174	#[test]
175	fn test_saturating_sub_clamps_at_zero() {
176		assert_eq!(ByteSize::from_bytes(10).saturating_sub(ByteSize::from_bytes(4)), ByteSize::from_bytes(6));
177		assert_eq!(ByteSize::from_bytes(3).saturating_sub(ByteSize::from_bytes(9)), ByteSize::ZERO);
178	}
179}