Skip to content

Cash Register — Rust implementation#99

Open
georgeglarson wants to merge 19 commits intoTrueFit:mainfrom
georgeglarson:main
Open

Cash Register — Rust implementation#99
georgeglarson wants to merge 19 commits intoTrueFit:mainfrom
georgeglarson:main

Conversation

@georgeglarson
Copy link

Summary

Rust implementation of the Cash Register problem. Reads a flat file of owed,paid pairs and outputs change denominations, with randomized denominations when the owed amount is divisible by 3.

  • Integer cents arithmetic — all money is u32 cents, no floating-point math
  • Strategy patternGreedyStrategy (minimum coins) and RandomStrategy<R: Rng> (randomized), dispatched by a rules engine
  • Multi-currency support — USD and EUR via --currency flag
  • 74 tests — 48 unit, 18 integration, 8 property-based (proptest)

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 test

Extensibility

  • Change the divisor? --divisor N at the CLI, no code changes
  • Add a new strategy? One file + one match arm in rules.rs
  • New currency? Add a denomination table to currency.rs and a CLI match arm

See README for full design decisions and architecture.

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.
Copilot AI review requested due to automatic review settings February 27, 2026 00:56
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +24 to +28
let dollars: u32 = s
.parse()
.map_err(|_| format!("not a valid number: \"{s}\""))?;
Ok(dollars * 100)
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +50
let cents: u32 = padded
.parse()
.map_err(|_| format!("invalid cents part: \"{s}\""))?;

Ok(dollars * 100 + cents)
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +81 to +84
args.iter()
.position(|a| a == flag)
.and_then(|i| args.get(i + 1))
.and_then(|v| v.parse().ok())
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
Comment on lines +222 to +225
let dir = env!("CARGO_MANIFEST_DIR");
let path = format!("{dir}/test_underpayment.txt");
std::fs::write(&path, "5.00,3.00\n").unwrap();

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +247 to +250
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();

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +275 to +278
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();

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants