We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ground Rules: Values, Types, Control Flow step 23 of 24
String vs &str, part 2: functions that take and return text
Write a function that takes a list of words, uppercases the first character of each word, and joins the results into one string with a single space between consecutive words.
pub fn title_case(words: &[String]) -> String
The exact contract
- Each element of
wordsis one word. The function does not split, trim or otherwise inspect the text inside an element. - The first character of each word is replaced by its Unicode uppercase form,
exactly as
char::to_uppercaseproduces it. Some characters have an uppercase form that is more than one character long, for example'ß'becomes"SS". - Everything after the first character is kept unchanged, including any letters that are already uppercase.
- A character with no uppercase form, such as a digit, stays as it is.
- An empty word stays empty. It still takes its place in the join, so
["a", "", "b"]gives"A B"(two spaces). - The words are joined in their original order with exactly one space between
each pair. An empty list gives
"".
Examples:
| Input | Output |
|---|---|
["hello", "world"] |
"Hello World" |
[] |
"" |
["a"] |
"A" |
["rust", "IS", "fun"] |
"Rust IS Fun" |
["élan", "vital"] |
"Élan Vital" |
["ßeta"] |
"SSeta" |
["1st", "place"] |
"1st Place" |
Note that this is plain uppercasing of the first character, not the Unicode
"titlecase" mapping. The two differ for a few characters: titlecase would
turn 'ß' into "Ss", but this exercise expects "SS".
The starter does not compile. Fixing the compiler error is not enough on its
own: the most direct fix still panics on non-ASCII words such as "élan".
Its parameter type, &Vec<String>, also works but is less flexible than
&[String]. The sections below cover each of these.
Choosing parameter and return types
A common default for functions that work with text is:
Take
&str(a borrowed view of text). ReturnString(owned text) when you build new text. StoreStringin structs that must own their data.
A &str parameter accepts many kinds of caller. Someone with a String named
s passes &s, someone with a string literal passes it directly, and someone
with part of a string passes a slice such as &s[2..7]. For a list of strings,
the same idea gives &[String], a borrowed view of any contiguous sequence of
String values: a Vec<String>, an array, or part of either.
Returning String fits this function because it builds new text. The result
is not part of any input, so the function must hand back text the caller owns.
(A function that returns part of its input can return a &str borrowed from
that input instead.)
Clippy has a lint for the parameter half of this guideline. ptr_arg
warns about a parameter of type &Vec<T> and suggests &[T], and warns about
&String and suggests &str. It does not catch every case: it stays silent
when the parameter is used in a way it cannot prove a slice would support.
With Clippy 1.98.1, the starter's for word in words loop over the &Vec is
one such use: once the compile error is fixed, that loop does not trigger
ptr_arg, while for word in words.iter() does. (Clippy cannot lint the
starter as given, because it does not compile.) Change the parameter to &[String] anyway,
as the signature above shows. This does not break callers that pass &vec:
Rust converts a &Vec<String> to a &[String] automatically.
E0369: two &str values cannot be added
The starter tries this:
let capitalized = &word[0..1] + &word[1..];
Both operands are &str, and the compiler rejects it:
error[E0369]: cannot add `&str` to `&str`
|
| let capitalized = &word[0..1] + &word[1..];
| ----------- ^ ---------- &str
| | |
| | `+` cannot be used to concatenate two `&str` strings
| &str
|
= note: string concatenation requires an owned `String` on the left
The compiler also offers a suggestion to remove the first &. Following it
produces a different error (cannot add &str to str), because word[0..1] on
its own is a str, not an owned String.
The reason is how + is defined for strings. The standard library implements
it like this:
impl Add<&str> for String {
type Output = String;
fn add(self, other: &str) -> String
}
There is no implementation of + for &str + &str. The left operand must be
an owned String, and add takes it by value (self), so the addition
consumes it. The result reuses the left string's buffer and appends the right
side to it. That buffer may still need to grow, which can allocate, but no
separate new string is created for the result.
Because the left String is consumed, you cannot use it afterwards:
let a = String::from("foo");
let b = a + "bar"; // `a` is moved into the addition
// println!("{a}"); // error[E0382]: borrow of moved value: `a`
format! works differently. It only reads its arguments and always builds a
new String:
let a = String::from("foo");
let b = format!("{a}bar"); // `a` is only read and is still usable
println!("{a} {b}"); // prints "foo foobar"
So + moves its left operand and format! borrows its arguments. When you
join more than two pieces, format! is often easier to read. It is not useful
for turning a value that is already a String into a String:
format!("{}", s) with s: String makes Clippy's useless_format lint
suggest s.to_string() instead.
Uppercasing the first character safely
Do not take the first character with a byte range. &word[0..1] means "bytes
0 to 1", not "the first character". Rust strings are UTF-8, where a character
can take from one to four bytes. 'é' takes two, so &"élan"[0..1] cuts it in
half and the program panics at run time with a message like
end byte index 1 is not a char boundary. For an empty word, &word[0..1]
also panics because byte 1 is past the end. There are tests with "élan" and
"ßeta".
Instead, step through the characters:
let mut chars = word.chars();
match chars.next() {
Some(first) => { /* `first` is the first char */ }
None => { /* the word was empty */ }
}
chars.next() returns the first character and leaves the iterator positioned
just after it. After that call, chars.as_str() returns the rest of the word,
unchanged, as a &str. Together they split any word, including an empty one,
into "first character" and "the rest" without cutting a character in half.
char::to_uppercase returns an iterator of char values, not a single
char, because some characters uppercase to more than one character: 'ß'
becomes "SS". You can collect it into a String with .to_string() or
.collect::<String>(), and then append the rest of the word.
One more Clippy lint
needless_borrow warns when you write & in front of an expression that
is already a reference that the compiler could use directly. For example, if
word is a &String (as it is inside for word in words) and a function
takes a &str, passing &word creates a &&String that the compiler then
has to dereference again; passing word is enough. If it fires, remove the
extra &.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.