Beginner Projects
CSV-Style Text Processor
A CSV-style text processor parses delimited tabular records—correctly handling quoted fields containing commas and escaped double quotes ("") according to RFC 4180—and performs column projection (select), row filtering (where), and numeric column aggregation (sum, min, max, avg).
RFC 4180 CSV Row State Machine (`parse_csv_row`):
Outside Quotes:
- `,` ---> End current field, push to row
- `"` ---> Enter `InQuotes` mode (if at start of field)
- `ch` ---> Append `ch` to current field
Inside Quotes (`InQuotes`):
- `""` ---> Escaped literal `"`! Append single `"` and stay in `InQuotes`
- `"` ---> Exit `InQuotes` mode
- `,`/any ---> Literal character inside quotes! Append to current field
Why Naive line.split(',') Fails on Real CSV Data
Calling line.split(',') splits inside quoted strings like "Narayanganj, Dhaka",1200—corrupting column alignment! A small state-machine parser handles quoted commas and "" escaped quotes in a single pass.
Complete Runnable Implementation
use std::collections::HashMap;
/// Parses a single RFC 4180 CSV line, supporting quoted fields and `""` escaped quotes.
pub fn parse_csv_row(line: &str) -> Vec<String> {
let mut fields = Vec::new();
let mut current = String::new();
let mut chars = line.trim_end_matches(['\r', '\n']).chars().peekable();
let mut in_quotes = false;
while let Some(ch) = chars.next() {
if in_quotes {
if ch == '"' {
if chars.peek() == Some(&'"') {
// `""` inside quotes is an escaped literal `"`
current.push('"');
chars.next();
} else {
in_quotes = false;
}
} else {
current.push(ch);
}
} else {
match ch {
'"' if current.is_empty() => in_quotes = true,
',' => {
fields.push(std::mem::take(&mut current));
}
other => current.push(other),
}
}
}
fields.push(current);
fields
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ColumnSummary {
pub count: usize,
pub sum: f64,
pub min: f64,
pub max: f64,
pub mean: f64,
}
pub struct CsvTable {
headers: Vec<String>,
header_index: HashMap<String, usize>,
rows: Vec<Vec<String>>,
}
impl CsvTable {
pub fn parse(csv_text: &str) -> Option<Self> {
let mut lines = csv_text.lines().filter(|l| !l.trim().is_empty());
let headers = parse_csv_row(lines.next()?);
let header_index: HashMap<String, usize> = headers
.iter()
.enumerate()
.map(|(i, h)| (h.trim().to_string(), i))
.collect();
let rows: Vec<Vec<String>> = lines.map(parse_csv_row).collect();
Some(Self {
headers,
header_index,
rows,
})
}
pub fn headers(&self) -> &[String] {
&self.headers
}
/// Computes summary statistics (`count`, `sum`, `min`, `max`, `mean`) for a numeric column.
pub fn summarize_column(&self, col_name: &str) -> Option<ColumnSummary> {
let &col_idx = self.header_index.get(col_name)?;
let mut count = 0usize;
let mut sum = 0.0f64;
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for row in &self.rows {
if let Some(cell) = row.get(col_idx) {
if let Ok(val) = cell.trim().parse::<f64>() {
count += 1;
sum += val;
min = min.min(val);
max = max.max(val);
}
}
}
if count == 0 {
return None;
}
Some(ColumnSummary {
count,
sum,
min,
max,
mean: sum / (count as f64),
})
}
/// Filters rows where `filter_col == expected_val` and projects `select_cols`.
pub fn filter_and_project(
&self,
filter_col: &str,
expected_val: &str,
select_cols: &[&str],
) -> Vec<Vec<String>> {
let Some(&filter_idx) = self.header_index.get(filter_col) else {
return Vec::new();
};
let proj_indices: Vec<usize> = select_cols
.iter()
.filter_map(|&c| self.header_index.get(c).copied())
.collect();
self.rows
.iter()
.filter(|row| row.get(filter_idx).is_some_and(|v| v == expected_val))
.map(|row| {
proj_indices
.iter()
.map(|&idx| row.get(idx).cloned().unwrap_or_default())
.collect()
})
.collect()
}
}
fn main() {
let raw_csv = "\
id,item,category,price
1,\"Keyboard, Mechanical\",hardware,120.50
2,\"Monitor 27\"\" 4K\",hardware,349.50
3,Rust Book,books,39.00
4,USB-C Hub,hardware,30.00
";
let table = CsvTable::parse(raw_csv).unwrap();
assert_eq!(table.headers(), &["id", "item", "category", "price"]);
let hw_items = table.filter_and_project("category", "hardware", &["item", "price"]);
assert_eq!(hw_items.len(), 3);
// Verify quoted comma and escaped `""` were parsed accurately:
assert_eq!(hw_items[0][0], "Keyboard, Mechanical");
assert_eq!(hw_items[1][0], "Monitor 27\" 4K");
let price_stats = table.summarize_column("price").unwrap();
println!(
"Price stats: count={}, sum={:.2}, min={:.2}, max={:.2}, mean={:.2}",
price_stats.count, price_stats.sum, price_stats.min, price_stats.max, price_stats.mean
);
println!("Projected hardware rows: {hw_items:?}");
}
Price stats: count=4, sum=539.00, min=30.00, max=349.50, mean=134.75
Projected hardware rows: [["Keyboard, Mechanical", "120.50"], ["Monitor 27\" 4K", "349.50"], ["USB-C Hub", "30.00"]]