Cash Register — Rust implementation#99
Conversation
Initialize the cash-register binary crate with rand, thiserror, and proptest dependencies. Add sample_input.txt from the problem statement and a minimal main.rs / lib.rs skeleton.
Define CashRegisterError with thiserror for structured errors including line numbers. Add Denomination/Currency types with static USD and EUR tables. Denominations are const data — no heap allocation for config.
Parse dollar strings to cents via string manipulation — no floating-point math. Handles whole numbers, 1-2 decimal places, whitespace trimming. Reports malformed lines and underpayments with line numbers. Includes 16 unit tests for parsing edge cases.
Define the ChangeStrategy trait and Breakdown type. GreedyStrategy iterates denominations largest-to-smallest, taking the maximum of each — producing minimum denomination count. Verified against the sample output (88 cents = 3 quarters, 1 dime, 3 pennies).
RandomStrategy<R: Rng> picks random counts for each denomination with the last one absorbing the remainder — guaranteeing exact change in O(n) with no retries. Generic RNG enables deterministic testing via StdRng::seed_from_u64().
rules.rs dispatches to greedy or random strategy based on the divisor predicate (owed_cents % divisor == 0). format.rs handles singular/plural denomination names and comma joining. Both are independently testable with no coupling to each other.
Integration tests run the binary against sample_input.txt and verify greedy lines match exactly, random lines sum correctly, and --seed produces deterministic output. Property tests (proptest) verify random strategy invariants across thousands of inputs: always sums to target, only valid denominations, all counts > 0.
Document build/run/test commands, architecture overview, and key design decisions (integer cents, strategy trait, injectable RNG). Answer the three "Things to Consider" questions: changing the divisor, adding new strategies, and supporting French currency.
EUR was defined in currency.rs but inaccessible from the CLI. Add --currency USD|EUR flag so the France extensibility claim is actually demonstrable, not just theoretical.
sample_edge_cases.txt: exact payment, single penny, large amount, sub-dollar amounts. sample_eur.txt: EUR transactions for demoing multi-currency support. Both are runnable out of the box.
Test greedy with EUR denominations (63c, 387c), single penny, and large amounts ($99.99). Test random with EUR across 100 seeds. Ensures the strategies are currency-agnostic, not just USD-tested.
Integration: EUR end-to-end greedy/random, edge case file, unknown currency error, underpayment stderr message, malformed line with line number, mixed valid/invalid input processing. Property tests: EUR random sums, EUR greedy sums, EUR valid denominations only.
Add EUR and edge-case sample output examples. Update usage line, flags section, and France extensibility answer to reference the now-functional --currency EUR flag.
Shows "Owed $2.12, Paid $3.00 -> 3 quarters,1 dime,3 pennies" with "(random)" label on randomized lines. Default output remains spec-compliant. Makes sample files self-explanatory when demoing.
Test verbose context lines, random labeling, edge cases (no change, large amounts), and EUR verbose output. Ensures verbose mode shows correct transaction details without breaking spec-format tests.
Lead sample output with --verbose so readers immediately see what each transaction does. Document all flags. Update test counts to 72 total (46 unit + 18 integration + 8 property-based).
EUR verbose now shows €, not $. Currency struct gains a symbol field that drives format_verbose(). Removed redundant parse_string_flag (parse_flag::<String> suffices). Applied is_multiple_of() per clippy suggestion.
Normalize formatting to match rustfmt defaults. No logic changes.
There was a problem hiding this comment.
Pull request overview
Implements the “Cash Register” CLI in Rust: parse owed,paid input lines, choose greedy vs randomized change-making based on a divisor rule, support USD/EUR, and provide formatted output with optional verbose context.
Changes:
- Added core library modules for parsing, strategy-based change computation (greedy/random), rule dispatch, and formatting.
- Added a CLI binary (
cash-register) with flags for divisor/seed/currency/verbose and sample input files. - Added extensive test coverage: unit tests, end-to-end integration tests, and property-based tests.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/main.rs |
CLI wiring: args, file I/O, RNG seeding, invoking rules + formatting |
src/lib.rs |
Library module exports |
src/currency.rs |
USD/EUR denomination tables and invariants |
src/parse.rs |
Input parsing and validation into Transaction |
src/strategy/mod.rs |
Strategy trait and Breakdown type |
src/strategy/greedy.rs |
Greedy (minimum denomination count) change strategy + tests |
src/strategy/random.rs |
Randomized change strategy with injectable RNG + tests |
src/rules.rs |
Business rule dispatch: divisor check → greedy or random + tests |
src/format.rs |
Output formatting for normal + verbose modes + tests |
src/error.rs |
Structured parse errors with line numbers |
tests/integration.rs |
End-to-end CLI tests via cargo run |
tests/proptest.rs |
Property-based invariant tests for strategies |
README.md |
Usage/design docs and examples |
Cargo.toml |
Crate configuration and dependencies |
.gitignore |
Build artifacts and lockfile ignore rules |
sample_input.txt / sample_eur.txt / sample_edge_cases.txt |
Example input fixtures used by docs/tests |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let dollars: u32 = s | ||
| .parse() | ||
| .map_err(|_| format!("not a valid number: \"{s}\""))?; | ||
| Ok(dollars * 100) | ||
| } |
There was a problem hiding this comment.
parse_dollars_to_cents uses unchecked u32 arithmetic (dollars * 100). Large inputs can overflow in release builds and silently wrap, yielding incorrect cents values and potentially incorrect change. Use checked_mul/checked_add (or u64 intermediate + range check) and return a parse error on overflow.
| let cents: u32 = padded | ||
| .parse() | ||
| .map_err(|_| format!("invalid cents part: \"{s}\""))?; | ||
|
|
||
| Ok(dollars * 100 + cents) | ||
| } |
There was a problem hiding this comment.
This addition (dollars * 100 + cents) can overflow u32 for large dollar amounts, leading to wrapped values in release builds. Consider doing the math in u64 and validating the result fits in u32, or using checked_mul/checked_add and returning an error when it doesn’t.
| args.iter() | ||
| .position(|a| a == flag) | ||
| .and_then(|i| args.get(i + 1)) | ||
| .and_then(|v| v.parse().ok()) |
There was a problem hiding this comment.
parse_flag silently treats missing/invalid flag values as “flag not present” (v.parse().ok()), which can lead to surprising behavior (e.g., --divisor foo falls back to default 3, --seed with no value falls back to entropy). Consider validating flags and exiting with a clear usage/error message when a flag is provided but missing/invalid.
| args.iter() | |
| .position(|a| a == flag) | |
| .and_then(|i| args.get(i + 1)) | |
| .and_then(|v| v.parse().ok()) | |
| if let Some(i) = args.iter().position(|a| a == flag) { | |
| // Flag is present; ensure there is a following value and that it parses. | |
| let value_str = match args.get(i + 1) { | |
| Some(v) => v, | |
| None => { | |
| eprintln!("Error: missing value for flag '{}'", flag); | |
| process::exit(1); | |
| } | |
| }; | |
| match value_str.parse() { | |
| Ok(v) => Some(v), | |
| Err(_) => { | |
| eprintln!( | |
| "Error: invalid value '{}' for flag '{}'", | |
| value_str, flag | |
| ); | |
| process::exit(1); | |
| } | |
| } | |
| } else { | |
| // Flag not present at all. | |
| None | |
| } |
| let dir = env!("CARGO_MANIFEST_DIR"); | ||
| let path = format!("{dir}/test_underpayment.txt"); | ||
| std::fs::write(&path, "5.00,3.00\n").unwrap(); | ||
|
|
There was a problem hiding this comment.
These tests create temp files with fixed names under the repo root (e.g., test_underpayment.txt). Since Rust tests run in parallel by default, this can cause racy failures when multiple tests read/write/remove the same path. Use a unique temp path (e.g., tempfile::NamedTempFile / tempdir, or include PID + counter) and let the tempfile cleanup handle deletion.
| let dir = env!("CARGO_MANIFEST_DIR"); | ||
| let path = format!("{dir}/test_malformed.txt"); | ||
| std::fs::write(&path, "2.12,3.00\nbad_line\n1.97,2.00\n").unwrap(); | ||
|
|
There was a problem hiding this comment.
This test writes to a fixed filename (test_malformed.txt) in the project directory, which can collide with other concurrently running tests and lead to flaky behavior. Prefer a unique temp file (tempfile crate) or generate a per-test unique name in a temp directory.
| let dir = env!("CARGO_MANIFEST_DIR"); | ||
| let path = format!("{dir}/test_mixed.txt"); | ||
| std::fs::write(&path, "1.00,2.00\nabc,def\n0.50,1.00\n").unwrap(); | ||
|
|
There was a problem hiding this comment.
This test writes to a fixed filename (test_mixed.txt) in the project directory. With parallel test execution, this can race with other tests and intermittently fail. Use a unique temp file path (e.g., tempfile) to avoid cross-test interference.
Summary
Rust implementation of the Cash Register problem. Reads a flat file of
owed,paidpairs and outputs change denominations, with randomized denominations when the owed amount is divisible by 3.u32cents, no floating-point mathGreedyStrategy(minimum coins) andRandomStrategy<R: Rng>(randomized), dispatched by a rules engine--currencyflagproptest)Quick start
cargo build cargo run -- sample_input.txt cargo run -- sample_input.txt --verbose --seed 42 cargo run -- sample_eur.txt --currency EUR --verbose cargo testExtensibility
--divisor Nat the CLI, no code changesrules.rscurrency.rsand a CLI match armSee README for full design decisions and architecture.