Parse an integer, ignoring any non-numeric suffix

Viewed 806

I have a list of strings all starting with a number. However, I do not know what they end with. Some examples might be "12/1/1", "33", or "17abc". I want to read the number at the start of each string, turning the examples into 12, 33, and 17 respectively.

I have gotten it to work with the code below.

"12/1/1".chars().take_while(|c| c.is_numeric()).collect::<String>().parse::<usize>()

However, I do feel like a more efficient/cleaner solution exists. One without the collection of the String and/or not as verbose.
I would prefer not to use any external crates.

3 Answers

In nightly version of Rust you can use experimental map_while on iterators to solve the problem quite fancy by mapping each digit character to an integer number (until we can't to do it, i.e. c.to_digit(10) returns None). Then we construct the final number using fold.

#![feature(iter_map_while)]

fn main() {
    let number = "12/1/1"
        .chars()
        .map_while(|c| c.to_digit(10))
        .fold(0, |acc, digit| acc * 10 + digit);
    println!("{}", number); // 12
}

Notice that there is no creation of a separate String, which is preferable, since creating many of them on the heap takes quite some time (relatively to just dealing with numbers on stack).

In stable Rust it's a bit more verbose:

fn main() {
    let number = "12/1/1"
        .chars()
        .map(|c| c.to_digit(10))
        .take_while(|opt| opt.is_some())
        .fold(0, |acc, digit| acc * 10 + digit.unwrap());
    println!("{}", number); // 12
}

It depends on what your definition of "better". If you simply want to remove the needless String creation, then you can use atoi crate.

Alternatively, you can also use the nom crate, and use digit0() to create your own atoi() function:

// nom = "6.0"
use nom::character::complete::digit0;
use nom::error::Error;
use std::str::FromStr;

fn atoi<F: FromStr>(input: &str) -> Result<F, <F as FromStr>::Err> {
    digit0::<_, Error<_>>(input).unwrap().1.parse::<F>()
}

fn main() {
    println!("{:?}", atoi::<usize>("12/1/1")); // Prints `Ok(12)`
    println!("{:?}", atoi::<usize>("33"));     // Prints `Ok(33)`
    println!("{:?}", atoi::<usize>("17abc"));  // Prints `Ok(17)`
    println!("{:?}", atoi::<usize>("abc"));    // Prints `Err(ParseIntError { kind: Empty })`
    println!("{:?}", atoi::<usize>(""));       // Prints `Err(ParseIntError { kind: Empty })`
}

The unwrap() is safe, as if there are no digits then Ok is still returned, and the empty string is handled by parse().


Since you don't want to use any external crates, then instead you can use find() to get the index of the first non-numeric character.

Note that, @AlexLarionov's answer panics on overflow, while this one does not.

use std::str::FromStr;

fn atoi<F: FromStr>(input: &str) -> Result<F, <F as FromStr>::Err> {
    let i = input.find(|c: char| !c.is_numeric()).unwrap_or_else(|| input.len());
    input[..i].parse::<F>()
}

fn main() {
    println!("{:?}", atoi::<usize>("12/1/1")); // Prints `Ok(12)`
    println!("{:?}", atoi::<usize>("33"));     // Prints `Ok(33)`
    println!("{:?}", atoi::<usize>("17abc"));  // Prints `Ok(17)`
    println!("{:?}", atoi::<usize>("abc"));    // Prints `Err(ParseIntError { kind: Empty })`
    println!("{:?}", atoi::<usize>(""));       // Prints `Err(ParseIntError { kind: Empty })`

    println!("{:?}", atoi::<usize>("123456789101112131415"));
    // Prints `Err(ParseIntError { kind: PosOverflow })`
}

This looks like a job for Regular Expressions (Regex) to me. All you want is to get the first group of numerics so you could use the "/d" (integers) descriptor or use "[0-9]" (the range of characters between 0 and 9 inclusive) and then "+" to show you want the character class to repeat 1 or more times.

I'm not a Rust programmer so I can't help you too much with the details but you can find Regex documentation for Rust here: https://docs.rs/regex/1.4.2/regex/

Related