3

I want to convert "foo.bar.baz" to "FooBarBaz". My input will always be only ASCII. I tried:

let result = "foo.bar.baz"
    .to_string()
    .split(".")
    .map(|x| x[0..1].to_string().to_uppercase() + &x[1..])
    .fold("".to_string(), |acc, x| acc + &x);
println!("{}", result);

but that feels inefficient.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Harald Hoyer
  • 1,303
  • 1
  • 8
  • 8
  • 1
    You (and potential answerers) may like to read [Why is capitalizing the first letter of a string so convoluted in Rust?](https://stackoverflow.com/questions/38406793/why-is-capitalizing-the-first-letter-of-a-string-so-convoluted-in-rust) – trent Jan 19 '18 at 13:09

2 Answers2

4

Your solution is a good start. You could probably make it work without heap allocations in the "functional" style; I prefer putting complex logic into normal for loops though.

Also I don't like assuming input is in ASCII without actually checking - this should work with any string.

You probably could also use String::with_capacity in your code to avoid reallocations in standard cases.

Playground

fn dotted_to_pascal_case(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    for part in s.split('.') {
        let mut cs = part.chars();
        if let Some(c) = cs.next() {
            result.extend(c.to_uppercase());
        }
        result.push_str(cs.as_str());
    }
    result
}

fn main() {
    println!("{}", dotted_to_pascal_case("foo.bar.baz"));
}
Stefan
  • 5,304
  • 2
  • 25
  • 44
  • 1
    I probably wouldn't use this, but it amuses me that it works: `result.extend(cs.next().into_iter().flat_map(|c| c.to_uppercase()));` – Shepmaster Jan 19 '18 at 14:34
3

Stefan's answer is correct, but I decided to get rid of that first String allocation and go full-functional, without loops:

fn dotted_to_pascal_case(s: &str) -> String {
    s.split('.')
        .map(|piece| piece.chars())
        .flat_map(|mut chars| {
            chars
                .next()
                .expect("empty section between dots!")
                .to_uppercase()
                .chain(chars)
        })
        .collect()
}

fn main() {
    println!("{}", dotted_to_pascal_case("foo.bar.baz"));
}
ljedrz
  • 20,316
  • 4
  • 69
  • 97
  • Yes, looks similar. Do you know whether this will (pre)allocate the string with the correct length, or is it similar to the fold starting with an empty string? – Stefan Jan 19 '18 at 14:22
  • @Stefan I took a look at the source of `collect` and it looks like it starts with an empty string and pushes individual characters ([source](https://doc.rust-lang.org/stable/src/alloc/string.rs.html#1602-1606)). I doubt there is a significant difference between our approaches, my reasoning was primarily "not enough iterators" ;). – ljedrz Jan 19 '18 at 14:33
  • 1
    You could replace the `.expect` with `.iter()` if you want it to accept strings that contain consecutive dots. – Jmb Jan 19 '18 at 15:41
  • 1
    `FromIterator` for `String` can't usefully preallocate because any of those `char`s could be 1, 2, 3, or 4 bytes long when encoded as UTF-8. `Chars::size_hint` ([source](https://doc.rust-lang.org/beta/src/core/str/mod.rs.html#584-590)) has the inverse problem: it can't know how many `char`s the string contains because they can be variable length. (And let's not get into how changing a character's case can alter its length...) – trent Jan 19 '18 at 15:46
  • 1
    @trentcl: It could definitely usefully pre-allocate, the result will *at least* have as many bytes as there are unicode characters, so it's never wasteful to start from that much. At worst, there'd be 2 growth if a lot of characters are actually 4 bytes wide, which is better than "lots". – Matthieu M. Jan 19 '18 at 16:07