1#![forbid(unsafe_code)]
2#[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}