use core::fmt;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
#[error("value {value} is out of range {range}")]
pub struct OutOfRange {
pub value: f64,
pub range: &'static str,
}
macro_rules! ranged_unit {
(
$(#[$meta:meta])*
name = $name:ident,
repr = $repr:ty,
min = $min:expr,
max = $max:expr,
range_doc = $range_doc:literal $(,)?
) => {
$(#[$meta])*
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct $name($repr);
impl $name {
pub const MIN: Self = Self($min);
pub const MAX: Self = Self($max);
pub fn new(value: $repr) -> Result<Self, OutOfRange> {
if !value.is_finite() || value < $min || value > $max {
return Err(OutOfRange {
value: f64::from(value),
range: $range_doc,
});
}
Ok(Self(value))
}
#[must_use]
pub const fn new_unchecked(value: $repr) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> $repr {
self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
};
}
ranged_unit! {
name = Brightness,
repr = f32,
min = 0.0,
max = 1.0,
range_doc = "0.0..=1.0",
}
ranged_unit! {
name = Volume,
repr = f32,
min = 0.0,
max = 1.0,
range_doc = "0.0..=1.0",
}
ranged_unit! {
name = Pan,
repr = f32,
min = -1.0,
max = 1.0,
range_doc = "-1.0..=1.0",
}
ranged_unit! {
name = PlaybackRate,
repr = f32,
min = 0.25,
max = 4.0,
range_doc = "0.25..=4.0",
}
ranged_unit! {
name = Pitch,
repr = f32,
min = 0.5,
max = 2.0,
range_doc = "0.5..=2.0",
}
ranged_unit! {
name = Zoom,
repr = f32,
min = 1.0,
max = 100.0,
range_doc = "1.0..=100.0",
}
ranged_unit! {
name = Latitude,
repr = f64,
min = -90.0,
max = 90.0,
range_doc = "-90.0..=90.0",
}
ranged_unit! {
name = Longitude,
repr = f64,
min = -180.0,
max = 180.0,
range_doc = "-180.0..=180.0",
}
ranged_unit! {
name = RefreshRate,
repr = f32,
min = 1.0,
max = 480.0,
range_doc = "1.0..=480.0",
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn brightness_accepts_in_range() {
assert!(Brightness::new(0.0).is_ok());
assert!(Brightness::new(1.0).is_ok());
assert!(Brightness::new(0.5).is_ok());
}
#[test]
fn brightness_rejects_out_of_range() {
assert!(Brightness::new(-0.1).is_err());
assert!(Brightness::new(1.1).is_err());
assert!(Brightness::new(f32::NAN).is_err());
assert!(Brightness::new(f32::INFINITY).is_err());
}
#[test]
fn pan_accepts_negative_range() {
assert!(Pan::new(-1.0).is_ok());
assert!(Pan::new(0.0).is_ok());
assert!(Pan::new(1.0).is_ok());
assert!(Pan::new(-1.001).is_err());
}
#[test]
fn min_max_consts_are_consistent() {
assert!((Brightness::MIN.get() - 0.0).abs() < f32::EPSILON);
assert!((Brightness::MAX.get() - 1.0).abs() < f32::EPSILON);
assert!((Pan::MIN.get() - -1.0).abs() < f32::EPSILON);
assert!((Pan::MAX.get() - 1.0).abs() < f32::EPSILON);
}
}