From 291bb4de2a2e5940fbebd8d400f35c1fc3b0c7a2 Mon Sep 17 00:00:00 2001 From: Ben Bridle Date: Sun, 24 Dec 2023 21:07:11 +1300 Subject: First commit --- .gitignore | 2 + Cargo.toml | 11 ++++++ src/internal.rs | 90 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 8 ++++ src/proportion.rs | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/tests.rs | 66 +++++++++++++++++++++++++++++++++ 6 files changed, 284 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/internal.rs create mode 100644 src/lib.rs create mode 100644 src/proportion.rs create mode 100644 src/tests.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..869df07 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..123f3e7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "proportion" +version = "1.0.0" +authors = ["Ben Bridle"] +edition = "2021" +description = "Common proportion type for representing a value ranging from 0 to 1" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dev-dependencies] +mini_paste = "0.1.11" diff --git a/src/internal.rs b/src/internal.rs new file mode 100644 index 0000000..ca099a2 --- /dev/null +++ b/src/internal.rs @@ -0,0 +1,90 @@ +pub trait Internal: Copy + Clone + PartialEq + std::ops::Sub { + const MIN: Self; + const MAX: Self; + + fn clamp(self) -> Self; + fn as_u8(self) -> u8; + fn as_f32(self) -> f32; + fn as_f64(self) -> f64; + + fn mul_u8(self, value: u8) -> u8; + fn mul_u16(self, value: u16) -> u16; + fn mul_u32(self, value: u32) -> u32; + fn mul_f32(self, value: f32) -> f32; + fn mul_f64(self, value: f64) -> f64; +} + +impl Internal for u8 { + const MIN: Self = u8::MIN; + const MAX: Self = u8::MAX; + + fn clamp(self) -> Self { + self + } + fn as_u8(self) -> u8 { + self + } + fn as_f32(self) -> f32 { + self as f32 / u8::MAX as f32 + } + fn as_f64(self) -> f64 { + self as f64 / u8::MAX as f64 + } + + fn mul_u8(self, value: u8) -> u8 { + (value as u16 * self as u16 / 255u16) as u8 + } + fn mul_u16(self, value: u16) -> u16 { + (value as u32 * self as u32 / 255u32) as u16 + } + fn mul_u32(self, value: u32) -> u32 { + (value as u64 * self as u64 / 255u64) as u32 + } + fn mul_f32(self, value: f32) -> f32 { + value * self.as_f32() + } + fn mul_f64(self, value: f64) -> f64 { + value * self.as_f64() + } +} + +macro_rules! impl_for_float { + ($type:ty) => { + impl Internal for $type { + const MIN: Self = 0.0; + const MAX: Self = 1.0; + + fn clamp(self) -> Self { + <$type>::clamp(self, ::MIN, ::MAX) + } + fn as_u8(self) -> u8 { + (self * 255.0) as u8 + } + fn as_f32(self) -> f32 { + self as f32 + } + fn as_f64(self) -> f64 { + self as f64 + } + + fn mul_u8(self, value: u8) -> u8 { + (value as Self * self) as u8 + } + fn mul_u16(self, value: u16) -> u16 { + (value as Self * self).round() as u16 + } + fn mul_u32(self, value: u32) -> u32 { + (value as Self * self).round() as u32 + } + fn mul_f32(self, value: f32) -> f32 { + value * self.as_f32() + } + fn mul_f64(self, value: f64) -> f64 { + value * self.as_f64() + } + } + }; +} + +impl_for_float!(f32); +impl_for_float!(f64); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fd5f93c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +#[cfg(test)] +mod tests; + +mod internal; +mod proportion; + +pub use crate::proportion::Proportion; +pub use internal::Internal; diff --git a/src/proportion.rs b/src/proportion.rs new file mode 100644 index 0000000..aa8d1ae --- /dev/null +++ b/src/proportion.rs @@ -0,0 +1,107 @@ +use crate::*; + +#[derive(Copy, Clone)] +pub struct Proportion { + pub(crate) value: T, +} + +impl Proportion { + pub fn new(value: I) -> Self { + Self { value: value.clamp() } + } + + pub const MIN: Self = Proportion { + value: ::MIN, + }; + pub const MAX: Self = Proportion { + value: ::MAX, + }; + pub const ZERO: Self = Self::MIN; + pub const ONE: Self = Self::MAX; + + pub fn set(&mut self, new_value: I) { + self.value = new_value.clamp(); + } + + pub fn is_min(&self) -> bool { + self.value == I::MIN + } + pub fn is_max(&self) -> bool { + self.value == I::MAX + } + + pub fn is_zero(&self) -> bool { + self.is_min() + } + pub fn is_one(&self) -> bool { + self.is_max() + } + + pub fn as_u8(&self) -> u8 { + self.value.as_u8() + } + pub fn as_f32(&self) -> f32 { + self.value.as_f32() + } + pub fn as_f64(&self) -> f64 { + self.value.as_f64() + } + + pub fn invert(&self) -> Self { + Self { + value: I::MAX - self.value, + } + } + + pub fn value(&self) -> I { + self.value + } +} + +macro_rules! impl_mul { + ($type:ty, $method:ident) => { + impl std::ops::Mul> for $type { + type Output = $type; + fn mul(self, proportion: Proportion) -> $type { + proportion.value.$method(self) + } + } + impl std::ops::Mul<$type> for Proportion { + type Output = $type; + fn mul(self, value: $type) -> $type { + self.value.$method(value) + } + } + impl std::ops::Mul<&Proportion> for $type { + type Output = $type; + fn mul(self, proportion: &Proportion) -> $type { + proportion.value.$method(self) + } + } + impl std::ops::Mul<$type> for &Proportion { + type Output = $type; + fn mul(self, value: $type) -> $type { + self.value.$method(value) + } + } + }; +} + +impl_mul!(u8, mul_u8); +impl_mul!(u16, mul_u16); +impl_mul!(u32, mul_u32); +impl_mul!(f32, mul_f32); +impl_mul!(f64, mul_f64); + +use std::fmt::{Debug, Display, Formatter}; + +impl Debug for Proportion { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + write!(f, "Proportion {{ {:.2} }}", self.as_f32()) + } +} +impl Display for Proportion { + fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { + write!(f, "{:.2}", self.as_f32()) + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..28e9038 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,66 @@ +use crate::*; + +macro_rules! test_fn { + ($test_name:tt => $test_body:tt) => { + mini_paste::item! { + #[test] + fn $test_name() { + $test_body + } + } + }; +} + +macro_rules! test_edges_for { + ($vtype:ty, P[$ptype:ty]) => { + test_edges_for!($vtype, P[$ptype], <$vtype>::MIN, <$vtype>::MAX); + }; + ($vtype:ty, P[$ptype:ty], $vmin:expr, $vmax:expr) => { + test_fn!([< test_ $vtype _min_by_ $ptype _min >] => { + assert!($vmin * Proportion::<$ptype>::MIN == $vmin); + }); + test_fn!([< test_ $vtype _min_by_ $ptype _max >] => { + assert!($vmin * Proportion::<$ptype>::MAX == $vmin); + }); + test_fn!([< test_ $vtype _max_by_ $ptype _min >] => { + assert!($vmax * Proportion::<$ptype>::MIN == $vmin); + }); + test_fn!([< test_ $vtype _max_by_ $ptype _max >] => { + assert!($vmax * Proportion::<$ptype>::MAX == $vmax); + }); + }; +} + +macro_rules! test_edges_for_ptype { + (P[$ptype:ty]) => { + test_edges_for!(u8, P[$ptype]); + test_edges_for!(u16, P[$ptype]); + test_edges_for!(u32, P[$ptype]); + test_edges_for!(f32, P[$ptype], 0.0, 1.0); + test_edges_for!(f64, P[$ptype], 0.0, 1.0); + }; +} + +test_edges_for_ptype!(P[u8]); +test_edges_for_ptype!(P[f32]); +test_edges_for_ptype!(P[f64]); + +macro_rules! test_invert_for { + ($ptype:ty) => { + test_fn!([< test_ $ptype _min_invert >] => { + assert!(Proportion::<$ptype>::MIN.invert().value == Proportion::<$ptype>::MAX.value); + }); + test_fn!([< test_ $ptype _max_invert >] => { + assert!(Proportion::<$ptype>::MAX.invert().value == Proportion::<$ptype>::MIN.value); + }); + } +} + +test_invert_for!(u8); +test_invert_for!(f32); +test_invert_for!(f64); + +// TODO: Test MAX*MID, MIN*MID, MID*MID=QUARTER +// test_fn!(test_u8_max_by_u8_mid => { +// assert!(u8::MAX * Proportion::::new(128) == 128u8); +// }); -- cgit v1.2.3-70-g09d2