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,
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 Display for ByteSize {
64	fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
65		let bytes = self.0;
66		if bytes >= GIB && bytes.is_multiple_of(GIB) {
67			write!(f, "{} GiB", bytes / GIB)
68		} else if bytes >= MIB && bytes.is_multiple_of(MIB) {
69			write!(f, "{} MiB", bytes / MIB)
70		} else if bytes >= KIB && bytes.is_multiple_of(KIB) {
71			write!(f, "{} KiB", bytes / KIB)
72		} else {
73			write!(f, "{} B", bytes)
74		}
75	}
76}
77
78impl From<ByteSize> for u64 {
79	fn from(size: ByteSize) -> Self {
80		size.0
81	}
82}
83
84impl From<u64> for ByteSize {
85	fn from(bytes: u64) -> Self {
86		Self(bytes)
87	}
88}
89
90#[cfg(test)]
91mod tests {
92	use super::*;
93
94	#[test]
95	fn test_from_bytes_round_trips() {
96		assert_eq!(ByteSize::from_bytes(4096).as_bytes(), 4096);
97	}
98
99	#[test]
100	fn test_unit_constructors_use_binary_base() {
101		assert_eq!(ByteSize::from_kib(1).as_bytes(), 1024);
102		assert_eq!(ByteSize::from_mib(64).as_bytes(), 64 * 1024 * 1024);
103		assert_eq!(ByteSize::from_gib(1).as_bytes(), 1024 * 1024 * 1024);
104	}
105
106	#[test]
107	fn test_as_kib_matches_sqlite_cache_semantics() {
108		// SQLite negative cache_size is interpreted in KiB; from_kib(2000) must read back as 2000.
109		assert_eq!(ByteSize::from_kib(2000).as_kib(), 2000);
110	}
111
112	#[test]
113	fn test_as_kib_truncates_sub_kib_remainder() {
114		assert_eq!(ByteSize::from_bytes(2047).as_kib(), 1);
115		assert_eq!(ByteSize::from_bytes(1023).as_kib(), 0);
116	}
117
118	#[test]
119	fn test_zero() {
120		assert_eq!(ByteSize::ZERO.as_bytes(), 0);
121		assert_eq!(ByteSize::ZERO, ByteSize::from_bytes(0));
122	}
123
124	#[test]
125	fn test_ordering_compares_by_byte_count() {
126		assert!(ByteSize::from_kib(1) < ByteSize::from_mib(1));
127		assert!(ByteSize::from_mib(256) > ByteSize::from_mib(64));
128		assert_eq!(ByteSize::from_kib(1024), ByteSize::from_mib(1));
129	}
130
131	#[test]
132	fn test_display_picks_largest_exact_unit() {
133		assert_eq!(ByteSize::from_gib(2).to_string(), "2 GiB");
134		assert_eq!(ByteSize::from_mib(64).to_string(), "64 MiB");
135		assert_eq!(ByteSize::from_kib(2000).to_string(), "2000 KiB");
136		assert_eq!(ByteSize::from_bytes(4096).to_string(), "4 KiB");
137		assert_eq!(ByteSize::from_bytes(1500).to_string(), "1500 B");
138		assert_eq!(ByteSize::ZERO.to_string(), "0 B");
139	}
140
141	#[test]
142	fn test_add_sums_bytes() {
143		assert_eq!(ByteSize::from_kib(1) + ByteSize::from_kib(3), ByteSize::from_kib(4));
144		assert_eq!(ByteSize::from_mib(1) + ByteSize::from_bytes(512), ByteSize::from_bytes(1024 * 1024 + 512));
145		assert_eq!(ByteSize::ZERO + ByteSize::from_bytes(7), ByteSize::from_bytes(7));
146	}
147
148	#[test]
149	fn test_saturating_add_clamps_at_max() {
150		assert_eq!(ByteSize::from_bytes(10).saturating_add(ByteSize::from_bytes(5)), ByteSize::from_bytes(15));
151		assert_eq!(
152			ByteSize::from_bytes(u64::MAX).saturating_add(ByteSize::from_bytes(1)),
153			ByteSize::from_bytes(u64::MAX)
154		);
155	}
156
157	#[test]
158	fn test_saturating_sub_clamps_at_zero() {
159		assert_eq!(ByteSize::from_bytes(10).saturating_sub(ByteSize::from_bytes(4)), ByteSize::from_bytes(6));
160		assert_eq!(ByteSize::from_bytes(3).saturating_sub(ByteSize::from_bytes(9)), ByteSize::ZERO);
161	}
162}