Collections and String Handling Practice
Practice vectors, hash maps, strings, and parsing by building one transaction log analyzer in Rust.
This practice page should not split your attention across multiple projects.
It should give you one realistic program that forces the collections lesson to become usable.
That is what this page does.
The project is small on purpose.
If you can store records, parse input, group totals, and answer simple queries without getting lost, then this section is doing its job.
The Project
Build a transaction log analyzer.
The program will read a small list of text commands like these:
send alice bob 500
send bob carol 200
send alice dave 300
send carol alice 100Then it will:
- parse each line
- store every valid transfer in a
Vec<Transfer> - calculate per-wallet totals with
HashMap<String, u64> - print a readable summary
This is the right project for this lesson because it uses the exact skills you just learned:
Vec<T>for ordered historyHashMap<K, V>for lookup and aggregationStringfor owned stored data&strfor borrowed parsing inputsplit_whitespace,parse, andtrimfor text processing
What You Are Proving
By the end, you should be able to explain:
- why the transfer history belongs in a vector
- why per-wallet totals belong in a hash map
- why parsed input starts as
&strand later becomesString - where ownership is required and where borrowing is enough
If you cannot explain those four things, the code is not the real win yet.
Step 1: Define The Core Type
Start with one struct.
#[derive(Debug, Clone)]
struct Transfer {
from: String,
to: String,
amount: u64,
}This struct is intentionally small.
You do not need timestamps, memo fields, signatures, token metadata, or network state.
Those would only distract from the collections lesson.
Step 2: Parse One Line Correctly
Write one function that turns a command line into a Transfer.
fn parse_transfer(line: &str) -> Result<Transfer, String> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() != 4 {
return Err(String::from("expected format: send <from> <to> <amount>"));
}
if parts[0] != "send" {
return Err(String::from("command must start with 'send'"));
}
let amount = parts[3]
.parse::<u64>()
.map_err(|_| String::from("amount must be a positive integer"))?;
Ok(Transfer {
from: parts[1].to_string(),
to: parts[2].to_string(),
amount,
})
}What this step teaches
lineis borrowed as&strbecause parsing does not require ownership of the whole inputpartsbecomesVec<&str>becausesplit_whitespaceborrows slices from the original linefromandtobecomeStringbecause theTransferstruct needs to own them after parsing finishes
That ownership shift matters.
It is the main string-handling idea in this practice.
Step 3: Store Every Parsed Transfer In A Vector
Now use a vector as the ordered history.
fn main() {
let input = vec![
"send alice bob 500",
"send bob carol 200",
"send alice dave 300",
"send carol alice 100",
];
let mut transfers: Vec<Transfer> = Vec::new();
for line in input {
match parse_transfer(line) {
Ok(transfer) => transfers.push(transfer),
Err(error) => println!("skipping line: {} | {}", line, error),
}
}
println!("parsed transfers: {:?}", transfers);
}A vector is the right tool here because:
- order matters
- you want to keep every record
- you want to iterate through the whole history later
Step 4: Build A Hash Map For Totals Sent
Now aggregate the transfers by sender.
use std::collections::HashMap;
fn totals_sent(transfers: &[Transfer]) -> HashMap<String, u64> {
let mut totals = HashMap::new();
for transfer in transfers {
*totals.entry(transfer.from.clone()).or_insert(0) += transfer.amount;
}
totals
}Why the hash map belongs here
A vector is good for the raw history.
A hash map is better for questions like:
- how much did Alice send in total
- how much did Bob send in total
That is keyed lookup.
That is what HashMap is for.
Step 5: Build A Hash Map For Totals Received
Do the same thing for recipients.
use std::collections::HashMap;
fn totals_received(transfers: &[Transfer]) -> HashMap<String, u64> {
let mut totals = HashMap::new();
for transfer in transfers {
*totals.entry(transfer.to.clone()).or_insert(0) += transfer.amount;
}
totals
}This is a good checkpoint.
You now have:
- one vector for the full history
- one hash map for sent totals
- one hash map for received totals
That is exactly the kind of separation a real program needs.
Step 6: Print A Useful Summary
Now make the program produce something readable.
fn main() {
let input = vec![
"send alice bob 500",
"send bob carol 200",
"send alice dave 300",
"send carol alice 100",
];
let mut transfers: Vec<Transfer> = Vec::new();
for line in input {
match parse_transfer(line) {
Ok(transfer) => transfers.push(transfer),
Err(error) => println!("skipping line: {} | {}", line, error),
}
}
let sent = totals_sent(&transfers);
let received = totals_received(&transfers);
println!("transfers parsed: {}", transfers.len());
println!("totals sent: {:?}", sent);
println!("totals received: {:?}", received);
}You can improve the formatting later.
At this stage, correctness matters more than pretty output.
Step 7: Add One Real Query Function
Make one helper that answers a practical question.
fn transfers_for_wallet<'a>(transfers: &'a [Transfer], wallet: &str) -> Vec<&'a Transfer> {
transfers
.iter()
.filter(|transfer| transfer.from == wallet || transfer.to == wallet)
.collect()
}This proves another useful distinction:
- the history vector owns
Transfervalues - the query result can return
&Transferreferences because it only borrows from the history
That is a good ownership decision.
You do not need to clone the whole history just to answer one query.
Step 8: Handle Bad Input On Purpose
A practice project is weak if every line is valid.
Add bad input and make sure your parser behaves correctly.
let input = vec![
"send alice bob 500",
"send bob carol two_hundred",
"deposit alice 400",
"send carol alice 100",
];What should happen:
- valid
sendcommands becomeTransferrecords - invalid lines do not panic the program
- invalid lines report a clear reason and get skipped
That is a much stronger exercise than just parsing the happy path.
Suggested Build Order
Use this order and do not rush it:
- write
Transfer - write
parse_transfer - parse a few valid lines into
Vec<Transfer> - add one invalid line and verify the error path
- write
totals_sent - write
totals_received - write
transfers_for_wallet - print one full summary
That order keeps the difficulty under control.
Common Mistakes
Mistake 1: using a hash map for the raw history
If you store the transaction log itself in a hash map, you lose the simple ordered record list.
The history is a vector.
The summary lookups are hash maps.
Mistake 2: converting everything to String too early
When parsing, borrowed &str pieces are fine for temporary work.
Only convert to String when the final stored struct needs ownership.
Mistake 3: cloning more than necessary
If a function only needs to read transfers, borrow &[Transfer].
Do not clone the whole vector just because borrowing feels harder.
Mistake 4: mixing parsing logic with summary logic too early
First parse the input.
Then aggregate.
Then print results.
If all three happen in one tangled block, the lesson gets harder than it needs to be.
What A Good Finished Version Looks Like
By the end, your program should:
- parse a list of transfer commands
- skip invalid lines safely
- store valid transfers in order
- compute total sent per wallet
- compute total received per wallet
- answer at least one query for a specific wallet
That is enough.
If you can do that cleanly, you understand the point of this lesson.
Optional Extension
Only add this after the main version is clean.
Add a function that builds a net balance change per wallet:
- sent amount subtracts from the wallet
- received amount adds to the wallet
That is a good extension because it still uses the same core tools:
- vectors
- hash maps
- string parsing
- careful ownership
What Comes Next
After Rust Foundations, the next layers of the curriculum start asking you to use these same ideas in more realistic Solana code.
That only works if collections and strings now feel normal instead of magical.