Skip to main content

use_measure/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive readable measure helpers.
3//!
4//! The helpers here focus on character-count estimates, not a full text
5//! layout engine.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_measure::{Measure, characters_per_line, container_width_for_measure, is_readable_measure};
11//!
12//! let measure = Measure::new(66.0).unwrap();
13//!
14//! assert_eq!(measure.characters_per_line(), 66.0);
15//! assert!(measure.is_readable());
16//! assert_eq!(characters_per_line(528.0, 8.0).unwrap(), 66.0);
17//! assert_eq!(container_width_for_measure(66.0, 8.0).unwrap(), 528.0);
18//! assert!(is_readable_measure(66.0).unwrap());
19//! ```
20
21#[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}