6
\$\begingroup\$

I'm learning Rust and I'd like advice on how to make my code more idiomatic. I have a set of entries that are colon separated string-number pairs, a name and a size, like so:

"foo:1234" " bar:943" "baz:666" 

I want to split these into two variables, a String and a u64 which the code below does but it feels really ugly.

  • There might be entries where one of the values is missing so I check for this by splitting the string into a vector so I can check the length. Is there a better way to do this?
  • The fact that have a vector means that when I pop the values I have to deal with pop returning an Option that might be None. This is why there are match statements that assert if the value can't be found (a case that I don't think can occur because I know the length is 2).
  • The variables, name and size are mutable because I need to set them at declaration time or the compiler complains that only one of the paths through the match initializes them and they might be uninitialized when the prints display them. Can I mark the None case in some way to let the compiler know that the print is only reached through the Some case?

As well as the above questions any suggestions for how to make this code more idiomatic would be much appreciated.

for data in entries { let mut split_data: Vec<String> = data.split(":").map(|s| s.to_string()).collect(); if split_data.len() == 2 { let mut size: u64 = 0; match split_data.pop() { Some(m) => {size = m.parse::<u64>().unwrap()}, None => {assert!(false, "error when reading size");}, } let mut name: String = "".to_string(); match split_data.pop() { Some(m) => {name = m.trim().to_string()}, None => {assert!(false, "error when reading name");}, } println!("name = {}", name); println!("size = {}", size); } } 
\$\endgroup\$

    2 Answers 2

    5
    \$\begingroup\$

    I suggest making your data type a two-element struct and implementing the std::string::FromStr trait for it. This lets you parse strings using str::parse. I think this is more idiomatic than TryFrom. Rust by Example recommends FromStr rather than TryFrom when parsing strings.

    There’s example code of how to do this for a similar two-item structin the trait doecumentation at rust-lang.org. Here’s a quick version based on that:

    use std::str::FromStr; #[derive(Debug, PartialEq)] struct Entry { name: String, size: u64, } #[derive(Debug, PartialEq, Eq)] struct ParseEntryError; impl FromStr for Entry { type Err = ParseEntryError; fn from_str(s: &str) -> Result<Self, Self::Err> { let (name, sizeStr) = s .trim() .split_once(':') .ok_or(ParseEntryError{})?; // Could indicate which error. let size = sizeStr.parse() .or(Err (ParseEntryError{}))?; Ok(Entry { name: name.to_string(), size: size, }) } } fn foo(entries: Vec<String>) -> Result<(), ParseEntryError> { for s in entries { let e: Entry = s.parse()?; println!("[{}: {}]", e.name, e.size); } Ok (()) } 

    Or maybe you’d prefer an if let or match structure like:

    impl FromStr for Entry { type Err = ParseEntryError; fn from_str(s: &str) -> Result<Self, Self::Err> { if let Some((name, sizeStr)) = s.trim().split_once(':') { if let Ok(size) = sizeStr.parse() { Ok(Entry { name: name.to_string(), size: size, }) } else { // Could give more detail about the error. Err(ParseEntryError {}) } } else { Err(ParseEntryError {}) } } // end fn } 

    You could also define Entry as &str (as Richard Neumann did in a good answer that seems to have been deleted) and remove the call .to_string(). This gets you an Entry optimized for a temporary object that will not outlive the input string you parsed, but which can’t be used for anything else.

    \$\endgroup\$
    0
      4
      \$\begingroup\$

      Personally, I would work with &str types directly instead of converting to String types. You can also index the split strings directly.

      fn main() { let entries = [ "foo:1234", " bar:943", "baz:666", "baz:666:::", "baz666", "x:y" ]; for data in entries { let split_data: Vec<&str> = data.split(":").collect(); if split_data.len() == 2 { let name = split_data[0].trim().to_string(); let size = split_data[1].parse::<u64>().unwrap(); println!("name = {}", name); println!("size = {}", size); } } } 

      This produces the same output as the provided code for the test cases (including a panic when trying to unwrap an invalid size).

      \$\endgroup\$

        Start asking to get answers

        Find the answer to your question by asking.

        Ask question

        Explore related questions

        See similar questions with these tags.