about summary refs log tree commit diff stats
path: root/crates/bytes/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--crates/bytes/src/error.rs38
-rw-r--r--crates/bytes/src/lib.rs215
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());
+    }
+}