1pub fn scaled_decimal(text: &str, scale: i8) -> Result<i128, String> {
7 let bad = || format!("`{text}` isn't a decimal that fits scale {scale}");
8 let t = text.trim();
9 let (neg, t) = match t.strip_prefix('-') {
10 Some(r) => (true, r),
11 None => (false, t.strip_prefix('+').unwrap_or(t)),
12 };
13 let (int, frac) = t.split_once('.').unwrap_or((t, ""));
14 if int.is_empty() && frac.is_empty() || !int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit()) {
15 return Err(bad());
16 }
17 let mut digits = format!("{int}{frac}");
19 let mut exp = -(frac.len() as i32);
20 let target = -i32::from(scale);
21 while exp < target {
23 match digits.pop() {
24 Some('0') => exp += 1,
25 Some(_) => return Err(bad()),
26 None => break,
27 }
28 }
29 while exp > target {
30 digits.push('0');
31 exp -= 1;
32 }
33 let digits = digits.trim_start_matches('0');
34 let v: i128 = if digits.is_empty() {
35 0
36 } else {
37 digits.parse().map_err(|_| bad())?
38 };
39 Ok(if neg { -v } else { v })
40}
41
42pub fn parse_cell(s: &str) -> Option<(u32, u16)> {
45 let s = s.trim();
46 let s = s.strip_prefix('$').unwrap_or(s);
47 let split = s.find(|c: char| !c.is_ascii_alphabetic())?;
48 let (letters, rest) = s.split_at(split);
49 let digits = rest.strip_prefix('$').unwrap_or(rest);
50 if letters.is_empty()
51 || letters.len() > 3
52 || digits.is_empty()
53 || !digits.bytes().all(|b| b.is_ascii_digit())
54 {
55 return None;
56 }
57 let col = letters
58 .to_ascii_uppercase()
59 .bytes()
60 .fold(0u32, |acc, b| acc * 26 + u32::from(b - b'A' + 1));
61 let row: u32 = digits.parse().ok()?;
62 if col == 0 || col > 16_384 || row == 0 || row > 1_048_576 {
63 return None;
64 }
65 Some((row - 1, (col - 1) as u16))
66}
67
68pub fn percent_encode(s: &str, keep_slash: bool) -> String {
70 let mut out = String::with_capacity(s.len());
71 for b in s.bytes() {
72 match b {
73 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
74 b'/' if keep_slash => out.push('/'),
75 b => out.push_str(&format!("%{b:02X}")),
76 }
77 }
78 out
79}
80
81pub fn summarize(text: &str, max: usize) -> String {
83 let one = text.split_whitespace().collect::<Vec<_>>().join(" ");
84 if one.chars().count() > max {
85 format!("{}…", one.chars().take(max).collect::<String>())
86 } else {
87 one
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn scales_decimals_exactly() {
97 assert_eq!(scaled_decimal("12.34", 2), Ok(1234));
98 assert_eq!(scaled_decimal("-0.5", 3), Ok(-500));
99 assert_eq!(scaled_decimal("7", 2), Ok(700));
100 assert_eq!(scaled_decimal("12.3400", 2), Ok(1234));
101 assert_eq!(scaled_decimal("1200", -2), Ok(12));
102 assert_eq!(scaled_decimal("0.000", 0), Ok(0));
103 }
104
105 #[test]
106 fn refuses_to_drop_significant_digits() {
107 assert!(scaled_decimal("12.345", 2).is_err());
108 assert!(scaled_decimal("1250", -2).is_err());
109 assert!(scaled_decimal("abc", 2).is_err());
110 assert!(scaled_decimal("", 2).is_err());
111 }
112
113 #[test]
114 fn parses_cell_references() {
115 assert_eq!(parse_cell("A1"), Some((0, 0)));
116 assert_eq!(parse_cell("$B$12"), Some((11, 1)));
117 assert_eq!(parse_cell("b12"), Some((11, 1)));
118 assert_eq!(parse_cell("XFD1048576"), Some((1_048_575, 16_383)));
119 for bad in ["XFE1", "A0", "1A", "B$1$2", "$$A1", "A", "12", "A1B"] {
120 assert_eq!(parse_cell(bad), None, "{bad}");
121 }
122 }
123
124 #[test]
125 fn percent_encodes() {
126 assert_eq!(percent_encode("a b/c.csv", true), "a%20b/c.csv");
127 assert_eq!(percent_encode("a b/c", false), "a%20b%2Fc");
128 }
129}