String::pop for str

String::pop removes the last character of a String. Is there a similar method that works on str slices? I'm trying to do something like this:

let rust = "Rust";
let popped = rust.pop();
assert_eq!(popped, "Rus");

How about &rust[..rust.len() - 1]?

Something like this should work:

let rust = "Rust";
let popped = {
    let mut chars = rust.chars();
    chars.next_back();
    chars.as_str()
};
assert_eq!(popped, "Rus");
6 Likes

That won't work if the last codepoint is outside ascii

I would implement my own extension trait.

trait StrExt {
  fn pop(&mut self) -> Option<char>;
}

impl<'a> StrExt for &'a str {
  fn pop(&mut self) -> Option<char> {
    let mut chars = self.chars();
    let letter = chars.next_back()?;
    *self = chars.as_str();
    Some(letter)
  }
}


fn main() {
    let mut word = "Rust";
    
    let t = word.pop().unwrap();
    
    println!("{:?} {}", word, t);  // "Rus" t
}

(playground)

This has the benefit of giving you method syntax, and handling empty or non-ascii strings.

4 Likes

Thanks both of you, today I learned about DoubleEndedIterator.

2 Likes

Note that this impl could use lifetime elision, since the lifetime isn’t used/written anywhere except for the Self type.

impl StrExt for &str {
  fn pop(&mut self) -> Option<char> {
    let mut chars = self.chars();
    let letter = chars.next_back()?;
    *self = chars.as_str();
    Some(letter)
  }
}
2 Likes

This combines poorly with non-ASCII text. It'll lead to broken text since graphemes can consist of multiple bytes.

Actually, graphemes (which are arguably way closer to a true notion of “character” than the type char) can even consist of multiple chars (i.e. unicode scalar values). Multi-byte characters consisting of a single scalar value will not lead to broken text, but &rust[..rust.len() - 1] would simply panic. I’m not certian if there’s even any grapheme consisting of more than one scalar value (i.e. char) whose final character is only a single byte long; if there isn’t, then broken text would never happen with the above slicing code, just panics. For example, combining diacritical marks come after the character they modify, and those combining marks themself only start beyond the one-byte-UTF8-encodable (i.e. ASCII) range.

On the other hand, the “more proper” solution of removing a whole char can lead to “broken” text where just part of a “character”, e.g. a combining diacritical mark, or part of a composed emoji was removed. For popping a whole grapheme, one could use the .graphemes() method from the unicode-segmentation crate in place of .chars().

use unicode_segmentation::UnicodeSegmentation;

trait StrExt: Sized {
  fn pop_grapheme(&mut self) -> Option<Self>;
}

impl StrExt for &str {
  fn pop_grapheme(&mut self) -> Option<Self> {
    let mut graphemes = self.graphemes(true);
    let letter = graphemes.next_back()?;
    *self = graphemes.as_str();
    Some(letter)
  }
}


fn main() {
    let mut word = "Rust";
    
    let t = word.pop_grapheme().unwrap();
    
    println!("{:?} {}", word, t);  // "Rus" t
}
5 Likes

This would indeed be my suggested solution.

1 Like