1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Measure {
23 characters_per_line: f64,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum MeasureError {
28 InvalidCharactersPerLine,
29 InvalidContainerWidth,
30 InvalidCharacterWidth,
31}
32
33fn validate_positive(value: f64, error: MeasureError) -> Result<f64, MeasureError> {
34 if !value.is_finite() || value <= 0.0 {
35 Err(error)
36 } else {
37 Ok(value)
38 }
39}
40
41impl Measure {
42 pub fn new(characters_per_line: f64) -> Result<Self, MeasureError> {
43 Ok(Self {
44 characters_per_line: validate_positive(
45 characters_per_line,
46 MeasureError::InvalidCharactersPerLine,
47 )?,
48 })
49 }
50
51 #[must_use]
52 pub fn characters_per_line(&self) -> f64 {
53 self.characters_per_line
54 }
55
56 #[must_use]
57 pub fn is_readable(&self) -> bool {
58 (45.0..=90.0).contains(&self.characters_per_line)
59 }
60}
61
62pub fn characters_per_line(
63 container_width_px: f64,
64 average_character_width_px: f64,
65) -> Result<f64, MeasureError> {
66 Ok(
67 validate_positive(container_width_px, MeasureError::InvalidContainerWidth)?
68 / validate_positive(
69 average_character_width_px,
70 MeasureError::InvalidCharacterWidth,
71 )?,
72 )
73}
74
75pub fn container_width_for_measure(
76 characters_per_line: f64,
77 average_character_width_px: f64,
78) -> Result<f64, MeasureError> {
79 Ok(
80 validate_positive(characters_per_line, MeasureError::InvalidCharactersPerLine)?
81 * validate_positive(
82 average_character_width_px,
83 MeasureError::InvalidCharacterWidth,
84 )?,
85 )
86}
87
88pub fn is_readable_measure(characters_per_line: f64) -> Result<bool, MeasureError> {
89 let characters_per_line =
90 validate_positive(characters_per_line, MeasureError::InvalidCharactersPerLine)?;
91 Ok((45.0..=90.0).contains(&characters_per_line))
92}
93
94#[cfg(test)]
95mod tests {
96 use super::{
97 characters_per_line, container_width_for_measure, is_readable_measure, Measure,
98 MeasureError,
99 };
100
101 #[test]
102 fn computes_readable_measure_helpers() {
103 let measure = Measure::new(66.0).unwrap();
104
105 assert_eq!(measure.characters_per_line(), 66.0);
106 assert!(measure.is_readable());
107 assert_eq!(characters_per_line(528.0, 8.0).unwrap(), 66.0);
108 assert_eq!(container_width_for_measure(66.0, 8.0).unwrap(), 528.0);
109 }
110
111 #[test]
112 fn checks_readable_measure_thresholds() {
113 assert!(is_readable_measure(45.0).unwrap());
114 assert!(is_readable_measure(90.0).unwrap());
115 assert!(!is_readable_measure(44.9).unwrap());
116 assert!(!is_readable_measure(90.1).unwrap());
117 }
118
119 #[test]
120 fn rejects_invalid_measure_inputs() {
121 assert_eq!(
122 Measure::new(0.0),
123 Err(MeasureError::InvalidCharactersPerLine)
124 );
125 assert_eq!(
126 characters_per_line(-1.0, 8.0),
127 Err(MeasureError::InvalidContainerWidth)
128 );
129 assert_eq!(
130 container_width_for_measure(66.0, 0.0),
131 Err(MeasureError::InvalidCharacterWidth)
132 );
133 }
134}