6
\$\begingroup\$

I want to create a function that takes a string and returns an array of versions of it in which always one pair of characters is transposed (swapped).

I came up with this:

fn transposes(s: &str) -> Vec<String> {
    let mut vec = vec![];

    let b = s.as_bytes();

    for i in 1..b.len() {
        let mut transpose = String::with_capacity(s.len());
        transpose.push_str(&s[..i - 1]);
        transpose.push(s.as_bytes()[i] as char);
        transpose.push(s.as_bytes()[i - 1] as char);
        transpose.push_str(&s[i + 1..]);

        vec.push(transpose);
    }

    vec
}

fn main() {
    let s = "maxi".to_owned();
    println!("{:?}", transposes(&s));   
}

Output is:

["amxi", "mxai", "maix"]

That result is what I want, it does:

  • First swap m and a
  • Then swap a and x
  • Then swap i and x

I don't find the code super readable because of the string slicing going on. In C it would be a mere "pointer swap", but my Rust version looks a bit more involved.

Is there a better way to do what I want?

It is a fair assumption that this will only work correctly for ASCII strings.

\$\endgroup\$
4
\$\begingroup\$
  1. If it's just ASCII, treat it as a byte array all the way until the end. Vec has a swap method and you can reconstruct a String from the bytes.
  2. Use map and collect instead of allocating a Vec and manually pushing.
  3. There's no need to allocate a String to pass to &str.
fn transposes(s: &str) -> Vec<String> {
    let bytes = s.as_bytes();

    (1..bytes.len()).map(|i| {
        let mut transpose = bytes.to_owned();
        transpose.swap(i - 1, i);
        String::from_utf8(transpose).expect("Invalid UTF-8")
    }).collect()
}

fn main() {
    println!("{:?}", transposes("maxi"));
}

Even better, if your data is ASCII, encode that in the type system:

extern crate ascii;

use ascii::{AsciiStr, AsciiString};

fn transposes(s: &AsciiStr) -> Vec<AsciiString> {
    (1..s.len()).map(|i| {
        let mut transpose = s.to_owned();
        transpose.as_mut_slice().swap(i - 1, i);
        transpose
    }).collect()
}

fn main() {
    let s = AsciiStr::from_ascii("maxi").expect("Not ASCII");
    println!("{:?}", transposes(s));
}

Note that the transposes function can no longer fail due to invalid strings.

\$\endgroup\$
  • \$\begingroup\$ Excellent answer as usual, my code is still far away from good Rust apparently. With There's no need to allocate a String to pass to &str. you mean the let s = "maxi".to_owned(); ? \$\endgroup\$ – Max Feb 15 '17 at 1:01
  • \$\begingroup\$ I tried the non-ASCIIStr version and it doesn't do the same as my version though. My version outputs ["amxi", "mxai", "maix"] which is correct, your version has one incorrect transposition ["amxi", "maxi", "mxai"] (the x and i are not swapped). \$\endgroup\$ – Max Feb 15 '17 at 1:09
  • \$\begingroup\$ @Max haha, that's embarrassing. I had (i-1, 1) instead of (i-1, i). Yes, I did mean the outer to_owned was not needed. \$\endgroup\$ – Shepmaster Feb 15 '17 at 1:39
  • \$\begingroup\$ Haha I see, as they say there only two types of string algorithm errors, off by one errors. \$\endgroup\$ – Max Feb 15 '17 at 2:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.