//! sample-main.rs: a small, complete Rust program with numbered comments. //! Free sample from 99helpers.com/file-types/rs //! //! It reads a short list of shared expenses, skips the lines it can't //! parse, and prints a total per person. //! //! Run it: rustc --edition 2024 sample-main.rs //! ./sample-main (sample-main.exe on Windows) //! Test it: rustc --edition 2024 --test sample-main.rs -o sample-tests //! ./sample-tests // 1. `use` brings names from the standard library into scope. use std::collections::BTreeMap; use std::fmt; // 2. A struct groups named fields. `derive` asks the compiler to write // common trait implementations for it: debug printing and comparison. #[derive(Debug, PartialEq)] struct Expense { who: String, cents: u64, category: Category, } // 3. An enum lists every value a type can have. #[derive(Debug, Clone, Copy, PartialEq)] enum Category { Food, Travel, Other, } // 4. An `impl` block adds functions to a type. `match` must cover every // case: `_` catches all the labels not listed above it. impl Category { fn from_label(label: &str) -> Category { match label { "food" => Category::Food, "travel" => Category::Travel, _ => Category::Other, } } } // 5. Errors are ordinary values. Implementing the `Display` trait // decides how an error prints for a person. #[derive(Debug, PartialEq)] enum ParseError { MissingField(usize), BadAmount { line: usize, text: String }, } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { ParseError::MissingField(line) => write!(f, "line {line}: expected 3 fields"), ParseError::BadAmount { line, text } => { write!(f, "line {line}: {text:?} is not an amount") } } } } // 6. Functions that can fail return `Result`: `Ok(value)` or // `Err(error)`. Rust has no exceptions. `?` hands an error straight // back to the caller. fn parse_line(number: usize, line: &str) -> Result { let mut fields = line.split(',').map(str::trim); let (Some(who), Some(amount), Some(label)) = (fields.next(), fields.next(), fields.next()) else { return Err(ParseError::MissingField(number)); }; let cents = parse_cents(amount).ok_or_else(|| ParseError::BadAmount { line: number, text: amount.to_string(), })?; Ok(Expense { who: who.to_string(), cents, category: Category::from_label(label), }) } // 7. `Option` is a value that may be missing: `Some(value)` or `None`. // Safe Rust has no null. Variables are immutable unless marked `mut`. fn parse_cents(text: &str) -> Option { let (whole, fraction) = text.split_once('.').unwrap_or((text, "0")); if fraction.len() > 2 { return None; } let whole: u64 = whole.parse().ok()?; let fraction: u64 = format!("{fraction:0<2}").parse().ok()?; whole.checked_mul(100)?.checked_add(fraction) } // 8. Borrowing: `&[Expense]` lends the list to the function without // copying it. The `&str` keys in the map borrow from the same list, // and the compiler checks that the list outlives them. fn totals_by_person(expenses: &[Expense]) -> BTreeMap<&str, u64> { let mut totals = BTreeMap::new(); for expense in expenses { *totals.entry(expense.who.as_str()).or_insert(0) += expense.cents; } totals } // 9. Iterators and closures: filter, map and sum, without a loop. fn total_for(expenses: &[Expense], category: Category) -> u64 { expenses .iter() .filter(|e| e.category == category) .map(|e| e.cents) .sum() } fn euros(cents: u64) -> String { format!("€{}.{:02}", cents / 100, cents % 100) } // 10. A constant string. The `\` at the end of the first line skips the // line break that follows it. The euro sign in `euros` above works // because Rust source files are always UTF-8. const INPUT: &str = "\ Ada Example, 42.50, food Ben Example, 120, travel Ada Example, 7.25, other Cleo Example, twelve, food Ben Example, 18.9, food"; // 11. `main` is where the program starts. `println!` and `eprintln!` are // macros (the `!` says so): one writes to standard output, the other // to standard error. fn main() { let mut expenses = Vec::new(); for (index, line) in INPUT.lines().enumerate() { match parse_line(index + 1, line) { Ok(expense) => expenses.push(expense), Err(error) => eprintln!("skipped {error}"), } } println!("{} expenses", expenses.len()); for (who, cents) in totals_by_person(&expenses) { println!("{who:<14}{:>9}", euros(cents)); } println!( "food {}, travel {}", euros(total_for(&expenses, Category::Food)), euros(total_for(&expenses, Category::Travel)) ); } // 12. Unit tests live next to the code. `#[cfg(test)]` means this module // is only compiled by `cargo test` or `rustc --test`, so it adds // nothing to the normal program. #[cfg(test)] mod tests { use super::*; #[test] fn parses_amounts() { assert_eq!(parse_cents("12.5"), Some(1250)); assert_eq!(parse_cents("7"), Some(700)); assert_eq!(parse_cents("1.234"), None); assert_eq!(parse_cents("twelve"), None); } #[test] fn rejects_short_lines() { assert_eq!( parse_line(3, "Ada Example, 1"), Err(ParseError::MissingField(3)) ); } #[test] fn adds_up_per_person() { let expenses: Vec = INPUT .lines() .enumerate() .filter_map(|(i, line)| parse_line(i + 1, line).ok()) .collect(); let totals = totals_by_person(&expenses); assert_eq!(totals["Ada Example"], 4975); assert_eq!(totals["Ben Example"], 13890); assert!(!totals.contains_key("Cleo Example")); } }