wallswitch/
dimension.rs

1use crate::{Colors, Config, Countable, MyResult, ResultExt};
2use serde::{Deserialize, Serialize};
3use std::{error::Error, fmt, num::ParseIntError};
4
5/// Dimension - width and length - of an image.
6///
7/// Image Size, Attribute, properties.
8#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
9pub struct Dimension {
10    /// width of an image
11    pub width: u64,
12    /// length of an image
13    pub height: u64,
14}
15
16impl Default for Dimension {
17    /// Default 4K UHD Resolution
18    fn default() -> Self {
19        Dimension {
20            width: 3840,
21            height: 2160,
22        }
23    }
24}
25
26impl Dimension {
27    /// Get an instance of Dimension by specifying concrete values ​​for each of the fields.
28    pub fn new(string: &str) -> MyResult<Dimension> {
29        let numbers: Vec<u64> = split_str(string).unwrap_result();
30        let (width, height) = (numbers[0], numbers[1]);
31        Ok(Dimension { width, height })
32    }
33
34    /// Get the minimum value between height and width.
35    pub fn minimum(&self) -> u64 {
36        self.height.min(self.width)
37    }
38
39    /// Get the maximum value between height and width.
40    pub fn maximum(&self) -> u64 {
41        self.height.max(self.width)
42    }
43
44    /// Check if the minimum and maximum value are valid.
45    pub fn is_valid(&self, config: &Config) -> bool {
46        config.in_range(self.minimum()) && config.in_range(self.maximum())
47    }
48
49    /// Get error messages related to the minimum value.
50    pub fn get_log_min(&self, config: &Config) -> String {
51        // Number of digits of maximum value
52        let num = self.maximum().count_chars();
53        let min = self.minimum();
54        let min = format!("{min:>num$}");
55        if !config.in_range(self.minimum()) {
56            format!(
57                "Minimum dimension: {min}. The condition ({config_min} <= {min} <= {config_max}) is false.\n",
58                min = min.yellow(),
59                config_min = config.min_dimension.green(),
60                config_max = config.max_dimension.green(),
61            )
62        } else {
63            "".to_string()
64        }
65    }
66
67    /// Get error messages related to the maximum value.
68    pub fn get_log_max(&self, config: &Config) -> String {
69        if !config.in_range(self.maximum()) {
70            format!(
71                "Maximum dimension: {max}. The condition ({config_min} <= {max} <= {config_max}) is false.\n",
72                max = self.maximum().yellow(),
73                config_min = config.min_dimension.green(),
74                config_max = config.max_dimension.green(),
75            )
76        } else {
77            "".to_string()
78        }
79    }
80}
81
82impl fmt::Display for Dimension {
83    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
84        let height = self.height;
85        let width = self.width;
86
87        let max = height.max(width);
88        let n = max.count_chars();
89
90        write!(
91            f,
92            "Dimension {{ height: {height:>n$}, width: {width:>n$} }}",
93        )
94    }
95}
96
97/**
98Split string into two numbers
99
100Example:
101```
102use wallswitch::{split_str, MyResult};
103
104fn main() -> MyResult<()> {
105    let string1: &str = "123x4567";
106    let string2: &str = " 123 x 4567 \n";
107
108    let integers1: Vec<u64> = split_str(string1)?;
109    let integers2: Vec<u64> = split_str(string2)?;
110
111    assert_eq!(integers1, [123, 4567]);
112    assert_eq!(integers1, integers2);
113
114    Ok(())
115}
116```
117*/
118pub fn split_str(string: &str) -> MyResult<Vec<u64>> {
119    let numbers: Vec<u64> = string
120        .split('x')
121        .map(|s| s.trim().parse::<u64>())
122        .collect::<Result<Vec<u64>, ParseIntError>>()
123        .map_err(|parse_error| {
124            // Add a custom error message
125            DimError::InvalidParse(string.to_string(), parse_error)
126        })?;
127
128    if numbers.len() != 2 {
129        return Err(Box::new(DimError::InvalidFormat));
130    }
131
132    if numbers.contains(&0) {
133        return Err(Box::new(DimError::ZeroDimension));
134    }
135
136    Ok(numbers)
137}
138
139#[derive(Debug)]
140enum DimError {
141    /// Parse InvalidFormat
142    InvalidFormat,
143    /// Parse ZeroDimension
144    ZeroDimension,
145    /// Parse InvalidParse
146    InvalidParse(String, ParseIntError),
147}
148
149impl std::fmt::Display for DimError {
150    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
151        match self {
152            DimError::InvalidParse(string, parse_error) => {
153                write!(f, "Error: '{string}' split failed!\n{parse_error}",)
154            }
155            DimError::InvalidFormat => write!(f, "Invalid format: expected 'heightxwidth'."),
156            DimError::ZeroDimension => write!(f, "Width or height cannot be zero."),
157        }
158    }
159}
160
161/// If we want to use std::error::Error in main, we need to implement it for DimError
162impl Error for DimError {}
163
164#[cfg(test)]
165mod test_dimension {
166    use super::*;
167
168    #[test]
169    /// `cargo test -- --show-output split_str_sample_1`
170    fn split_str_sample_1() {
171        let string = " 123 x 4567 ";
172
173        let result = split_str(string);
174
175        match result {
176            Ok(numbers) => {
177                assert_eq!(numbers[0], 123);
178                assert_eq!(numbers[1], 4567);
179            }
180            Err(err) => panic!("Unexpected error: {err}"),
181        }
182    }
183
184    #[test]
185    /// `cargo test -- --show-output split_str_sample_2`
186    ///
187    /// <https://doc.rust-lang.org/rust-by-example/error.html>
188    ///
189    /// <https://world.hey.com/ksiva/easy-error-handling-in-rust-19555479>
190    fn split_str_sample_2() {
191        let string = "x4567";
192
193        let result: MyResult<Vec<u64>> = split_str(string);
194        dbg!(&result);
195
196        let result_to_string: Result<Vec<u64>, String> =
197            split_str(string).map_err(|e| e.to_string());
198        dbg!(&result_to_string);
199
200        let opt_error: String = result_to_string.unwrap_err().to_string();
201        let parse_error = "cannot parse integer from empty string";
202        let output = format!("Error: '{string}' split failed!\n{parse_error}",);
203
204        let error = split_str(string).unwrap_err();
205        assert!(error.is::<DimError>());
206
207        assert!(result.is_err());
208        assert_eq!(opt_error, output);
209    }
210
211    #[test]
212    /// `cargo test -- --show-output split_str_sample_3`
213    fn split_str_sample_3() {
214        let string = "12ab3x4567";
215
216        let result: MyResult<Vec<u64>> = split_str(string);
217        dbg!(&result);
218
219        let result_to_string: Result<Vec<u64>, String> =
220            split_str(string).map_err(|e| e.to_string());
221        dbg!(&result_to_string);
222
223        let opt_error: String = result_to_string.unwrap_err().to_string();
224        let parse_error = "invalid digit found in string";
225        let output = format!("Error: '{string}' split failed!\n{parse_error}",);
226
227        let error = split_str(string).unwrap_err();
228        assert!(error.is::<DimError>());
229
230        assert!(result.is_err());
231        assert_eq!(opt_error, output);
232    }
233
234    #[test]
235    /// `cargo test -- --show-output split_str_sample_4`
236    fn split_str_sample_4() {
237        let string = "57x124x89";
238
239        let result = split_str(string);
240        dbg!(&result);
241        assert!(result.is_err());
242        assert_eq!(
243            result.unwrap_err().to_string(),
244            "Invalid format: expected 'heightxwidth'."
245        );
246    }
247
248    #[test]
249    /// `cargo test -- --show-output split_str_sample_5`
250    fn split_str_sample_5() {
251        let string = "57x0";
252
253        let result = split_str(string);
254        dbg!(&result);
255        assert!(result.is_err());
256        assert_eq!(
257            result.unwrap_err().to_string(),
258            "Width or height cannot be zero."
259        );
260    }
261}