2

This solution seems rather inelegant:

fn parse_range(&self, string_value: &str) -> Vec<u8> {
    let values: Vec<u8> = string_value
        .splitn(2, "-")
        .map(|part| part.parse().ok().unwrap())
        .collect();

    { values[0]..(values[1] + 1) }.collect()
}
  • Since splitn(2, "-") returns exactly two results for any valid string_value, it would be better to assign the tuple directly to two variables first and last rather than a seemingly arbitrary-length Vec. I can't seem to do this with a tuple.
  • There are two instances of collect(), and I wonder if it can be reduced to one (or even zero).
l0b0
  • 55,365
  • 30
  • 138
  • 223

2 Answers2

3

Trivial implementation

fn parse_range(string_value: &str) -> Vec<u8> {
    let pos = string_value.find(|c| c == '-').expect("No valid string");
    let (first, second) = string_value.split_at(pos);

    let first: u8 = first.parse().expect("Not a number");
    let second: u8 = second[1..].parse().expect("Not a number");

    { first..second + 1 }.collect()
}

Playground

I would recommend returning a Result<Vec<u8>, Error> instead of panicking with expect/unwrap.

Nightly implementation

My next thought was about the second collect. Here is a code example which uses nightly code, but you won't need any collect at all.

#![feature(conservative_impl_trait, inclusive_range_syntax)]

fn parse_range(string_value: &str) -> impl Iterator<Item = u8> {
    let pos = string_value.find(|c| c == '-').expect("No valid string");
    let (first, second) = string_value.split_at(pos);

    let first: u8 = first.parse().expect("Not a number");
    let second: u8 = second[1..].parse().expect("Not a number");

    first..=second
}

fn main() {
    println!("{:?}", parse_range("3-7").collect::<Vec<u8>>());
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
hellow
  • 12,430
  • 7
  • 56
  • 79
  • 2
    Wouldn't `inclusive_range_syntax` fix the overflow in `"0-255"` that the first version has? – Sebastian Redl Mar 08 '18 at 09:01
  • 1
    You could also avoid the `collect` by returning a `std::ops::Range` and not need nightly. Unfortunately you still need nightly to return `RangeInclusive`, but maybe one of these days... – trent Mar 08 '18 at 19:36
  • In the nightly implementation I'd rather return the `RangeInclusive` from the function and leave turning it into a vector (or treating it as iterator) to the caller. – CodesInChaos Mar 09 '18 at 12:02
  • @CodesInChaos that's what I did. Well... I didn't returned `RangeInclusive`, but it's the callers decision to collect the result or do something else with it. – hellow Mar 09 '18 at 12:05
3

Instead of calling collect the first time, just advance the iterator:

let mut values = string_value
    .splitn(2, "-")
    .map(|part| part.parse().unwrap());

let start = values.next().unwrap();
let end = values.next().unwrap();

Do not call .ok().unwrap() — that converts the Result with useful error information to an Option, which has no information. Just call unwrap directly on the Result.

As already mentioned, if you want to return a Vec, you'll want to call collect to create it. If you want to return an iterator, you can. It's not bad even in stable Rust:

fn parse_range(string_value: &str) -> std::ops::Range<u8> {
    let mut values = string_value
        .splitn(2, "-")
        .map(|part| part.parse().unwrap());

    let start = values.next().unwrap();
    let end = values.next().unwrap();

    start..end + 1
}

fn main() {
    assert!(parse_range("1-5").eq(1..6));
}

Sadly, inclusive ranges are not yet stable, so you'll need to continue to use +1 or switch to nightly.


Since splitn(2, "-") returns exactly two results for any valid string_value, it would be better to assign the tuple directly to two variables first and last rather than a seemingly arbitrary-length Vec. I can't seem to do this with a tuple.

This is not possible with Rust's type system. You are asking for dependent types, a way for runtime values to interact with the type system. You'd want splitn to return a (&str, &str) for a value of 2 and a (&str, &str, &str) for a value of 3. That gets even more complicated when the argument is a variable, especially when it's set at run time.

The closest workaround would be to have a runtime check that there are no more values:

assert!(values.next().is_none());

Such a check doesn't feel valuable to me.


See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • I don't like the implicitness of using `next()` twice (it's still implicit that there must be exactly two values), but the `unwrap()` tip and returning an iterator are both great! – l0b0 Mar 08 '18 at 20:03
  • @l0b0 I'm not sure you are using the word "implicit" the way that I understand it. Calling `next` seems very *explicit*, and the fact that there are two variables is also very *explicit*. – Shepmaster Mar 08 '18 at 20:12
  • 1
    I mean that it's not explicit that we've consumed all the available entries in `values`. If I could say `(first, last)=` instead it would be a runtime error if the value was for example "5--6" or "5-6-". Currently I have to rely on less obvious logic: That `parse()` won't simply ignore leading or trailing dashes, and that I've given the right `n` to `splitn` - my original implementation used `n`=`1` because I intuited that `n` was the number of *splits* and not the number of returned items. If for whatever reason `n` is set to three there would be an undetected bug when handling invalid input. – l0b0 Mar 08 '18 at 20:38
  • 1
    *because I intuited that n was the number of splits and not the number of returned items* — that would be [because of me](https://github.com/rust-lang/rfcs/pull/979), you're welcome! – Shepmaster Mar 08 '18 at 20:42
  • 1
    @l0b0 I seem to recall there being some suggestion of `impl FromIterator for Result<(T, T, ...), NotEnoughItems>` which would permit something like that. However, I can't find it now. I think it was blocked waiting on const generics or some other future feature... – trent Mar 08 '18 at 20:45