#[must_use]
pub fn normalize_number(text: &str) -> String {
if let Some(rest) = strip_radix_prefix(text) {
let (prefix, digits) = text.split_at(text.len() - rest.len());
return format!(
"{}{}",
prefix.to_ascii_lowercase(),
digits.to_ascii_lowercase()
);
}
let (mantissa, exponent) = match text.find(['e', 'E']) {
Some(index) => text.split_at(index),
None => (text, ""),
};
let mut mantissa = mantissa.to_string();
if mantissa.starts_with('.') {
mantissa.insert(0, '0');
}
if mantissa.ends_with('.') {
mantissa.push('0');
}
format!("{mantissa}{exponent}")
}
fn strip_radix_prefix(text: &str) -> Option<&str> {
let bytes = text.as_bytes();
if bytes.len() < 3 || bytes[0] != b'0' {
return None;
}
match bytes[1] {
b'x' | b'X' | b'b' | b'B' => Some(&text[2..]),
_ => None,
}
}
#[must_use]
pub fn normalize_string(text: &str) -> String {
let Some(quote_at) = text.find(['"', '\'']) else {
return text.to_string();
};
let (prefix, quoted) = text.split_at(quote_at);
if prefix.contains(['r', 'R']) {
return text.to_string();
}
let quote = quoted.as_bytes()[0] as char;
let triple = [quote; 3].iter().collect::<String>();
if quoted.starts_with(&triple) {
return text.to_string();
}
if quoted.len() < 2 || !quoted.ends_with(quote) {
return text.to_string();
}
let body = "ed[1..quoted.len() - 1];
let units = split_units(body);
let doubles = units.iter().filter(|unit| represents(unit, '"')).count();
let singles = units.iter().filter(|unit| represents(unit, '\'')).count();
let target = if doubles <= singles { '"' } else { '\'' };
let mut out = String::with_capacity(text.len());
out.push_str(prefix);
out.push(target);
for unit in &units {
if represents(unit, target) {
out.push('\\');
out.push(target);
} else if represents(unit, '"') {
out.push('"');
} else if represents(unit, '\'') {
out.push('\'');
} else {
out.push_str(unit);
}
}
out.push(target);
out
}
fn split_units(body: &str) -> Vec<&str> {
let mut units = Vec::new();
let mut chars = body.char_indices();
while let Some((start, c)) = chars.next() {
if c == '\\'
&& let Some((next_start, next)) = chars.next()
{
units.push(&body[start..next_start + next.len_utf8()]);
continue;
}
units.push(&body[start..start + c.len_utf8()]);
}
units
}
fn represents(unit: &str, quote: char) -> bool {
let bare = unit.len() == quote.len_utf8() && unit.starts_with(quote);
let escaped = unit.starts_with('\\') && unit.ends_with(quote) && unit.chars().count() == 2;
bare || escaped
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn floats_get_a_digit_on_both_sides() {
assert_eq!(normalize_number(".234"), "0.234");
assert_eq!(normalize_number("13."), "13.0");
assert_eq!(normalize_number("0.234"), "0.234");
assert_eq!(normalize_number("13.0"), "13.0");
}
#[test]
fn hexadecimal_letters_are_lowercased() {
assert_eq!(normalize_number("0xFB8C0B"), "0xfb8c0b");
assert_eq!(normalize_number("0Xfb8c0b"), "0xfb8c0b");
assert_eq!(normalize_number("0xffff_f8f8_0000"), "0xffff_f8f8_0000");
}
#[test]
fn digit_separators_are_left_alone() {
assert_eq!(normalize_number("1_234_567_890"), "1_234_567_890");
assert_eq!(normalize_number("12345"), "12345");
assert_eq!(normalize_number("12_345"), "12_345");
}
#[test]
fn an_exponent_does_not_confuse_the_point_rules() {
assert_eq!(normalize_number("1.e5"), "1.0e5");
assert_eq!(normalize_number("1.5e-3"), "1.5e-3");
}
#[test]
fn binary_literals_keep_their_separators() {
assert_eq!(normalize_number("0b1101_0010_1010"), "0b1101_0010_1010");
}
#[test]
fn quote_choice_follows_the_style_guide_samples() {
assert_eq!(normalize_string(r#""hello world""#), r#""hello world""#);
assert_eq!(normalize_string(r#""hello 'world'""#), r#""hello 'world'""#);
assert_eq!(normalize_string(r#"'hello "world"'"#), r#"'hello "world"'"#);
assert_eq!(
normalize_string(r#""'hello' \"world\"""#),
r#""'hello' \"world\"""#
);
}
#[test]
fn single_quotes_become_double_when_that_costs_nothing() {
assert_eq!(normalize_string("'plain'"), r#""plain""#);
assert_eq!(normalize_string(r"'it\'s'"), r#""it's""#);
}
#[test]
fn double_quotes_become_single_when_that_removes_escapes() {
assert_eq!(normalize_string(r#""say \"hi\"""#), r#"'say "hi"'"#);
}
#[test]
fn a_tie_prefers_double_quotes() {
assert_eq!(normalize_string(r#"'\'a\' "b"'"#), r#""'a' \"b\"""#);
}
#[test]
fn other_escapes_survive_a_quote_change() {
assert_eq!(normalize_string(r"'a\nb\tc'"), r#""a\nb\tc""#);
assert_eq!(normalize_string(r"'\\'"), r#""\\""#);
}
#[test]
fn raw_and_triple_quoted_strings_are_left_alone() {
assert_eq!(normalize_string(r"r'raw'"), r"r'raw'");
assert_eq!(normalize_string(r"'''triple'''"), r"'''triple'''");
assert_eq!(normalize_string(r#""""triple""""#), r#""""triple""""#);
}
#[test]
fn prefixed_string_forms_keep_their_sigil() {
assert_eq!(normalize_string("&'name'"), r#"&"name""#);
assert_eq!(normalize_string("^'path'"), r#"^"path""#);
assert_eq!(normalize_string("$Node/Path"), "$Node/Path");
}
}