convert_byte_size_string/
lib.rs

1use std::convert::TryInto;
2
3#[cfg(test)]
4mod tests {
5    use super::{
6        convert_to_bytes, convert_to_bytes_base_10, convert_to_bytes_base_2, ConversionError,
7    };
8    #[test]
9    fn test_kb_uppercase() {
10        assert_eq!(1000_u128, convert_to_bytes("1 KB").unwrap());
11    }
12
13    #[test]
14    fn test_kb_lowercase() {
15        assert_eq!(1000_u128, convert_to_bytes("1 kb").unwrap());
16    }
17
18    #[test]
19    fn test_kb_mixedcase() {
20        assert_eq!(1000_u128, convert_to_bytes("1 kB").unwrap());
21    }
22
23    #[test]
24    fn test_kib() {
25        assert_eq!(1024_u128, convert_to_bytes("1 KiB").unwrap());
26    }
27
28    #[test]
29    fn test_mb() {
30        assert_eq!(1_000_000_u128, convert_to_bytes("1 MB").unwrap());
31    }
32
33    #[test]
34    fn test_gb() {
35        assert_eq!(1_000_000_000_u128, convert_to_bytes("1 GB").unwrap());
36    }
37
38    #[test]
39    fn test_tb() {
40        assert_eq!(1_000_000_000_000_u128, convert_to_bytes("1 TB").unwrap());
41    }
42
43    #[test]
44    fn test_pb() {
45        assert_eq!(
46            1_000_000_000_000_000_u128,
47            convert_to_bytes("1 PB").unwrap()
48        );
49    }
50
51    #[test]
52    fn test_eb() {
53        assert_eq!(
54            1_000_000_000_000_000_000_u128,
55            convert_to_bytes("1 EB").unwrap()
56        );
57    }
58
59    #[test]
60    fn test_zb() {
61        assert_eq!(
62            1_000_000_000_000_000_000_000_u128,
63            convert_to_bytes("1 ZB").unwrap()
64        );
65    }
66
67    #[test]
68    fn test_yb() {
69        assert_eq!(
70            1_000_000_000_000_000_000_000_000_u128,
71            convert_to_bytes("1 YB").unwrap()
72        );
73    }
74
75    #[test]
76    fn test_decimal() {
77        assert_eq!(
78            9_108_079_886_394_091_110_u128,
79            convert_to_bytes("7.9 EiB").unwrap()
80        );
81    }
82
83    #[test]
84    fn test_long_decimal() {
85        assert_eq!(1_530_000, convert_to_bytes("1.53 MB").unwrap());
86    }
87
88    #[test]
89    fn test_no_space() {
90        assert_eq!(1024, convert_to_bytes("1KiB").unwrap());
91    }
92
93    #[test]
94    fn test_decimal_no_space() {
95        assert_eq!(1_530_000, convert_to_bytes("1.53MB").unwrap());
96    }
97
98    #[test]
99    fn test_forced_bases() {
100        assert_eq!(1000, convert_to_bytes_base_10("1KiB").unwrap());
101        assert_eq!(1024, convert_to_bytes_base_2("1KB").unwrap());
102    }
103
104    #[test]
105    fn test_too_large() {
106        match convert_to_bytes("281474976710657 YiB") {
107            Err(ConversionError::TooLarge) => return,
108            _ => panic!("Did not get a TooLarge error"),
109        }
110    }
111
112    #[test]
113    fn test_invalid() {
114        match convert_to_bytes("invalid input") {
115            Err(ConversionError::InputInvalid(_)) => return,
116            _ => panic!("Did not get an InputInvalid error"),
117        }
118    }
119
120    #[test]
121    fn test_invalid_units() {
122        match convert_to_bytes("1 bad unit") {
123            Err(ConversionError::InputInvalid(_)) => (),
124            _ => panic!("Did not get an InputInvalid error"),
125        }
126        match convert_to_bytes("1 kilobadunit") {
127            Err(ConversionError::InputInvalid(_)) => (),
128            _ => panic!("Did not get an InputInvalid error"),
129        }
130        match convert_to_bytes("1kbu") {
131            Err(ConversionError::InputInvalid(_)) => (return),
132            _ => panic!("Did not get an InputInvalid error"),
133        }
134    }
135}
136
137/// Represents possible errors the library can return.
138#[derive(Debug)]
139pub enum ConversionError {
140    /// The provided input could not be parsed; more details may be available in the String.
141    InputInvalid(String),
142    /// The provided input was parsed correctly, but the output was too large to fit in a u128.
143    TooLarge,
144}
145
146/// Stores a number as either an i128 or an f64
147enum ParsingNumber {
148    Int(u128),
149    Float((u128, u128)),
150}
151
152/// Represent whether the conversion should be forced to base 10, base 2, or implied
153enum ForceBase {
154    Base2,
155    Base10,
156    Implied,
157}
158
159/// Convert the provided string to a u128 value containing the number of bytes represented by the string.
160/// Implies the base to use based on the string, e.g. "KiB" uses base 2 and "KB" uses base 10.
161///
162/// # Arguments
163///
164/// * `string` - The string to convert.
165///
166/// # Returns
167///
168/// * `Err(ConversionError::InputInvalid)` if the input was invalid. The String may contain more information.
169/// * `Err(ConversionError::TooLarge)` if the
170///
171/// # Example
172///
173/// ```rust
174/// use convert_byte_size_string::convert_to_bytes;
175/// assert_eq!(1024_u128, convert_to_bytes("1KiB").unwrap());
176/// assert_eq!(1000_u128, convert_to_bytes("1KB").unwrap());
177/// ```
178pub fn convert_to_bytes(string: &str) -> Result<u128, ConversionError> {
179    convert_to_bytes_with_base(string, ForceBase::Implied)
180}
181
182/// Like `convert_to_bytes` but forces the units to be treated as base 10 units (multiples of 1000).
183pub fn convert_to_bytes_base_10(string: &str) -> Result<u128, ConversionError> {
184    convert_to_bytes_with_base(string, ForceBase::Base10)
185}
186
187/// Like `convert_to_bytes` but forces the units to be treated as base 2 units (multiples of 1024).
188pub fn convert_to_bytes_base_2(string: &str) -> Result<u128, ConversionError> {
189    convert_to_bytes_with_base(string, ForceBase::Base2)
190}
191
192/// Does the actual work for the publicly exposed functions. Takes the string to convert and the base to use.
193fn convert_to_bytes_with_base(string: &str, base: ForceBase) -> Result<u128, ConversionError> {
194    let lowercase = string.to_lowercase();
195    let mut splits: Vec<&str> = lowercase.trim().split_whitespace().collect();
196    if splits.len() < 2 {
197        splits.clear();
198        let (index, _) = match lowercase
199            .trim()
200            .match_indices(|c: char| {
201                c == 'k'
202                    || c == 'm'
203                    || c == 'g'
204                    || c == 't'
205                    || c == 'p'
206                    || c == 'e'
207                    || c == 'z'
208                    || c == 'y'
209            })
210            .next()
211        {
212            Some(val) => val,
213            None => {
214                return Err(ConversionError::InputInvalid(String::from(
215                    "Did not find two parts in string",
216                )));
217            }
218        };
219
220        splits.push(&lowercase[..index]);
221        splits.push(&lowercase[index..]);
222    }
223
224    let mantissa: ParsingNumber;
225    match splits[0].parse::<u128>() {
226        Ok(n) => mantissa = ParsingNumber::Int(n),
227        Err(_) => {
228            let float_splits: Vec<&str> = splits[0].split('.').collect();
229            if float_splits.len() != 2 {
230                return Err(ConversionError::InputInvalid(format!(
231                    "Could not parse '{}' into an i128 or an f64",
232                    splits[0]
233                )));
234            }
235
236            let whole = match float_splits[0].parse::<u128>() {
237                Ok(val) => val,
238                Err(_) => {
239                    return Err(ConversionError::InputInvalid(format!(
240                        "Could not parse '{}' into an i128 or an f64",
241                        splits[0]
242                    )));
243                }
244            };
245            let fraction = match float_splits[1].parse::<u128>() {
246                Ok(val) => val,
247                Err(_) => {
248                    return Err(ConversionError::InputInvalid(format!(
249                        "Could not parse '{}' into an i128 or an f64",
250                        splits[0]
251                    )));
252                }
253            };
254            mantissa = ParsingNumber::Float((whole, fraction));
255        }
256    }
257
258    let exponent = parse_exponent(splits[1], base)?;
259
260    match mantissa {
261        ParsingNumber::Int(m) => match m.checked_mul(exponent) {
262            Some(val) => Ok(val),
263            None => Err(ConversionError::TooLarge),
264        },
265        ParsingNumber::Float(m) => {
266            if let Some(whole) = m.0.checked_mul(exponent) {
267                let fraction_digits: u32 = match m.1.to_string().len().try_into() {
268                    Ok(val) => val,
269                    Err(_) => {
270                        return Err(ConversionError::InputInvalid(String::from(
271                            "Decimal portion of input too long",
272                        )));
273                    }
274                };
275                if let Some(fraction) = m.1.checked_mul(exponent) {
276                    if let Some(fraction) = fraction.checked_div(10u128.pow(fraction_digits)) {
277                        if let Some(val) = whole.checked_add(fraction) {
278                            return Ok(val);
279                        }
280                    }
281                }
282            }
283            Err(ConversionError::TooLarge)
284        }
285    }
286}
287
288/// Parse the correct exponent to use based on the string. If base is `ForceBase::Implied`, then imply the base to use, otherwise use the one specified.
289fn parse_exponent(string: &str, base: ForceBase) -> Result<u128, ConversionError> {
290    if !string.is_ascii() {
291        return Err(ConversionError::InputInvalid(format!(
292            "Could not parse '{}' because it contains invalid characters",
293            string
294        )));
295    }
296
297    let chars: Vec<char> = string.to_lowercase().chars().collect();
298    if chars.len() < 2 {
299        return Err(ConversionError::InputInvalid(String::from(
300            "Unit not long enough",
301        )));
302    }
303
304    let base_1000: u128 = match base {
305        ForceBase::Implied => match chars[1] {
306            'b' if chars.len() == 2 => 1000,
307            'i' if chars.len() > 2 && chars[2] == 'b' => 1024,
308            _ if chars[2] == 'b' => {
309                return Err(ConversionError::InputInvalid(format!(
310                    "Invalid character in unit: {}",
311                    chars[1]
312                )));
313            }
314            _ => {
315                return Err(ConversionError::InputInvalid(format!(
316                    "Invalid unit: {}",
317                    string
318                )));
319            }
320        },
321        ForceBase::Base10 => 1000,
322        ForceBase::Base2 => 1024,
323    };
324
325    let exponent: u128 = match chars[0] {
326        'k' => base_1000,
327        'm' => base_1000 * base_1000,
328        'g' => base_1000 * base_1000 * base_1000,
329        't' => base_1000 * base_1000 * base_1000 * base_1000,
330        'p' => base_1000 * base_1000 * base_1000 * base_1000 * base_1000,
331        'e' => base_1000 * base_1000 * base_1000 * base_1000 * base_1000 * base_1000,
332        'z' => base_1000 * base_1000 * base_1000 * base_1000 * base_1000 * base_1000 * base_1000,
333        'y' => {
334            base_1000
335                * base_1000
336                * base_1000
337                * base_1000
338                * base_1000
339                * base_1000
340                * base_1000
341                * base_1000
342        }
343        _ => {
344            return Err(ConversionError::InputInvalid(format!(
345                "Invalid character in unit: {}",
346                chars[0]
347            )));
348        }
349    };
350
351    Ok(exponent)
352}