Skip to main content

facto_core/
normalize.rs

1/// Normalize a Python package name per PEP 503:
2/// lowercase, replace any run of [-_.] with a single hyphen.
3pub fn pypi_normalize(name: &str) -> String {
4    let mut result = String::with_capacity(name.len());
5    let mut in_separator = false;
6
7    for c in name.chars() {
8        if c == '-' || c == '_' || c == '.' {
9            if !in_separator {
10                result.push('-');
11                in_separator = true;
12            }
13            // Skip additional consecutive separators
14        } else {
15            in_separator = false;
16            // Push lowercase char
17            for lc in c.to_lowercase() {
18                result.push(lc);
19            }
20        }
21    }
22
23    result
24}
25
26#[cfg(test)]
27mod tests {
28    use super::*;
29
30    #[test]
31    fn test_pypi_normalize_basic() {
32        assert_eq!(pypi_normalize("My_Package"), "my-package");
33        assert_eq!(pypi_normalize("some.thing"), "some-thing");
34        assert_eq!(pypi_normalize("UPPER-CASE"), "upper-case");
35        assert_eq!(pypi_normalize("already-normalized"), "already-normalized");
36        assert_eq!(pypi_normalize("CamelCase"), "camelcase");
37    }
38
39    #[test]
40    fn test_pypi_normalize_consecutive_separators() {
41        assert_eq!(
42            pypi_normalize("multiple___underscores"),
43            "multiple-underscores"
44        );
45        assert_eq!(pypi_normalize("dots...and___mixed"), "dots-and-mixed");
46        assert_eq!(pypi_normalize("a_b.c-d"), "a-b-c-d");
47        assert_eq!(pypi_normalize("a-_.-b"), "a-b");
48    }
49
50    #[test]
51    fn test_pypi_normalize_edge_cases() {
52        assert_eq!(pypi_normalize("a"), "a");
53        assert_eq!(pypi_normalize(""), "");
54        assert_eq!(pypi_normalize("_leading"), "-leading");
55        assert_eq!(pypi_normalize("trailing_"), "trailing-");
56    }
57}