diff options
Diffstat (limited to '')
-rw-r--r-- | crates/bytes/src/error.rs | 38 | ||||
-rw-r--r-- | crates/bytes/src/lib.rs | 215 |
2 files changed, 253 insertions, 0 deletions
diff --git a/crates/bytes/src/error.rs b/crates/bytes/src/error.rs new file mode 100644 index 0000000..7643109 --- /dev/null +++ b/crates/bytes/src/error.rs @@ -0,0 +1,38 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{fmt::Display, num::ParseIntError}; + +#[derive(Debug)] +pub enum BytesError { + BytesParseIntError(ParseIntError), + NotYetSupported(String), +} + +impl Display for BytesError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + BytesError::BytesParseIntError(e) => { + f.write_fmt(format_args!("Failed to parse a number as integer: '{}'", e)) + }, + BytesError::NotYetSupported(other) => { + f.write_fmt(format_args!("Your extension '{}' is not yet supported. Only KB,MB,GB or KiB,MiB,GiB are supported", other)) + } + } + } +} + +impl From<ParseIntError> for BytesError { + fn from(value: ParseIntError) -> Self { + Self::BytesParseIntError(value) + } +} + +impl std::error::Error for BytesError {} diff --git a/crates/bytes/src/lib.rs b/crates/bytes/src/lib.rs new file mode 100644 index 0000000..f80b864 --- /dev/null +++ b/crates/bytes/src/lib.rs @@ -0,0 +1,215 @@ +// yt - A fully featured command line YouTube client +// +// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de> +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Yt. +// +// You should have received a copy of the License along with this program. +// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. + +use std::{fmt::Display, str::FromStr}; + +use error::BytesError; + +const B: u64 = 1; + +const KIB: u64 = 1024 * B; +const MIB: u64 = 1024 * KIB; +const GIB: u64 = 1024 * MIB; +const TIB: u64 = 1024 * GIB; +const PIB: u64 = 1024 * TIB; + +const KB: u64 = 1000 * B; +const MB: u64 = 1000 * KB; +const GB: u64 = 1000 * MB; +const TB: u64 = 1000 * GB; + +pub mod error; + +#[derive(Clone, Copy)] +pub struct Bytes(u64); + +impl Bytes { + pub fn as_u64(self) -> u64 { + self.0 + } + pub fn new(v: u64) -> Self { + Self(v) + } +} + +impl FromStr for Bytes { + type Err = BytesError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let s = s + .chars() + .filter(|elem| !elem.is_whitespace()) + .collect::<String>(); + + let number: u64 = s + .chars() + .take_while(|x| x.is_numeric()) + .collect::<String>() + .parse()?; + let extension = s.chars().skip_while(|x| x.is_numeric()).collect::<String>(); + + let output = match extension.to_lowercase().as_str() { + "" => number, + "b" => number * B, + "kib" => number * KIB, + "mib" => number * MIB, + "gib" => number * GIB, + "tib" => number * TIB, + "kb" => number * KB, + "mb" => number * MB, + "gb" => number * GB, + "tb" => number * TB, + other => return Err(BytesError::NotYetSupported(other.to_owned())), + }; + + Ok(Self(output)) + } +} + +impl Display for Bytes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let num = self.0; + + match num { + 0..KIB => f.write_fmt(format_args!("{} {}", num, "B"))?, + KIB..MIB => f.write_fmt(format_args!( + "{} {}", + precision_f64((num as f64) / (KIB as f64), 3), + "KiB" + ))?, + MIB..GIB => f.write_fmt(format_args!( + "{} {}", + precision_f64((num as f64) / (MIB as f64), 3), + "MiB" + ))?, + GIB..TIB => f.write_fmt(format_args!( + "{} {}", + precision_f64((num as f64) / (GIB as f64), 3), + "GiB" + ))?, + TIB..PIB => f.write_fmt(format_args!( + "{} {}", + precision_f64((num as f64) / (TIB as f64), 3), + "TiB" + ))?, + PIB.. => todo!(), + } + + Ok(()) + } +} + +// taken from this stack overflow question: https://stackoverflow.com/a/76572321 +/// Round to significant digits (rather than digits after the decimal). +/// +/// Not implemented for `f32`, because such an implementation showed precision +/// glitches (e.g. `precision_f32(12300.0, 2) == 11999.999`), so for `f32` +/// floats, convert to `f64` for this function and back as needed. +/// +/// Examples: +/// ``` +///# fn main() { +///# use bytes::precision_f64; +/// assert_eq!(precision_f64(1.2300, 2), 1.2f64); +/// assert_eq!(precision_f64(1.2300_f64, 2), 1.2f64); +/// assert_eq!(precision_f64(1.2300_f32 as f64, 2), 1.2f64); +/// assert_eq!(precision_f64(1.2300_f32 as f64, 2) as f32, 1.2f32); +///# } +/// ``` +pub fn precision_f64(x: f64, decimals: u32) -> f64 { + if x == 0. || decimals == 0 { + 0. + } else { + let shift = decimals as i32 - x.abs().log10().ceil() as i32; + let shift_factor = 10_f64.powi(shift); + + (x * shift_factor).round() / shift_factor + } +} + +#[cfg(test)] +mod tests { + use super::{Bytes, GIB}; + + #[test] + fn parsing() { + let input: Bytes = "20 GiB".parse().unwrap(); + let expected = 20 * GIB; + + assert_eq!(expected, input.0); + } + #[test] + fn round_trip_1kib() { + let input = "1 KiB"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!(input.to_owned(), parsed.to_string()); + } + #[test] + fn round_trip_2kib() { + let input = "2 KiB"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!(input.to_owned(), parsed.to_string()); + } + + #[test] + fn round_trip_1mib() { + let input = "1 MiB"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!(input.to_owned(), parsed.to_string()); + } + #[test] + fn round_trip_2mib() { + let input = "2 MiB"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!(input.to_owned(), parsed.to_string()); + } + + #[test] + fn round_trip_1gib() { + let input = "1 GiB"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!(input.to_owned(), parsed.to_string()); + } + #[test] + fn round_trip_2gib() { + let input = "2 GiB"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!(input.to_owned(), parsed.to_string()); + } + + #[test] + fn round_trip() { + let input = "20 TiB"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!(input.to_owned(), parsed.to_string()); + } + + #[test] + fn round_trip_decmimal() { + let input = "20 TB"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!("18.2 TiB", parsed.to_string()); + } + #[test] + fn round_trip_1b() { + let input = "1"; + let parsed: Bytes = input.parse().unwrap(); + + assert_eq!("1 B", parsed.to_string()); + } +} |