graphitepdf_utils/
string.rs1#[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}