#[inline]
fn is_sep(c: char) -> bool {
matches!(c, '_' | '-' | '.' | ' ')
}
fn flush(cur: &mut String, out: &mut Vec<String>) {
if !cur.is_empty() {
out.push(std::mem::take(cur).to_lowercase());
}
}
pub fn canonical(s: &str) -> Vec<String> {
let chars: Vec<char> = s.chars().collect();
let mut out: Vec<String> = Vec::new();
let mut cur = String::new();
for i in 0..chars.len() {
let c = chars[i];
if is_sep(c) || !c.is_ascii_alphanumeric() {
flush(&mut cur, &mut out);
continue;
}
if cur.is_empty() {
cur.push(c);
continue;
}
let prev = cur.chars().last().unwrap();
if c.is_ascii_uppercase() {
let prev_lower_or_digit = prev.is_ascii_lowercase() || prev.is_ascii_digit();
let next_lower = chars
.get(i + 1)
.map(|n| n.is_ascii_lowercase())
.unwrap_or(false);
if prev_lower_or_digit || (prev.is_ascii_uppercase() && next_lower) {
flush(&mut cur, &mut out);
}
cur.push(c);
} else {
cur.push(c);
}
}
flush(&mut cur, &mut out);
out
}
fn cap(s: &str) -> String {
let mut it = s.chars();
match it.next() {
Some(first) => first.to_ascii_uppercase().to_string() + it.as_str(),
None => String::new(),
}
}
pub fn to_snake(tokens: &[String]) -> String {
tokens.join("_")
}
pub fn to_camel(tokens: &[String]) -> String {
let mut out = String::new();
for (i, t) in tokens.iter().enumerate() {
if i == 0 {
out.push_str(t);
} else {
out.push_str(&cap(t));
}
}
out
}
pub fn to_pascal(tokens: &[String]) -> String {
tokens.iter().map(|t| cap(t)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn v(xs: &[&str]) -> Vec<String> {
xs.iter().map(|s| s.to_string()).collect()
}
#[test]
fn canonical_design_vectors() {
assert_eq!(canonical("orderTotal"), v(&["order", "total"]));
assert_eq!(canonical("order_total"), v(&["order", "total"]));
assert_eq!(canonical("Order.Total"), v(&["order", "total"]));
assert_eq!(canonical("order2Items"), v(&["order2", "items"]));
assert_eq!(canonical("HTTPServer"), v(&["http", "server"]));
assert_eq!(canonical("userID"), v(&["user", "id"]));
}
#[test]
fn canonical_edges() {
assert_eq!(canonical(""), v(&[]));
assert_eq!(canonical("order-Total"), v(&["order", "total"]));
assert_eq!(canonical("order total"), v(&["order", "total"])); assert_eq!(canonical("_id"), v(&["id"])); assert_eq!(canonical("HTTP"), v(&["http"])); }
#[test]
fn reassemble_vectors() {
let t = v(&["order", "total"]);
assert_eq!(to_snake(&t), "order_total");
assert_eq!(to_camel(&t), "orderTotal");
assert_eq!(to_pascal(&t), "OrderTotal");
}
#[test]
fn roundtrip_stable() {
for s in [
"orderTotal",
"order_total",
"Order.Total",
"userID",
"HTTPServer",
] {
let t = canonical(s);
assert_eq!(canonical(&to_snake(&t)), t);
assert_eq!(canonical(&to_camel(&t)), t);
assert_eq!(canonical(&to_pascal(&t)), t);
}
}
}