bytesize/
parse.rs

1use alloc::{borrow::ToOwned as _, format, string::String};
2use core::{fmt, str};
3
4use super::ByteSize;
5
6impl str::FromStr for ByteSize {
7    type Err = String;
8
9    fn from_str(value: &str) -> Result<Self, Self::Err> {
10        if let Ok(v) = value.parse::<u64>() {
11            return Ok(Self(v));
12        }
13        let number = take_while(value, |c| c.is_ascii_digit() || c == '.');
14        match number.parse::<f64>() {
15            Ok(v) => {
16                let suffix = skip_while(&value[number.len()..], char::is_whitespace);
17                match suffix.parse::<Unit>() {
18                    Ok(u) => Ok(Self((v * u) as u64)),
19                    Err(error) => Err(format!(
20                        "couldn't parse {suffix:?} into a known SI unit, {error}"
21                    )),
22                }
23            }
24            Err(error) => Err(format!("couldn't parse {value:?} into a ByteSize, {error}")),
25        }
26    }
27}
28
29fn take_while<P>(s: &str, mut predicate: P) -> &str
30where
31    P: FnMut(char) -> bool,
32{
33    let offset = s
34        .chars()
35        .take_while(|ch| predicate(*ch))
36        .map(|ch| ch.len_utf8())
37        .sum();
38    &s[..offset]
39}
40
41fn skip_while<P>(s: &str, mut predicate: P) -> &str
42where
43    P: FnMut(char) -> bool,
44{
45    let offset: usize = s
46        .chars()
47        .skip_while(|ch| predicate(*ch))
48        .map(|ch| ch.len_utf8())
49        .sum();
50    &s[(s.len() - offset)..]
51}
52
53/// Scale unit.
54///
55/// ```
56/// use bytesize::Unit;
57///
58/// assert_eq!(
59///     "GiB".parse::<Unit>().unwrap(),
60///     Unit::GibiByte,
61/// );
62///
63/// "gibibyte".parse::<Unit>().unwrap_err();
64/// ```
65#[non_exhaustive]
66#[derive(Debug, Clone, PartialEq)]
67pub enum Unit {
68    /// Single byte.
69    Byte,
70
71    // power of tens
72    /// Kilobyte (10^3 bytes).
73    KiloByte,
74
75    /// Megabyte (10^6 bytes)
76    MegaByte,
77
78    /// Gigabyte (10^9 bytes)
79    GigaByte,
80
81    /// Terabyte (10^12 bytes)
82    TeraByte,
83
84    /// Petabyte (10^15 bytes)
85    PetaByte,
86
87    /// Exabyte (10^18 bytes)
88    ExaByte,
89
90    // power of twos
91    /// Kibibyte (2^10 bytes)
92    KibiByte,
93
94    /// Mebibyte (2^20 bytes)
95    MebiByte,
96
97    /// Gibibyte (2^30 bytes)
98    GibiByte,
99
100    /// Tebibyte (2^40 bytes)
101    TebiByte,
102
103    /// Pebibyte (2^50 bytes)
104    PebiByte,
105
106    /// Exbibyte (2^60 bytes)
107    ExbiByte,
108}
109
110impl Unit {
111    fn factor(&self) -> u64 {
112        match self {
113            Self::Byte => 1,
114            // decimal units
115            Self::KiloByte => crate::KB,
116            Self::MegaByte => crate::MB,
117            Self::GigaByte => crate::GB,
118            Self::TeraByte => crate::TB,
119            Self::PetaByte => crate::PB,
120            Self::ExaByte => crate::EB,
121            // binary units
122            Self::KibiByte => crate::KIB,
123            Self::MebiByte => crate::MIB,
124            Self::GibiByte => crate::GIB,
125            Self::TebiByte => crate::TIB,
126            Self::PebiByte => crate::PIB,
127            Self::ExbiByte => crate::EIB,
128        }
129    }
130}
131
132mod impl_ops {
133    use super::Unit;
134    use core::ops;
135
136    impl ops::Add<u64> for Unit {
137        type Output = u64;
138
139        fn add(self, other: u64) -> Self::Output {
140            self.factor() + other
141        }
142    }
143
144    impl ops::Add<Unit> for u64 {
145        type Output = u64;
146
147        fn add(self, other: Unit) -> Self::Output {
148            self + other.factor()
149        }
150    }
151
152    impl ops::Mul<u64> for Unit {
153        type Output = u64;
154
155        fn mul(self, other: u64) -> Self::Output {
156            self.factor() * other
157        }
158    }
159
160    impl ops::Mul<Unit> for u64 {
161        type Output = u64;
162
163        fn mul(self, other: Unit) -> Self::Output {
164            self * other.factor()
165        }
166    }
167
168    impl ops::Add<f64> for Unit {
169        type Output = f64;
170
171        fn add(self, other: f64) -> Self::Output {
172            self.factor() as f64 + other
173        }
174    }
175
176    impl ops::Add<Unit> for f64 {
177        type Output = f64;
178
179        fn add(self, other: Unit) -> Self::Output {
180            other.factor() as f64 + self
181        }
182    }
183
184    impl ops::Mul<f64> for Unit {
185        type Output = f64;
186
187        fn mul(self, other: f64) -> Self::Output {
188            self.factor() as f64 * other
189        }
190    }
191
192    impl ops::Mul<Unit> for f64 {
193        type Output = f64;
194
195        fn mul(self, other: Unit) -> Self::Output {
196            other.factor() as f64 * self
197        }
198    }
199}
200
201impl str::FromStr for Unit {
202    type Err = UnitParseError;
203
204    fn from_str(unit: &str) -> Result<Self, Self::Err> {
205        match () {
206            _ if unit.eq_ignore_ascii_case("b") => Ok(Self::Byte),
207            _ if unit.eq_ignore_ascii_case("k") | unit.eq_ignore_ascii_case("kb") => {
208                Ok(Self::KiloByte)
209            }
210            _ if unit.eq_ignore_ascii_case("m") | unit.eq_ignore_ascii_case("mb") => {
211                Ok(Self::MegaByte)
212            }
213            _ if unit.eq_ignore_ascii_case("g") | unit.eq_ignore_ascii_case("gb") => {
214                Ok(Self::GigaByte)
215            }
216            _ if unit.eq_ignore_ascii_case("t") | unit.eq_ignore_ascii_case("tb") => {
217                Ok(Self::TeraByte)
218            }
219            _ if unit.eq_ignore_ascii_case("p") | unit.eq_ignore_ascii_case("pb") => {
220                Ok(Self::PetaByte)
221            }
222            _ if unit.eq_ignore_ascii_case("e") | unit.eq_ignore_ascii_case("eb") => {
223                Ok(Self::ExaByte)
224            }
225            _ if unit.eq_ignore_ascii_case("ki") | unit.eq_ignore_ascii_case("kib") => {
226                Ok(Self::KibiByte)
227            }
228            _ if unit.eq_ignore_ascii_case("mi") | unit.eq_ignore_ascii_case("mib") => {
229                Ok(Self::MebiByte)
230            }
231            _ if unit.eq_ignore_ascii_case("gi") | unit.eq_ignore_ascii_case("gib") => {
232                Ok(Self::GibiByte)
233            }
234            _ if unit.eq_ignore_ascii_case("ti") | unit.eq_ignore_ascii_case("tib") => {
235                Ok(Self::TebiByte)
236            }
237            _ if unit.eq_ignore_ascii_case("pi") | unit.eq_ignore_ascii_case("pib") => {
238                Ok(Self::PebiByte)
239            }
240            _ if unit.eq_ignore_ascii_case("ei") | unit.eq_ignore_ascii_case("eib") => {
241                Ok(Self::ExbiByte)
242            }
243            _ => Err(UnitParseError(to_string_truncate(unit))),
244        }
245    }
246}
247
248/// Safely truncates
249fn to_string_truncate(unit: &str) -> String {
250    const MAX_UNIT_LEN: usize = 3;
251
252    if unit.len() > MAX_UNIT_LEN {
253        // TODO(MSRV 1.91): use ceil_char_boundary
254
255        if unit.is_char_boundary(3) {
256            format!("{}...", &unit[..3])
257        } else if unit.is_char_boundary(4) {
258            format!("{}...", &unit[..4])
259        } else if unit.is_char_boundary(5) {
260            format!("{}...", &unit[..5])
261        } else if unit.is_char_boundary(6) {
262            format!("{}...", &unit[..6])
263        } else {
264            unreachable!("char boundary will be within 4 bytes")
265        }
266    } else {
267        unit.to_owned()
268    }
269}
270
271/// Error returned when parsing a [`Unit`] fails.
272#[derive(Debug)]
273pub struct UnitParseError(String);
274
275impl fmt::Display for UnitParseError {
276    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277        write!(f, "Failed to parse unit \"{}\"", self.0)
278    }
279}
280
281#[cfg(feature = "std")]
282impl std::error::Error for UnitParseError {}
283
284#[cfg(test)]
285mod tests {
286    use alloc::string::ToString as _;
287
288    use super::*;
289
290    #[test]
291    fn truncating_error_strings() {
292        assert_eq!("", to_string_truncate(""));
293        assert_eq!("b", to_string_truncate("b"));
294        assert_eq!("ob", to_string_truncate("ob"));
295        assert_eq!("foo", to_string_truncate("foo"));
296
297        assert_eq!("foo...", to_string_truncate("foob"));
298        assert_eq!("foo...", to_string_truncate("foobar"));
299    }
300
301    #[test]
302    fn when_ok() {
303        // shortcut for writing test cases
304        fn parse(s: &str) -> u64 {
305            s.parse::<ByteSize>().unwrap().0
306        }
307
308        assert_eq!("0".parse::<ByteSize>().unwrap().0, 0);
309        assert_eq!(parse("0"), 0);
310        assert_eq!(parse("500"), 500);
311        assert_eq!(parse("1K"), Unit::KiloByte * 1);
312        assert_eq!(parse("1Ki"), Unit::KibiByte * 1);
313        assert_eq!(parse("1.5Ki"), (1.5 * Unit::KibiByte) as u64);
314        assert_eq!(parse("1KiB"), 1 * Unit::KibiByte);
315        assert_eq!(parse("1.5KiB"), (1.5 * Unit::KibiByte) as u64);
316        assert_eq!(parse("3 MB"), Unit::MegaByte * 3);
317        assert_eq!(parse("4 MiB"), Unit::MebiByte * 4);
318        assert_eq!(parse("6 GB"), 6 * Unit::GigaByte);
319        assert_eq!(parse("4 GiB"), 4 * Unit::GibiByte);
320        assert_eq!(parse("88TB"), 88 * Unit::TeraByte);
321        assert_eq!(parse("521TiB"), 521 * Unit::TebiByte);
322        assert_eq!(parse("8 PB"), 8 * Unit::PetaByte);
323        assert_eq!(parse("8P"), 8 * Unit::PetaByte);
324        assert_eq!(parse("12 PiB"), 12 * Unit::PebiByte);
325    }
326
327    #[test]
328    fn when_err() {
329        // shortcut for writing test cases
330        fn parse(s: &str) -> Result<ByteSize, String> {
331            s.parse::<ByteSize>()
332        }
333
334        assert!(parse("").is_err());
335        assert!(parse("a124GB").is_err());
336        assert!(parse("1.3 42.0 B").is_err());
337        assert!(parse("1.3 ... B").is_err());
338        // The original implementation did not account for the possibility that users may
339        // use whitespace to visually separate digits, thus treat it as an error
340        assert!(parse("1 000 B").is_err());
341    }
342
343    #[test]
344    fn to_and_from_str() {
345        // shortcut for writing test cases
346        fn parse(s: &str) -> u64 {
347            s.parse::<ByteSize>().unwrap().0
348        }
349
350        assert_eq!(parse(&parse("128GB").to_string()), 128 * Unit::GigaByte);
351        assert_eq!(
352            parse(&ByteSize(parse("128.000 GiB")).to_string()),
353            128 * Unit::GibiByte,
354        );
355    }
356}