Skip to main content

graphitepdf_utils/
string.rs

1#[derive(Clone, Copy, Debug, PartialEq)]
2pub struct PercentMatch {
3    pub value: f32,
4    pub percent: f32,
5}
6
7pub fn upper_first(input: &str) -> String {
8    let mut chars = input.chars();
9
10    match chars.next() {
11        Some(first) => {
12            let mut result = String::new();
13            result.extend(first.to_uppercase());
14            result.push_str(chars.as_str());
15            result
16        }
17        None => String::new(),
18    }
19}
20
21pub fn capitalize(input: &str) -> String {
22    let mut result = String::with_capacity(input.len());
23    let mut should_capitalize = true;
24
25    for ch in input.chars() {
26        if ch.is_whitespace() {
27            should_capitalize = true;
28            result.push(ch);
29            continue;
30        }
31
32        if should_capitalize {
33            result.extend(ch.to_uppercase());
34            should_capitalize = false;
35        } else {
36            result.push(ch);
37        }
38    }
39
40    result
41}
42
43pub fn match_percent(input: &str) -> Option<PercentMatch> {
44    let trimmed = input.trim();
45
46    if !trimmed.ends_with('%') {
47        return None;
48    }
49
50    let numeric = trimmed[..trimmed.len().saturating_sub(1)].trim();
51    let value = numeric.parse::<f32>().ok()?;
52
53    Some(PercentMatch {
54        value,
55        percent: value / 100.0,
56    })
57}
58
59pub fn parse_float(input: &str) -> Option<f32> {
60    let trimmed = input.trim();
61    let mut consumed = 0_usize;
62    let mut seen_digit = false;
63    let mut seen_decimal = false;
64
65    for (index, ch) in trimmed.char_indices() {
66        let allowed = if index == 0 && (ch == '+' || ch == '-') {
67            true
68        } else if ch.is_ascii_digit() {
69            seen_digit = true;
70            true
71        } else if ch == '.' && !seen_decimal {
72            seen_decimal = true;
73            true
74        } else {
75            false
76        };
77
78        if !allowed {
79            break;
80        }
81
82        consumed = index + ch.len_utf8();
83    }
84
85    if !seen_digit || consumed == 0 {
86        return None;
87    }
88
89    trimmed[..consumed].parse::<f32>().ok()
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn capitalizes_words_and_first_letter() {
98        assert_eq!(capitalize("hello world"), "Hello World");
99        assert_eq!(upper_first("hello"), "Hello");
100    }
101
102    #[test]
103    fn parses_percentages() {
104        assert_eq!(
105            match_percent("50%"),
106            Some(PercentMatch {
107                value: 50.0,
108                percent: 0.5,
109            })
110        );
111        assert_eq!(match_percent("abc"), None);
112    }
113
114    #[test]
115    fn parses_leading_floats() {
116        assert_eq!(parse_float("3.14"), Some(314.0_f32 / 100.0));
117        assert_eq!(parse_float("10px"), Some(10.0));
118        assert_eq!(parse_float("abc"), None);
119    }
120}