Skip to main content

use_aspect_ratio/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive aspect-ratio helpers.
3//!
4//! These helpers validate dimensions, simplify ratios, and perform small
5//! tolerance checks.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_aspect_ratio::{AspectRatio, aspect_label, fits_aspect_ratio};
11//!
12//! let ratio = AspectRatio::new(1920, 1080).unwrap();
13//!
14//! assert!((ratio.ratio() - (16.0 / 9.0)).abs() < 1.0e-12);
15//! assert_eq!(ratio.simplified().label(), "16:9");
16//! assert_eq!(aspect_label(1920, 1080).unwrap(), "16:9");
17//! assert!(fits_aspect_ratio(1920, 1080, 1280, 720, 0.001).unwrap());
18//! ```
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct AspectRatio {
22    width: u32,
23    height: u32,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum AspectRatioError {
28    InvalidWidth,
29    InvalidHeight,
30    InvalidTolerance,
31}
32
33fn gcd(mut left: u32, mut right: u32) -> u32 {
34    while right != 0 {
35        let remainder = left % right;
36        left = right;
37        right = remainder;
38    }
39
40    left
41}
42
43fn validate_dimensions(width: u32, height: u32) -> Result<(u32, u32), AspectRatioError> {
44    if width == 0 {
45        return Err(AspectRatioError::InvalidWidth);
46    }
47
48    if height == 0 {
49        return Err(AspectRatioError::InvalidHeight);
50    }
51
52    Ok((width, height))
53}
54
55fn validate_tolerance(tolerance: f64) -> Result<f64, AspectRatioError> {
56    if !tolerance.is_finite() || tolerance < 0.0 {
57        Err(AspectRatioError::InvalidTolerance)
58    } else {
59        Ok(tolerance)
60    }
61}
62
63impl AspectRatio {
64    pub fn new(width: u32, height: u32) -> Result<Self, AspectRatioError> {
65        let (width, height) = validate_dimensions(width, height)?;
66        Ok(Self { width, height })
67    }
68
69    #[must_use]
70    pub fn width(&self) -> u32 {
71        self.width
72    }
73
74    #[must_use]
75    pub fn height(&self) -> u32 {
76        self.height
77    }
78
79    #[must_use]
80    pub fn ratio(&self) -> f64 {
81        f64::from(self.width) / f64::from(self.height)
82    }
83
84    #[must_use]
85    pub fn simplified(&self) -> Self {
86        let divisor = gcd(self.width, self.height);
87
88        Self {
89            width: self.width / divisor,
90            height: self.height / divisor,
91        }
92    }
93
94    #[must_use]
95    pub fn label(&self) -> String {
96        let simplified = self.simplified();
97        format!("{}:{}", simplified.width, simplified.height)
98    }
99}
100
101pub fn aspect_ratio(width: u32, height: u32) -> Result<f64, AspectRatioError> {
102    Ok(AspectRatio::new(width, height)?.ratio())
103}
104
105pub fn simplify_ratio(width: u32, height: u32) -> Result<(u32, u32), AspectRatioError> {
106    let simplified = AspectRatio::new(width, height)?.simplified();
107    Ok((simplified.width, simplified.height))
108}
109
110pub fn aspect_label(width: u32, height: u32) -> Result<String, AspectRatioError> {
111    Ok(AspectRatio::new(width, height)?.label())
112}
113
114pub fn fits_aspect_ratio(
115    width: u32,
116    height: u32,
117    target_width: u32,
118    target_height: u32,
119    tolerance: f64,
120) -> Result<bool, AspectRatioError> {
121    let tolerance = validate_tolerance(tolerance)?;
122    let source = aspect_ratio(width, height)?;
123    let target = aspect_ratio(target_width, target_height)?;
124
125    Ok((source - target).abs() <= tolerance)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::{AspectRatio, AspectRatioError, aspect_label, fits_aspect_ratio, simplify_ratio};
131
132    #[test]
133    fn simplifies_common_aspect_ratios() {
134        let aspect = AspectRatio::new(1920, 1080).unwrap();
135
136        assert_eq!(aspect.width(), 1920);
137        assert_eq!(aspect.height(), 1080);
138        assert!((aspect.ratio() - (16.0 / 9.0)).abs() < 1.0e-12);
139        assert_eq!(aspect.simplified(), AspectRatio::new(16, 9).unwrap());
140        assert_eq!(simplify_ratio(1920, 1080).unwrap(), (16, 9));
141        assert_eq!(aspect_label(1920, 1080).unwrap(), "16:9");
142    }
143
144    #[test]
145    fn checks_ratio_tolerance_matches() {
146        assert!(fits_aspect_ratio(1920, 1080, 1280, 720, 0.001).unwrap());
147        assert!(!fits_aspect_ratio(1920, 1080, 1024, 768, 0.001).unwrap());
148    }
149
150    #[test]
151    fn rejects_invalid_ratio_inputs() {
152        assert_eq!(
153            AspectRatio::new(0, 1080),
154            Err(AspectRatioError::InvalidWidth)
155        );
156        assert_eq!(
157            AspectRatio::new(1920, 0),
158            Err(AspectRatioError::InvalidHeight)
159        );
160        assert_eq!(
161            fits_aspect_ratio(1920, 1080, 1280, 720, f64::NAN),
162            Err(AspectRatioError::InvalidTolerance)
163        );
164    }
165}