1#[must_use]
20pub fn normalize_number(text: &str) -> String {
21 if let Some(rest) = strip_radix_prefix(text) {
22 let (prefix, digits) = text.split_at(text.len() - rest.len());
23 return format!(
24 "{}{}",
25 prefix.to_ascii_lowercase(),
26 digits.to_ascii_lowercase()
27 );
28 }
29
30 let (mantissa, exponent) = match text.find(['e', 'E']) {
34 Some(index) => text.split_at(index),
35 None => (text, ""),
36 };
37
38 let mut mantissa = mantissa.to_string();
39 if mantissa.starts_with('.') {
40 mantissa.insert(0, '0');
41 }
42 if mantissa.ends_with('.') {
43 mantissa.push('0');
44 }
45 format!("{mantissa}{exponent}")
46}
47
48fn strip_radix_prefix(text: &str) -> Option<&str> {
49 let bytes = text.as_bytes();
50 if bytes.len() < 3 || bytes[0] != b'0' {
51 return None;
52 }
53 match bytes[1] {
54 b'x' | b'X' | b'b' | b'B' => Some(&text[2..]),
55 _ => None,
56 }
57}
58
59#[must_use]
70pub fn normalize_string(text: &str) -> String {
71 let Some(quote_at) = text.find(['"', '\'']) else {
72 return text.to_string();
74 };
75 let (prefix, quoted) = text.split_at(quote_at);
76
77 if prefix.contains(['r', 'R']) {
78 return text.to_string();
79 }
80
81 let quote = quoted.as_bytes()[0] as char;
82 let triple = [quote; 3].iter().collect::<String>();
83 if quoted.starts_with(&triple) {
84 return text.to_string();
85 }
86
87 if quoted.len() < 2 || !quoted.ends_with(quote) {
90 return text.to_string();
91 }
92 let body = "ed[1..quoted.len() - 1];
93
94 let units = split_units(body);
95 let doubles = units.iter().filter(|unit| represents(unit, '"')).count();
96 let singles = units.iter().filter(|unit| represents(unit, '\'')).count();
97
98 let target = if doubles <= singles { '"' } else { '\'' };
100
101 let mut out = String::with_capacity(text.len());
102 out.push_str(prefix);
103 out.push(target);
104 for unit in &units {
105 if represents(unit, target) {
106 out.push('\\');
107 out.push(target);
108 } else if represents(unit, '"') {
109 out.push('"');
110 } else if represents(unit, '\'') {
111 out.push('\'');
112 } else {
113 out.push_str(unit);
114 }
115 }
116 out.push(target);
117 out
118}
119
120fn split_units(body: &str) -> Vec<&str> {
125 let mut units = Vec::new();
126 let mut chars = body.char_indices();
127 while let Some((start, c)) = chars.next() {
128 if c == '\\'
129 && let Some((next_start, next)) = chars.next()
130 {
131 units.push(&body[start..next_start + next.len_utf8()]);
132 continue;
133 }
134 units.push(&body[start..start + c.len_utf8()]);
135 }
136 units
137}
138
139fn represents(unit: &str, quote: char) -> bool {
141 let bare = unit.len() == quote.len_utf8() && unit.starts_with(quote);
142 let escaped = unit.starts_with('\\') && unit.ends_with(quote) && unit.chars().count() == 2;
143 bare || escaped
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn floats_get_a_digit_on_both_sides() {
152 assert_eq!(normalize_number(".234"), "0.234");
154 assert_eq!(normalize_number("13."), "13.0");
155 assert_eq!(normalize_number("0.234"), "0.234");
156 assert_eq!(normalize_number("13.0"), "13.0");
157 }
158
159 #[test]
160 fn hexadecimal_letters_are_lowercased() {
161 assert_eq!(normalize_number("0xFB8C0B"), "0xfb8c0b");
162 assert_eq!(normalize_number("0Xfb8c0b"), "0xfb8c0b");
163 assert_eq!(normalize_number("0xffff_f8f8_0000"), "0xffff_f8f8_0000");
164 }
165
166 #[test]
167 fn digit_separators_are_left_alone() {
168 assert_eq!(normalize_number("1_234_567_890"), "1_234_567_890");
169 assert_eq!(normalize_number("12345"), "12345");
170 assert_eq!(normalize_number("12_345"), "12_345");
171 }
172
173 #[test]
174 fn an_exponent_does_not_confuse_the_point_rules() {
175 assert_eq!(normalize_number("1.e5"), "1.0e5");
176 assert_eq!(normalize_number("1.5e-3"), "1.5e-3");
177 }
178
179 #[test]
180 fn binary_literals_keep_their_separators() {
181 assert_eq!(normalize_number("0b1101_0010_1010"), "0b1101_0010_1010");
182 }
183
184 #[test]
185 fn quote_choice_follows_the_style_guide_samples() {
186 assert_eq!(normalize_string(r#""hello world""#), r#""hello world""#);
188 assert_eq!(normalize_string(r#""hello 'world'""#), r#""hello 'world'""#);
189 assert_eq!(normalize_string(r#"'hello "world"'"#), r#"'hello "world"'"#);
190 assert_eq!(
191 normalize_string(r#""'hello' \"world\"""#),
192 r#""'hello' \"world\"""#
193 );
194 }
195
196 #[test]
197 fn single_quotes_become_double_when_that_costs_nothing() {
198 assert_eq!(normalize_string("'plain'"), r#""plain""#);
199 assert_eq!(normalize_string(r"'it\'s'"), r#""it's""#);
200 }
201
202 #[test]
203 fn double_quotes_become_single_when_that_removes_escapes() {
204 assert_eq!(normalize_string(r#""say \"hi\"""#), r#"'say "hi"'"#);
205 }
206
207 #[test]
208 fn a_tie_prefers_double_quotes() {
209 assert_eq!(normalize_string(r#"'\'a\' "b"'"#), r#""'a' \"b\"""#);
210 }
211
212 #[test]
213 fn other_escapes_survive_a_quote_change() {
214 assert_eq!(normalize_string(r"'a\nb\tc'"), r#""a\nb\tc""#);
215 assert_eq!(normalize_string(r"'\\'"), r#""\\""#);
216 }
217
218 #[test]
219 fn raw_and_triple_quoted_strings_are_left_alone() {
220 assert_eq!(normalize_string(r"r'raw'"), r"r'raw'");
221 assert_eq!(normalize_string(r"'''triple'''"), r"'''triple'''");
222 assert_eq!(normalize_string(r#""""triple""""#), r#""""triple""""#);
223 }
224
225 #[test]
226 fn prefixed_string_forms_keep_their_sigil() {
227 assert_eq!(normalize_string("&'name'"), r#"&"name""#);
228 assert_eq!(normalize_string("^'path'"), r#"^"path""#);
229 assert_eq!(normalize_string("$Node/Path"), "$Node/Path");
231 }
232}