After completing this exercise, you will be able to:
Vec<T>)rustc and cargoRust provides two ways to compile programs: the rustc compiler directly, or the cargo build system. Both are useful in different situations.
For simple, single-file programs, you can use rustc directly:
// Save as: hello.rs
fn main() {
println!("Hello, Rust!");
}
rustc hello.rs ./hello
Cargo is Rust's package manager and build system. It handles dependencies, builds, and more:
# Create a new project cargo new my_project cd my_project # Build the project cargo build # Build and run in one step cargo run # Build optimized release version cargo build --release
Cargo creates a project structure:
my_project/
βββ Cargo.toml # Project configuration and dependencies
βββ src/
βββ main.rs # Main source file
rustc for quick tests and cargo for larger projects or when you need external dependencies (crates).
Command-line arguments allow users to pass information to a program when it is executed. In Rust, command-line arguments are accessed using the std::env module.
Rust provides functions in std::env to access command-line arguments:
| Function/Method | Description |
|---|---|
std::env::args() |
Returns an iterator over command-line arguments as String |
args().len() |
Returns the number of arguments (including program name) |
args().nth(n) |
Returns the nth argument as Option<String> |
args().collect() |
Collects all arguments into a Vec<String> |
// cmd_args.rs
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("Number of arguments: {}", args.len());
println!("Program name: {}", args[0]);
for (i, arg) in args.iter().enumerate() {
println!("args[{}] = {}", i, arg);
}
}
Compile and run with arguments:
rustc cmd_args.rs ./cmd_args hello world 123
args[0] is the program name itself in Rust. User-provided arguments start at args[1]. The total count includes the program name.
Here's an example that expects a filename as a command-line argument:
// process_args.rs
use std::env;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
// Check if correct number of arguments provided
if args.len() < 2 {
eprintln!("Usage: {} <filename> [count]", args[0]);
process::exit(1);
}
println!("Processing file: {}", args[1]);
// If a second argument is provided, treat it as a number
if args.len() >= 3 {
match args[2].parse::<i32>() {
Ok(count) => println!("Count parameter: {}", count),
Err(_) => eprintln!("Warning: '{}' is not a valid number", args[2]),
}
}
}
eprintln! to print to standard error (stderr) and process::exit() to exit with a specific code. The parse() method returns a Result type, which we handle with match.
// args_match.rs
use std::env;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
// Use slice pattern matching
let (program, args) = match args.as_slice() {
[program, rest @ ..] => (program, rest),
_ => {
eprintln!("Error: No program name found");
process::exit(1);
}
};
if args.is_empty() {
eprintln!("Usage: {} <filename>", program);
process::exit(1);
}
let filename = &args[0];
println!("Filename: {}", filename);
// Optional second argument
if let Some(count_str) = args.get(1) {
if let Ok(count) = count_str.parse::<i32>() {
println!("Count: {}", count);
}
}
}
Rust provides robust file handling capabilities through the std::fs and std::io modules. Rust's approach emphasizes explicit error handling.
The simplest way to read a file is using fs::read_to_string:
// read_file_simple.rs
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]);
process::exit(1);
}
let filename = &args[1];
// Read entire file into a String
match fs::read_to_string(filename) {
Ok(contents) => {
println!("File contents:");
println!("{}", contents);
}
Err(error) => {
eprintln!("Error reading file '{}': {}", filename, error);
process::exit(1);
}
}
}
For large files or line-by-line processing, use BufReader:
// read_lines.rs
use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]);
process::exit(1);
}
let filename = &args[1];
// Open the file
let file = match File::open(filename) {
Ok(file) => file,
Err(error) => {
eprintln!("Error opening file '{}': {}", filename, error);
process::exit(1);
}
};
// Create a buffered reader
let reader = BufReader::new(file);
// Read line by line
println!("File contents:");
for (line_num, line_result) in reader.lines().enumerate() {
match line_result {
Ok(line) => println!("{}: {}", line_num + 1, line),
Err(error) => {
eprintln!("Error reading line: {}", error);
break;
}
}
}
}
| Function/Method | Description |
|---|---|
fs::read_to_string(path) |
Reads entire file into a String |
File::open(path) |
Opens a file for reading |
File::create(path) |
Creates a new file (or truncates existing) |
BufReader::new(file) |
Creates a buffered reader for efficient reading |
reader.lines() |
Returns an iterator over lines |
path.exists() |
Checks if a path exists (use std::path::Path) |
Rust's ? operator simplifies error handling in functions that return Result:
// read_file_question.rs
use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;
use std::process;
fn read_and_print_file(filename: &str) -> io::Result<()> {
// The ? operator returns early if there's an error
let file = File::open(filename)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?; // ? propagates errors
println!("{}", line);
}
Ok(())
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]);
process::exit(1);
}
let filename = &args[1];
// Check if file exists before opening
if !Path::new(filename).exists() {
eprintln!("Error: File '{}' not found.", filename);
process::exit(1);
}
if let Err(error) = read_and_print_file(filename) {
eprintln!("Error reading file: {}", error);
process::exit(1);
}
}
? operator can only be used in functions that return Result or Option. It automatically converts errors using the From trait. This is the idiomatic way to handle errors in Rust.
// check_exists.rs
use std::env;
use std::fs;
use std::path::Path;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]);
process::exit(1);
}
let filename = &args[1];
let path = Path::new(filename);
// Check various properties
if !path.exists() {
eprintln!("Error: '{}' does not exist.", filename);
process::exit(1);
}
if !path.is_file() {
eprintln!("Error: '{}' is not a file.", filename);
process::exit(1);
}
// Get file metadata
match fs::metadata(path) {
Ok(metadata) => {
println!("File: {}", filename);
println!("Size: {} bytes", metadata.len());
println!("Is read-only: {}", metadata.permissions().readonly());
}
Err(e) => eprintln!("Error getting metadata: {}", e),
}
}
Tokenization is the process of breaking a string into smaller pieces (tokens) based on delimiters. Rust provides powerful string methods for this purpose.
| Type | Description | Ownership |
|---|---|---|
String |
Growable, heap-allocated string | Owned |
&str |
String slice (view into a string) | Borrowed |
&String |
Reference to a String | Borrowed |
| Method | Description | Returns |
|---|---|---|
s.split(pat) |
Splits string by pattern | Iterator of &str |
s.split_whitespace() |
Splits by whitespace | Iterator of &str |
s.lines() |
Splits by line endings | Iterator of &str |
s.trim() |
Removes leading/trailing whitespace | &str |
s.find(pat) |
Finds first occurrence | Option<usize> |
s.contains(pat) |
Checks if pattern exists | bool |
s.replace(from, to) |
Replaces all occurrences | String |
s.to_lowercase() |
Converts to lowercase | String |
// tokenize_demo.rs
fn main() {
let sentence = "The quick brown fox jumps";
println!("Original: {}", sentence);
println!("\nTokens (split by space):");
// Split by single character
for (i, token) in sentence.split(' ').enumerate() {
println!(" Token {}: {}", i + 1, token);
}
// Split by whitespace (handles multiple spaces)
let sentence2 = " Multiple spaces here ";
println!("\nSplit by whitespace:");
for token in sentence2.split_whitespace() {
println!(" '{}'", token);
}
// Collect into a vector
let tokens: Vec<&str> = sentence.split(' ').collect();
println!("\nAs vector: {:?}", tokens);
println!("Number of tokens: {}", tokens.len());
}
// csv_parse.rs
fn main() {
let csv_line = "Alice,25,Computer Science,3.85";
println!("Parsing CSV: {}", csv_line);
println!();
// Split by comma and collect into vector
let fields: Vec<&str> = csv_line.split(',').collect();
if fields.len() >= 4 {
let name = fields[0];
let age = fields[1];
let major = fields[2];
let gpa = fields[3];
println!("Name: {}", name);
println!("Age: {}", age);
println!("Major: {}", major);
println!("GPA: {}", gpa);
}
// Alternative: using iterator methods
println!("\nUsing iterator with enumerate:");
for (i, field) in csv_line.split(',').enumerate() {
println!(" Field {}: {}", i, field.trim());
}
}
// tokenizer_function.rs
/// Splits a string by delimiter and returns owned Strings
fn tokenize(text: &str, delimiter: char) -> Vec<String> {
text.split(delimiter)
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
/// Splits a string by delimiter and returns the next token,
/// modifying the source string (similar to Pascal/Ada approach)
fn get_next_token(source: &mut String, delimiter: char) -> Option<String> {
let trimmed = source.trim_start();
*source = trimmed.to_string();
if source.is_empty() {
return None;
}
match source.find(delimiter) {
Some(pos) => {
let token = source[..pos].trim().to_string();
*source = source[pos + 1..].to_string();
Some(token)
}
None => {
let token = source.trim().to_string();
source.clear();
if token.is_empty() {
None
} else {
Some(token)
}
}
}
}
fn main() {
// Using tokenize function
let csv = "Alice, 25, Computer Science, 3.85";
let tokens = tokenize(csv, ',');
println!("Tokenized: {:?}", tokens);
// Using get_next_token
println!("\nExtracting tokens one by one:");
let mut data = String::from("Bob,30,Physics,3.92");
while let Some(token) = get_next_token(&mut data, ',') {
println!(" Token: {}", token);
}
}
// tokenize_file.rs
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]);
process::exit(1);
}
let filename = &args[1];
if !Path::new(filename).exists() {
eprintln!("Error: File '{}' not found.", filename);
process::exit(1);
}
let file = File::open(filename).expect("Failed to open file");
let reader = BufReader::new(file);
let mut token_count = 0;
for line_result in reader.lines() {
let line = line_result.expect("Failed to read line");
// Tokenize each line by whitespace
for token in line.split_whitespace() {
token_count += 1;
}
}
println!("Total tokens in file: {}", token_count);
}
split().map().filter().collect() very efficient.
Rust uses Vec<T> (vectors) for dynamic arrays. Vectors are heap-allocated and can grow or shrink at runtime.
// vector_basics.rs
fn main() {
// Create an empty vector
let mut numbers: Vec<i32> = Vec::new();
// Create with initial values using vec! macro
let mut grades = vec![85.0, 92.5, 78.0, 95.5];
// Create with repeated value
let zeros = vec![0; 5]; // [0, 0, 0, 0, 0]
// Create with capacity (no initial elements)
let mut with_capacity: Vec<String> = Vec::with_capacity(10);
println!("numbers: {:?}", numbers);
println!("grades: {:?}", grades);
println!("zeros: {:?}", zeros);
println!("with_capacity length: {}, capacity: {}",
with_capacity.len(), with_capacity.capacity());
}
| Operation | Description |
|---|---|
vec.push(value) |
Adds element to the end |
vec.pop() |
Removes and returns last element as Option |
vec.len() |
Returns number of elements |
vec.is_empty() |
Returns true if empty |
vec[index] |
Access by index (panics if out of bounds) |
vec.get(index) |
Safe access, returns Option<&T> |
vec.first() |
Returns Option<&T> for first element |
vec.last() |
Returns Option<&T> for last element |
vec.insert(i, v) |
Inserts at index, shifting elements |
vec.remove(i) |
Removes at index, shifting elements |
vec.clear() |
Removes all elements |
vec.iter() |
Returns an iterator over references |
// vector_operations.rs
fn main() {
let mut numbers: Vec<i32> = Vec::new();
// Add elements
numbers.push(10);
numbers.push(20);
numbers.push(30);
numbers.push(40);
numbers.push(50);
println!("After pushing: {:?}", numbers);
println!("Length: {}", numbers.len());
// Access elements
println!("First element: {}", numbers[0]);
println!("Element at index 2: {}", numbers[2]);
// Safe access with get()
match numbers.get(10) {
Some(value) => println!("Element at 10: {}", value),
None => println!("No element at index 10"),
}
// Remove last element
if let Some(last) = numbers.pop() {
println!("Popped: {}", last);
}
println!("After pop: {:?}", numbers);
// Iterate over elements
println!("\nIterating:");
for (i, num) in numbers.iter().enumerate() {
println!(" numbers[{}] = {}", i, num);
}
// Modify elements
for num in numbers.iter_mut() {
*num *= 2;
}
println!("\nAfter doubling: {:?}", numbers);
}
vec.get(index) instead of vec[index] when the index might be out of bounds. The get method returns Option instead of panicking.
// string_vector.rs
fn main() {
let mut words: Vec<String> = Vec::new();
// Push owned Strings
words.push(String::from("Hello"));
words.push(String::from("Dynamic"));
words.push(String::from("World"));
println!("Words: {:?}", words);
// Iterate and print
for (i, word) in words.iter().enumerate() {
println!(" [{}]: {}", i, word);
}
// Create from string literals
let days: Vec<&str> = vec!["Mon", "Tue", "Wed", "Thu", "Fri"];
println!("\nDays: {:?}", days);
// Convert Vec<&str> to Vec<String>
let owned_days: Vec<String> = days.iter()
.map(|s| s.to_string())
.collect();
println!("Owned days: {:?}", owned_days);
}
// tokens_to_vector.rs
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: {} <filename>", args[0]);
process::exit(1);
}
let filename = &args[1];
if !Path::new(filename).exists() {
eprintln!("Error: File not found.");
process::exit(1);
}
let file = File::open(filename).expect("Failed to open file");
let reader = BufReader::new(file);
let mut tokens: Vec<String> = Vec::new();
for line_result in reader.lines() {
let line = line_result.expect("Failed to read line");
// Tokenize and store
for token in line.split_whitespace() {
tokens.push(token.to_string());
}
}
// Display results
println!("=== File Token Analysis ===");
println!("Total tokens: {}", tokens.len());
if let Some(first) = tokens.first() {
println!("First token: {}", first);
}
if let Some(last) = tokens.last() {
println!("Last token: {}", last);
}
println!("\nTokens longer than 5 characters:");
for (i, token) in tokens.iter().enumerate() {
if token.len() > 5 {
println!(" [{}]: {} (length: {})", i, token, token.len());
}
}
}
Rust's most distinctive feature is its ownership system, which provides memory safety without garbage collection. Understanding ownership is essential for writing Rust programs.
// ownership_demo.rs
fn main() {
// s1 owns the String
let s1 = String::from("Hello");
// Ownership moves from s1 to s2
let s2 = s1;
// This would cause a compile error:
// println!("{}", s1); // ERROR: s1 no longer valid
println!("{}", s2); // OK: s2 owns the String
// When s2 goes out of scope here, memory is freed
}
// borrowing_demo.rs
fn print_length(s: &String) {
// s is a reference (borrow) - doesn't own the data
println!("Length: {}", s.len());
} // s goes out of scope, but doesn't drop the String (it's borrowed)
fn add_exclamation(s: &mut String) {
// Mutable borrow - can modify without taking ownership
s.push('!');
}
fn main() {
let mut message = String::from("Hello");
// Immutable borrow
print_length(&message);
// Mutable borrow
add_exclamation(&mut message);
// message is still valid and accessible
println!("Message: {}", message);
}
| Feature | Rust | C/C++ | Java/Python (GC) |
|---|---|---|---|
| Memory management | Ownership system | Manual | Garbage collection |
| When memory freed | Immediately at scope end | When you call free/delete | When GC runs |
| Memory leaks | Prevented by compiler | Possible (if forget free) | Rare |
| Dangling pointers | Prevented by compiler | Possible | Prevented by GC |
| Data races | Prevented by compiler | Possible | Possible |
| Runtime overhead | Zero-cost abstractions | None | GC pauses |
| Null pointers | No null (use Option) | Possible | Possible |
// auto_memory_demo.rs
fn create_data() {
// Vector is allocated on the heap
let mut data: Vec<String> = Vec::new();
for i in 0..1000 {
data.push(format!("Item {}", i));
}
println!("Created {} items inside function", data.len());
// When this function returns, `data` goes out of scope
// Rust automatically:
// 1. Drops each String in the vector (freeing their heap memory)
// 2. Drops the vector itself (freeing its heap memory)
// No explicit cleanup needed!
}
fn main() {
println!("=== Automatic Memory Management Demo ===");
for i in 1..=5 {
println!("\nIteration {}", i);
create_data();
// Memory from previous call has been freed
}
println!("\nProgram completed - all memory cleaned up automatically");
}
// drop_demo.rs
struct Resource {
name: String,
}
impl Resource {
fn new(name: &str) -> Resource {
println!("Creating resource: {}", name);
Resource {
name: String::from(name),
}
}
}
impl Drop for Resource {
fn drop(&mut self) {
println!("Dropping resource: {}", self.name);
}
}
fn main() {
println!("=== Drop Trait Demo ===\n");
{
let r1 = Resource::new("First");
let r2 = Resource::new("Second");
println!("\nInside inner scope...\n");
} // r2 dropped first, then r1 (reverse order)
println!("\nBack in main scope\n");
let r3 = Resource::new("Third");
println!("\nAbout to exit main...\n");
} // r3 dropped here
// best_practices.rs
use std::fs::File;
use std::io::{self, BufRead, BufReader};
// 1. Use references when you don't need ownership
fn process_data(data: &[String]) {
for item in data {
println!("Processing: {}", item);
}
}
// 2. Return owned data when creating new values
fn create_list() -> Vec<String> {
vec![
String::from("One"),
String::from("Two"),
String::from("Three"),
]
}
// 3. Use Result for fallible operations
fn read_lines(filename: &str) -> io::Result<Vec<String>> {
let file = File::open(filename)?;
let reader = BufReader::new(file);
let mut lines = Vec::new();
for line_result in reader.lines() {
lines.push(line_result?);
}
Ok(lines)
}
fn main() {
// Create data - we own it
let my_list = create_list();
// Borrow data - function doesn't take ownership
process_data(&my_list);
// We can still use my_list
println!("\nList still accessible: {:?}", my_list);
// my_list dropped automatically when main ends
}
This comprehensive example combines all the concepts: command-line arguments, file reading, tokenization, vectors, and demonstrates Rust's memory management:
// word_frequency_analyzer.rs
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process;
/// Removes punctuation from the beginning and end of a word
fn strip_punctuation(word: &str) -> String {
let punctuation = ".,!?;:\"'()-";
let trimmed: String = word
.trim_start_matches(|c| punctuation.contains(c))
.trim_end_matches(|c| punctuation.contains(c))
.to_string();
trimmed
}
fn main() {
// Check command-line arguments
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Word Frequency Analyzer");
eprintln!("Usage: {} <filename>", args[0]);
process::exit(1);
}
let filename = &args[1];
// Check if file exists
if !Path::new(filename).exists() {
eprintln!("Error: File '{}' not found.", filename);
process::exit(1);
}
// Open and read the file
let file = match File::open(filename) {
Ok(f) => f,
Err(e) => {
eprintln!("Error opening file: {}", e);
process::exit(1);
}
};
let reader = BufReader::new(file);
// Store all tokens and count frequencies
let mut all_tokens: Vec<String> = Vec::new();
let mut word_counts: HashMap<String, u32> = HashMap::new();
for line_result in reader.lines() {
let line = match line_result {
Ok(l) => l,
Err(e) => {
eprintln!("Error reading line: {}", e);
continue;
}
};
// Tokenize and process each word
for token in line.split_whitespace() {
let word = strip_punctuation(token).to_lowercase();
if !word.is_empty() {
all_tokens.push(word.clone());
// Update word count
*word_counts.entry(word).or_insert(0) += 1;
}
}
}
// Display results
println!("=== Word Frequency Analysis ===");
println!("File: {}", filename);
println!("Total tokens: {}", all_tokens.len());
println!("Unique words: {}", word_counts.len());
println!();
// Find words appearing more than once
println!("Words appearing more than once:");
println!("{:-<40}", "");
let mut repeated_words: Vec<(&String, &u32)> = word_counts
.iter()
.filter(|(_, &count)| count > 1)
.collect();
// Sort by count (descending)
repeated_words.sort_by(|a, b| b.1.cmp(a.1));
for (word, count) in &repeated_words {
println!(" {:<20}: {}", word, count);
}
// Find most common word
if let Some((word, count)) = word_counts.iter().max_by_key(|(_, &v)| v) {
println!();
println!("Most common word: '{}' ({} occurrences)", word, count);
}
// Memory note: All data structures (all_tokens, word_counts, etc.)
// will be automatically freed when main() ends.
// Rust's ownership system ensures no memory leaks.
}
# With rustc rustc word_frequency_analyzer.rs ./word_frequency_analyzer sample.txt # With cargo (create project first) cargo new word_analyzer # Copy code to src/main.rs cargo run -- sample.txt
| Task | Rust Syntax |
|---|---|
| Get all arguments | let args: Vec<String> = env::args().collect(); |
| Get argument count | args.len() |
| Exit with error code | process::exit(1); |
| Print to stderr | eprintln!("Error: {}", msg); |
| Read file to string | fs::read_to_string(filename)? |
| Open file | File::open(filename)? |
| Create buffered reader | BufReader::new(file) |
| Read lines iterator | reader.lines() |
| Check file exists | Path::new(filename).exists() |
| Split string | s.split(',') or s.split_whitespace() |
| Trim whitespace | s.trim() |
| Parse string to number | s.parse::<i32>()? |
| Create empty vector | let v: Vec<T> = Vec::new(); |
| Create vector with values | let v = vec![1, 2, 3]; |
| Add to vector | v.push(value); |
| Get vector length | v.len() |
| Safe element access | v.get(index) (returns Option) |
| Iterate with index | for (i, item) in v.iter().enumerate() |
| Borrow (immutable) | &value |
| Borrow (mutable) | &mut value |
| Convert &str to String | s.to_string() or String::from(s) |
Write a Rust program that processes a CSV (Comma-Separated Values) file containing student grade data. Your program should:
StudentName,Score1,Score2,Score3Example:
Alice,85,92,78
String for student namesf64 for each student's average scoreAlice,85,92,78 Bob,90,88,95 Charlie,72,68,75 Diana,95,98,92
line.split(',') to parse each CSV lines.parse::<f64>() to convert score strings to numbers.unwrap_or(0.0) or proper error handling for parsingiter().enumerate() to loop with indicesiter().max_by() and iter().min_by() for finding extremesSave your program as grade_analyzer.rs and compile with:
rustc grade_analyzer.rs ./grade_analyzer grades.csv # Or with cargo: cargo new grade_analyzer # Edit src/main.rs cargo run -- grades.csv
Use the following template to get started. Fill in each blank marked with ______ with the appropriate code. Read the comments carefully for hints!
// grade_analyzer.rs
// CSV Grade Analyzer - Student Starter Code
// =========================================
// Complete the sections marked with TODO to finish this program.
//
// This program will:
// 1. Accept a CSV filename as a command-line argument
// 2. Read and parse the CSV file (Name,Score1,Score2,Score3)
// 3. Store student names and calculated averages in vectors
// 4. Display individual averages and class statistics
//
// Author: [Your Name]
// Date: [Date]
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process;
fn main() {
// ===================================================================
// PART 1: Command-Line Argument Handling
// ===================================================================
// TODO: Collect command-line arguments into a vector
// HINT: Use env::args().collect()
let args: Vec<String> = env::______().______();
// TODO: Check if a filename was provided (args[0] is the program name)
// HINT: Check if args.len() is less than 2
if args.______() < 2 {
eprintln!("=== Student Grade Analyzer ===");
// TODO: Print usage message with program name
// HINT: args[0] contains the program name
eprintln!("Usage: {} <filename>", ______[0]);
process::exit(1);
}
// TODO: Get the filename from args[1]
// HINT: Use &args[1] to borrow the string
let filename = &______[1];
// TODO: Check if file exists
// HINT: Use Path::new(filename).exists()
if !______(filename).______() {
eprintln!("Error: File '{}' not found.", filename);
process::exit(1);
}
// ===================================================================
// PART 2: Open File and Prepare for Reading
// ===================================================================
// TODO: Open the file
// HINT: Use File::open(filename) with match or expect
let file = match ______::______(filename) {
Ok(f) => f,
Err(e) => {
eprintln!("Error opening file: {}", e);
process::exit(1);
}
};
// TODO: Create a buffered reader
// HINT: Use BufReader::new(file)
let reader = ______::______(file);
// ===================================================================
// PART 3: Declare Vectors for Storing Data
// ===================================================================
// TODO: Create an empty vector for student names (String type)
// HINT: Use Vec::new() or vec![]
let mut names: Vec<String> = ______::______();
// TODO: Create an empty vector for student averages (f64 type)
let mut averages: Vec<f64> = ______::______();
// ===================================================================
// PART 4: Read and Parse the CSV File
// ===================================================================
// TODO: Iterate over lines in the file
// HINT: Use reader.lines()
for line_result in reader.______() {
// TODO: Handle the Result from reading the line
// HINT: Use match or if let Ok(line) = line_result
let line = match line_result {
Ok(l) => l,
Err(_) => continue,
};
// Skip empty lines
if line.trim().is_empty() {
continue;
}
// TODO: Split the line by comma and collect into a vector
// HINT: Use line.split(',').collect()
let fields: Vec<&str> = line.______(',').______();
// Check that we have enough fields
if fields.len() < 4 {
continue;
}
// TODO: Extract the student name (first field)
// HINT: fields[0] is a &str, use .to_string() to convert to String
let name = fields[0].trim().______();
// TODO: Parse the three scores
// HINT: Use .parse::<f64>().unwrap_or(0.0)
let score1: f64 = fields[1].trim().______::<f64>().unwrap_or(0.0);
let score2: f64 = fields[2].trim().______::<______>().unwrap_or(0.0);
let score3: f64 = fields[3].trim().______::<______>().______;
// TODO: Calculate the student's average
// HINT: (score1 + score2 + score3) / 3.0
let student_avg = (______ + ______ + ______) / 3.0;
// TODO: Add name and average to their respective vectors
// HINT: Use .push()
names.______(name);
averages.______(student_avg);
}
// ===================================================================
// PART 5: Calculate Statistics
// ===================================================================
// TODO: Calculate class total and find highest/lowest
let mut class_total: f64 = 0.0;
let mut highest_avg: f64 = 0.0;
let mut lowest_avg: f64 = f64::MAX;
let mut highest_name = String::new();
let mut lowest_name = String::new();
// TODO: Loop through all students using enumerate
// HINT: Use for (i, avg) in averages.iter().enumerate()
for (i, avg) in averages.______().______() {
class_total += avg;
// TODO: Check if this is the highest average
if *avg > ______ {
highest_avg = *avg;
// TODO: Get the corresponding name
// HINT: Use names[i].clone()
highest_name = names[i].______();
}
// TODO: Check if this is the lowest average
if *avg < ______ {
lowest_avg = *avg;
lowest_name = names[i].______();
}
}
// TODO: Calculate class average
// HINT: class_total / names.len() as f64
let class_avg = class_total / ______.______() as f64;
// ===================================================================
// PART 6: Display Results
// ===================================================================
println!("=== Student Grade Analyzer ===");
println!("File: {}", filename);
// TODO: Display number of students
// HINT: Use names.len()
println!("Students loaded: {}", names.______());
println!();
println!("Student Averages:");
// TODO: Loop through and display each student
for (i, name) in names.iter().enumerate() {
// TODO: Get the average for this student
let avg = averages[i];
println!(" {:<15}: {:.2}", name, avg);
}
println!();
println!("Class Statistics:");
println!(" Class Average : {:.2}", class_avg);
println!(" Highest Score : {} ({:.2})", highest_name, highest_avg);
println!(" Lowest Score : {} ({:.2})", lowest_name, lowest_avg);
// ===================================================================
// MEMORY MANAGEMENT EXPLANATION
// ===================================================================
// TODO: Answer these questions in comments:
//
// 1. Why don't we need to manually free the `names` and `averages` vectors?
// YOUR ANSWER: ________________________________________________
//
// 2. When exactly is the memory for these vectors released?
// YOUR ANSWER: ________________________________________________
//
// 3. What happens to the `file` and `reader` variables?
// YOUR ANSWER: ________________________________________________
//
// 4. How does Rust's approach differ from garbage-collected languages?
// YOUR ANSWER: ________________________________________________
}
Use this table to check your work:
| Section | Blank Location | Answer |
|---|---|---|
| Part 1 | env::______() |
args |
| Part 1 | .______() (collect) |
collect |
| Part 1 | args.______() (length) |
len |
| Part 1 | ______[0] |
args |
| Part 1 | &______[1] |
args |
| Part 1 | ______(filename) |
Path::new |
| Part 1 | .______() (exists) |
exists |
| Part 2 | ______::______(filename) |
File::open |
| Part 2 | ______::______(file) |
BufReader::new |
| Part 3 | ______::______() |
Vec::new |
| Part 4 | reader.______() |
lines |
| Part 4 | line.______(',').______() |
split, collect |
| Part 4 | .______() (to owned string) |
to_string |
| Part 4 | .______::<f64>() |
parse |
| Part 4 | Average calculation | score1, score2, score3 |
| Part 4 | .______() (add to vector) |
push |
| Part 5 | .______().______() |
iter, enumerate |
| Part 5 | > ______ |
highest_avg |
| Part 5 | .______() (clone string) |
clone |
| Part 5 | < ______ |
lowest_avg |
| Part 5 | ______.______() as f64 |
names.len |
| Part 6 | names.______() |
len |
Create a file named grades.csv:
Alice,85,92,78 Bob,90,88,95 Charlie,72,68,75 Diana,95,98,92
| Test | Command | Expected Result |
|---|---|---|
| No arguments | ./grade_analyzer |
Usage message displayed |
| Missing file | ./grade_analyzer nofile.csv |
Error message about file not found |
| Valid file | ./grade_analyzer grades.csv |
All statistics displayed correctly |
| Student | Average |
|---|---|
| Alice | 85.00 |
| Bob | 91.00 |
| Charlie | 71.67 |
| Diana | 95.00 |
| Class Average | 85.67 |
& to borrow when you don't need ownership, .clone() when you doparse::<f64>()*avg to get the value.to_string() to convert &str to String.unwrap_or(default) or proper match/if letmut for variables you'll modifyas f64 to convert integer types