1use std::fmt;
6use std::str::FromStr;
7
8use crate::swarm::Error;
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct Size {
14 bytes: i64,
15}
16
17const KB: i64 = 1_000;
18const MB: i64 = KB * 1_000;
19const GB: i64 = MB * 1_000;
20const TB: i64 = GB * 1_000;
21
22impl Size {
23 pub fn from_bytes(b: f64) -> Result<Self, Error> {
25 Self::new(b)
26 }
27
28 pub fn from_kilobytes(k: f64) -> Result<Self, Error> {
30 Self::new(k * KB as f64)
31 }
32
33 pub fn from_megabytes(m: f64) -> Result<Self, Error> {
35 Self::new(m * MB as f64)
36 }
37
38 pub fn from_gigabytes(g: f64) -> Result<Self, Error> {
40 Self::new(g * GB as f64)
41 }
42
43 pub fn from_terabytes(t: f64) -> Result<Self, Error> {
45 Self::new(t * TB as f64)
46 }
47
48 pub fn parse(s: &str) -> Result<Self, Error> {
53 <Self as FromStr>::from_str(s)
54 }
55
56 pub const fn to_bytes(self) -> i64 {
58 self.bytes
59 }
60
61 pub fn to_kilobytes(self) -> f64 {
63 self.bytes as f64 / KB as f64
64 }
65
66 pub fn to_megabytes(self) -> f64 {
68 self.bytes as f64 / MB as f64
69 }
70
71 pub fn to_gigabytes(self) -> f64 {
73 self.bytes as f64 / GB as f64
74 }
75
76 pub fn to_terabytes(self) -> f64 {
78 self.bytes as f64 / TB as f64
79 }
80
81 fn new(b: f64) -> Result<Self, Error> {
82 if b.is_nan() {
83 return Err(Error::argument("size is NaN"));
84 }
85 if b < 0.0 {
86 return Err(Error::argument("size must be at least 0"));
87 }
88 Ok(Self {
89 bytes: b.ceil() as i64,
90 })
91 }
92}
93
94impl fmt::Display for Size {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 if self.bytes >= TB {
98 write!(f, "{:.2} TB", self.to_terabytes())
99 } else if self.bytes >= GB {
100 write!(f, "{:.2} GB", self.to_gigabytes())
101 } else if self.bytes >= MB {
102 write!(f, "{:.2} MB", self.to_megabytes())
103 } else if self.bytes >= KB {
104 write!(f, "{:.2} kB", self.to_kilobytes())
105 } else {
106 write!(f, "{} B", self.bytes)
107 }
108 }
109}
110
111impl FromStr for Size {
112 type Err = Error;
113
114 fn from_str(s: &str) -> Result<Self, Error> {
115 let clean: String = s.chars().filter(|c| !c.is_whitespace()).collect();
116 let lower = clean.to_ascii_lowercase();
117 if lower.is_empty() {
118 return Err(Error::argument("empty size string"));
119 }
120
121 let mut total_bytes: f64 = 0.0;
122 let mut chars = lower.chars().peekable();
123 let mut found = false;
124
125 while chars.peek().is_some() {
126 let mut num = String::new();
127 while let Some(&c) = chars.peek() {
128 if c.is_ascii_digit() || c == '.' {
129 num.push(c);
130 chars.next();
131 } else {
132 break;
133 }
134 }
135 if num.is_empty() {
136 return Err(Error::argument(format!("unrecognized size string: {s}")));
137 }
138 let value: f64 = num
139 .parse()
140 .map_err(|_| Error::argument(format!("invalid size number: {num}")))?;
141
142 let mut unit = String::new();
143 while let Some(&c) = chars.peek() {
144 if c.is_ascii_alphabetic() {
145 unit.push(c);
146 chars.next();
147 } else {
148 break;
149 }
150 }
151 if unit.is_empty() {
152 return Err(Error::argument(format!("missing unit in: {s}")));
153 }
154 total_bytes += value * unit_to_bytes(&unit)?;
155 found = true;
156 }
157
158 if !found {
159 return Err(Error::argument(format!("unrecognized size string: {s}")));
160 }
161 Self::new(total_bytes)
162 }
163}
164
165fn unit_to_bytes(unit: &str) -> Result<f64, Error> {
166 Ok(match unit {
167 "b" | "byte" | "bytes" => 1.0,
168 "kb" | "kilobyte" | "kilobytes" => KB as f64,
169 "mb" | "megabyte" | "megabytes" => MB as f64,
170 "gb" | "gigabyte" | "gigabytes" => GB as f64,
171 "tb" | "terabyte" | "terabytes" => TB as f64,
172 other => return Err(Error::argument(format!("unsupported size unit: {other}"))),
173 })
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn from_bytes_rounds_up() {
182 assert_eq!(Size::from_bytes(10.4).unwrap().to_bytes(), 11);
183 assert_eq!(Size::from_bytes(0.0).unwrap().to_bytes(), 0);
184 }
185
186 #[test]
187 fn from_kilobytes_uses_decimal_base() {
188 let s = Size::from_kilobytes(1.5).unwrap();
189 assert_eq!(s.to_bytes(), 1500);
190 }
191
192 #[test]
193 fn from_megabytes_to_kilobytes_round_trip() {
194 let s = Size::from_megabytes(2.0).unwrap();
195 assert!((s.to_kilobytes() - 2_000.0).abs() < f64::EPSILON);
196 }
197
198 #[test]
199 fn from_str_parses_compound() {
200 let s = Size::parse("1gb256mb").unwrap();
201 assert_eq!(s.to_bytes(), GB + 256 * MB);
202 }
203
204 #[test]
205 fn from_str_handles_whitespace_and_mixed_case() {
206 let s = Size::parse(" 512 kB ").unwrap();
207 assert_eq!(s.to_bytes(), 512 * KB);
208 }
209
210 #[test]
211 fn from_str_decimal_value() {
212 let s = Size::parse("1.5gb").unwrap();
213 assert_eq!(s.to_bytes(), (1.5 * GB as f64).ceil() as i64);
214 }
215
216 #[test]
217 fn from_str_rejects_empty() {
218 assert!(Size::parse("").is_err());
219 assert!(Size::parse(" ").is_err());
220 }
221
222 #[test]
223 fn from_str_rejects_unknown_unit() {
224 assert!(Size::parse("2pb").is_err());
225 }
226
227 #[test]
228 fn negative_or_nan_rejected() {
229 assert!(Size::from_bytes(-1.0).is_err());
230 assert!(Size::from_bytes(f64::NAN).is_err());
231 }
232
233 #[test]
234 fn display_auto_scales() {
235 assert_eq!(Size::from_bytes(1.0).unwrap().to_string(), "1 B");
236 assert_eq!(Size::from_kilobytes(1.5).unwrap().to_string(), "1.50 kB");
237 assert_eq!(Size::from_megabytes(28.0).unwrap().to_string(), "28.00 MB");
238 assert_eq!(Size::from_gigabytes(1.0).unwrap().to_string(), "1.00 GB");
239 assert_eq!(Size::from_terabytes(2.0).unwrap().to_string(), "2.00 TB");
240 }
241}