James Mclaughlin
James Mclaughlin

Reputation: 702

How to check if for loop is on the last element of an iterator?

I'm trying to find a way of checking if I am on the last element of an iterator in a for loop without using .clone(); currently I'm doing this:

let sentence = "The quick brown fox.";
let words = sentence.split(' ');
let last = words.clone().last().unwrap();
 
for word in words {
    if word == last {
        print!("{}", word);
    }
} 

I've also tried using .collect() on the iterator, but this requires that I use .iter().enumerate() to check for the last index, which seems unnecessarily complicated to me:

let sentence = "The quick brown fox.";
let words: Vec<&str> = sentence.split(' ').collect();
 
for (i, word) in words.iter().enumerate() {
    if i == words.len() - 1 {
        print!("{}", word);
    }
}

Is there a way to do this in a more succinct way, perhaps just using the original iterator?

Upvotes: 22

Views: 15637

Answers (5)

Chayim Friedman
Chayim Friedman

Reputation: 70970

This may not be as performant as the other answers, but this is cleaner - you can use itertools's with_position():

use itertools::Itertools;

let sentence = "The quick brown fox.";
let words = sentence.split(' ');

for (pos, word) in words.with_position() {
    if let itertools::Position::Last | itertools::Position::Only = pos {
        print!("{}", word);
    }
}

You can also check for other conditions, such as first, middle etc..

Upvotes: 8

ctrl-alt-delor
ctrl-alt-delor

Reputation: 7735

I would like to add to @Masklinn 's answer.

I have used a trait to make it a little more readable.

The code allows it.peek().is_none() to be written as it.is_last().

trait IterEndPeek {
    fn is_last(&mut self) -> bool;
}

impl<I: Iterator> IterEndPeek for  std::iter::Peekable<I> {
    fn is_last(&mut self) -> bool {
        self.peek().is_none()
    }
}

fn main() {
    let sentence = "The quick brown fox.";
    let mut it = sentence.split(' ').peekable();
    while let Some(word) = it.next()  {
        if it.is_last() {
            println!("last {}", word);
        } else {
            println!("- {}", word);
        }
    }
}

Upvotes: 3

Denys S&#233;guret
Denys S&#233;guret

Reputation: 382150

For the general case of finite iterators, if you want to get the last item, you can, as suggests @Masklinn (see their answer), convert to a Peekable which will buffer so it always knows the next element.

In your precise case, when you just want to print the last word and don't care about the other ones, there is a much cheaper solution because splitting on a character implements DoubleEndedIterator.

So it's easy to get the last word, you don't have to collect the whole split nor to enumerate. It's also fast as the string will be searched from the end and nothing will be copied.

So you can do

let last_word = sentence
    .split(' ').rev().next().unwrap(); // SAFETY: there's always at least one word

Upvotes: 8

Masklinn
Masklinn

Reputation: 42282

Convert your iterator to a Peekable.

This will require desugaring the iteration to a while let, but if during the iteration peek() returns None you're on the last iteration:

let mut it = sentence.split(' ').peekable();
while let Some(word) = it.next()  {
    if it.peek().is_none() {
        println!("{}", word);
    }
}

Playground.

Upvotes: 43

jfMR
jfMR

Reputation: 24738

You could take advantage of the Iterator::map() iterator adaptor for achieving a more convenient usage:

let sentence = "The quick brown fox.";
let words: Vec<&str> = sentence.split(' ').collect();

for (word, is_last_element) in words.iter().enumerate()
  .map(|(i, w)| (w, i == words.len() - 1))
{
   if is_last_element {
      println!("{}", word);
   }
}

This way, instead of having to deal with the index in the body of the loop, you just focus on whether or not the given element in a particular iteration is the last one.

Upvotes: 4

Related Questions