Skip to content

← Closures and Iterators step 11 of 28

Easy Primitives

Split a message at its first blank line

Split a list of lines into a header and a body at the first blank line.

pub fn header_body(lines: Vec<String>) -> (Vec<String>, Vec<String>)

A line is blank when line.trim().is_empty() is true. This includes empty strings and strings containing only Rust's Unicode whitespace, such as spaces, tabs or non-breaking spaces. Use trimming only to decide where to split: preserve the original contents and order of every retained string.

Return all lines before the first blank in the header and all lines after it in the body. Discard exactly that one separator. Later blank lines belong to the body and must remain unchanged.

input:  ["Subject: hi", "From: me", "", "Hello", "", "World"]
header: ["Subject: hi", "From: me"]
body:   ["Hello", "", "World"]
Input Header Body
["a", "b"] ["a", "b"] []
["", "a"] [] ["a"]
["a", "", "", "b"] ["a"] ["", "b"]
[" title ", " ", " text "] [" title "] [" text "]
[] [] []

A trailing separator leaves an empty body. If there is no separator, every line belongs to the header.

Use take_while, skip_while and skip to build the two halves. Replace the starter's length-based skip and blank-line filter. Do not use index loops, length arithmetic or position() for this exercise.

Stop at a boundary or filter each item?

These adapters behave differently as their output is consumed:

Adapter Behavior
filter(predicate) Keeps each item whose predicate is true.
take_while(predicate) Yields a prefix, ending at the first false result.
skip_while(predicate) Skips a prefix, then yields the first failing item and everything after it.

The starter's filter removes every blank in the body. For ["a", "", "", "b"], it incorrectly returns ["b"] as the body. A predicate that tests only whether the current line is blank cannot distinguish the separator from later blanks. A stateful filter could track that distinction, but the required adapters express the boundary directly.

Drop only the separator

Start each chain with a fresh lines.iter():

  • For the header, take lines while they are not blank.
  • For the body, skip lines while they are not blank, then skip one more item.

skip_while pulls the first blank from the source and yields it. The following skip(1) discards that separator. After that, skip_while no longer tests its predicate, so later blanks pass through. Skipping an exhausted iterator is safe.

By comparison, take_while consumes its first failing item without yielding it. Reusing the same source iterator after that would already start after the separator. by_ref() alone does not change this behavior. The two fresh iterators here avoid sharing that consumption state.

See the iterator documentation for more on consuming the boundary item.

Build owned results

lines.iter() yields references to strings. Put .cloned() after selecting the wanted lines, then collect into Vec<String>. This copies retained strings for the owned result without cloning the skipped strings. The function owns the input, so another design could move strings; cloning is convenient for these two borrowed passes.

Passing this exercise requires compilation, the test cases and Clippy with warnings denied.