Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8affb89
Set up Rust project with Cargo.toml and entry point
georgeglarson Feb 27, 2026
abe005b
Add error types and currency denomination configuration
georgeglarson Feb 27, 2026
82175fb
Implement transaction parsing with integer cents arithmetic
georgeglarson Feb 27, 2026
341c2eb
Implement greedy change algorithm with ChangeStrategy trait
georgeglarson Feb 27, 2026
1f04140
Implement randomized change algorithm with injectable RNG
georgeglarson Feb 27, 2026
6d1b939
Add rules engine and output formatting
georgeglarson Feb 27, 2026
e693dc9
Add integration tests and property-based tests
georgeglarson Feb 27, 2026
9bb1d93
Update README with design decisions and extensibility notes
georgeglarson Feb 27, 2026
7a2b802
Add --currency flag to CLI for EUR support
georgeglarson Feb 27, 2026
7c83b35
Add sample files for edge cases and EUR currency
georgeglarson Feb 27, 2026
cd0f2f2
Add EUR and edge-case unit tests for both strategies
georgeglarson Feb 27, 2026
b5a22bb
Expand integration and property tests for EUR and error paths
georgeglarson Feb 27, 2026
a30ba57
Document --currency flag and sample files in README
georgeglarson Feb 27, 2026
0141ad2
Add --verbose flag for human-readable transaction output
georgeglarson Feb 27, 2026
2e1455f
Add integration tests for --verbose output formatting
georgeglarson Feb 27, 2026
38deab2
Update README with verbose examples and correct test counts
georgeglarson Feb 27, 2026
87b5537
Use correct currency symbol in verbose output, fix clippy warnings
georgeglarson Feb 27, 2026
fed2a83
Apply cargo fmt across all source and test files
georgeglarson Feb 27, 2026
74e5e9a
Fix EUR examples in README to show € symbol, update test counts
georgeglarson Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "cash-register"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "cash-register"
path = "src/main.rs"

[dependencies]
thiserror = "2"
rand = "0.8"

[dev-dependencies]
proptest = "1"
133 changes: 107 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,124 @@
# Cash Register

A command-line tool that calculates change denominations for cashiers. Written in Rust.

## Quick Start

```bash
cargo build
cargo run -- sample_input.txt
cargo test
```

## Usage

```
cash-register <input-file> [--divisor N] [--seed N] [--currency USD|EUR] [--verbose]
```

**Input file**: Each line contains `owed,paid` as dollar amounts (e.g., `2.13,3.00`). Blank lines are skipped.

**Output**: One line per transaction showing the change denominations.

```bash
$ cargo run -- sample_input.txt --verbose
Owed $2.12, Paid $3.00 -> 3 quarters,1 dime,3 pennies
Owed $1.97, Paid $2.00 -> 3 pennies
Owed $3.33, Paid $5.00 -> 5 quarters,5 nickels,17 pennies (random)

$ cargo run -- sample_edge_cases.txt --divisor 0 --verbose
Owed $5.00, Paid $5.00 -> no change
Owed $0.01, Paid $1.00 -> 3 quarters,2 dimes,4 pennies
Owed $100.00, Paid $200.00 -> 100 dollars
Owed $1.97, Paid $2.00 -> 3 pennies
Owed $3.00, Paid $5.00 -> 2 dollars
Owed $0.75, Paid $1.00 -> 1 quarter

$ cargo run -- sample_eur.txt --currency EUR --verbose
Owed €1.50, Paid €2.00 -> 1 20 cent coin,2 10 cent coins,2 5 cent coins (random)
Owed €3.33, Paid €5.00 -> 1 50 cent coin,1 10 cent coin,7 5 cent coins (random)
Owed €0.37, Paid €1.00 -> 1 50 cent coin,1 10 cent coin,1 2 cent coin,1 1 cent coin
Owed €7.77, Paid €10.00 -> 1 2 euro coin,1 20 cent coin,1 2 cent coin,1 1 cent coin (random)
```

Without `--verbose`, output matches the spec format exactly (`3 quarters,1 dime,3 pennies`).

### Flags

- `--divisor N` — Change which transactions get randomized denominations (default: 3). If `owed` in cents is divisible by N, the change is randomized. Use `--divisor 0` to disable randomization entirely.
- `--seed N` — Seed the random number generator for reproducible output. Useful for testing.
- `--currency USD|EUR` — Select the currency denomination set (default: USD).
- `--verbose` — Show transaction context alongside the change output. Labels random lines.

## The Problem
Creative Cash Draw Solutions is a client who wants to provide something different for the cashiers who use their system. The function of the application is to tell the cashier how much change is owed, and what denominations should be used. In most cases the app should return the minimum amount of physical change, but the client would like to add a twist. If the "owed" amount is divisible by 3, the app should randomly generate the change denominations (but the math still needs to be right :))

Please write a program which accomplishes the clients goals. The program should:
[Original problem statement from TrueFit](https://github.com/TrueFit/CashRegister): given a flat file of `owed,paid` pairs, output change denominations. When the owed amount is divisible by 3, randomize the denominations instead of minimizing them.

## Design Decisions

### Integer cents everywhere

All money is represented as `u32` cents. The string `"2.13"` is parsed via string manipulation into `213u32` — no floating-point arithmetic is ever used. This eliminates an entire class of rounding bugs (e.g., `0.1 + 0.2 != 0.3` in IEEE 754).

### Strategy trait with concrete types

A `ChangeStrategy` trait defines the contract. `GreedyStrategy` minimizes denomination count; `RandomStrategy` randomizes it. The `rules` module decides which strategy to use based on transaction properties. Each piece has a single responsibility:

- Algorithms don't know about business rules
- Rules don't know about algorithm internals
- Adding either doesn't require changing the other

### Injectable randomness

`RandomStrategy<R: Rng>` is generic over its RNG source. Tests inject `StdRng::seed_from_u64()` for deterministic assertions. Production uses `StdRng::from_entropy()`. Zero-cost abstraction via monomorphization — no `Box<dyn Rng>`.

### No heavy dependencies

CLI argument parsing is a 5-line generic function, not a 50KB dependency. The only runtime dependencies are `thiserror` (structured errors) and `rand` (randomization) — both are well-established, minimal crates.

1. Accept a flat file as input
1. Each line will contain the amount owed and the amount paid separated by a comma (for example: 2.13,3.00)
2. Expect that there will be multiple lines
2. Output the change the cashier should return to the customer
1. The return string should look like: 1 dollar,2 quarters,1 nickel, etc ...
2. Each new line in the input file should be a new line in the output file
### Property-based testing

## Sample Input
2.12,3.00
Unit tests verify specific cases. Property tests (`proptest`) verify invariants across thousands of random inputs: the random algorithm always sums to the target amount, only uses valid denominations, and never includes zero-count entries.

1.97,2.00
## Architecture

3.33,5.00
```
src/
main.rs CLI wiring: arg parsing, file I/O, exit codes
lib.rs Module re-exports
error.rs Error types with line numbers (thiserror)
currency.rs Denomination definitions — USD, EUR configs
parse.rs String → cents conversion, line → Transaction
strategy/
mod.rs ChangeStrategy trait, Breakdown type alias
greedy.rs Minimum denomination count algorithm
random.rs Randomized denomination algorithm
rules.rs Strategy dispatch: divisor check → greedy or random
format.rs Breakdown → output string (pluralization, joining)
tests/
integration.rs End-to-end binary tests
proptest.rs Property-based correctness tests
```

## Sample Output
3 quarters,1 dime,3 pennies
## Things to Consider

3 pennies
> What might happen if the client needs to change the random divisor?

1 dollar,1 quarter,6 nickels,12 pennies
Pass `--divisor N` at the command line. The divisor flows through `rules::make_change_for` as a parameter — no code changes needed. Setting `--divisor 0` disables randomization entirely.

*Remember the last one is random
> What might happen if the client needs to add another special case (like the random twist)?

## The Fine Print
Please use whatever technology and techniques you feel are applicable to solve the problem. We suggest that you approach this exercise as if this code was part of a larger system. The end result should be representative of your abilities and style.
Add a new strategy struct implementing `ChangeStrategy` (one file), then add a branch in `rules.rs`. Existing strategies and tests are untouched. For example, a "round up to nearest quarter" strategy would be ~20 lines of code and one new match arm.

Please fork this repository. When you have completed your solution, please issue a pull request to notify us that you are ready.
> What might happen if sales closes a new client in France?

Have fun.
Pass `--currency EUR`. The EUR denomination table is already defined and wired into the CLI. Try it: `cargo run -- sample_eur.txt --currency EUR`. The denomination table drives all formatting — singular/plural names, values, everything. One caveat: France uses commas as decimal separators (`2,13` not `2.13`), which conflicts with the comma-delimited input format. A real deployment would need a configurable delimiter or a different input format (e.g., TSV, JSON).

## Things To Consider
Here are a couple of thoughts about the domain that could influence your response:
## Testing

* What might happen if the client needs to change the random divisor?
* What might happen if the client needs to add another special case (like the random twist)?
* What might happen if sales closes a new client in France?
```bash
cargo test # All 74 tests: unit + integration + property-based
cargo test --lib # Unit tests only (48 tests)
cargo test --test integration # Integration tests only (18 tests)
cargo test --test proptest # Property-based tests only (8 tests)
```
6 changes: 6 additions & 0 deletions sample_edge_cases.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
5.00,5.00
0.01,1.00
100.00,200.00
1.97,2.00
3.00,5.00
0.75,1.00
4 changes: 4 additions & 0 deletions sample_eur.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1.50,2.00
3.33,5.00
0.37,1.00
7.77,10.00
3 changes: 3 additions & 0 deletions sample_input.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
2.12,3.00
1.97,2.00
3.33,5.00
129 changes: 129 additions & 0 deletions src/currency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/// A single denomination: its value in cents and display names.
#[derive(Debug, Clone, Copy)]
pub struct Denomination {
pub cents: u32,
pub singular: &'static str,
pub plural: &'static str,
}

/// A currency configuration: a name, symbol, and denominations (largest first).
#[derive(Debug, Clone)]
pub struct Currency {
pub name: &'static str,
pub symbol: &'static str,
pub denominations: &'static [Denomination],
}

pub static USD: Currency = Currency {
name: "USD",
symbol: "$",
denominations: &[
Denomination {
cents: 100,
singular: "dollar",
plural: "dollars",
},
Denomination {
cents: 25,
singular: "quarter",
plural: "quarters",
},
Denomination {
cents: 10,
singular: "dime",
plural: "dimes",
},
Denomination {
cents: 5,
singular: "nickel",
plural: "nickels",
},
Denomination {
cents: 1,
singular: "penny",
plural: "pennies",
},
],
};

pub static EUR: Currency = Currency {
name: "EUR",
symbol: "€",
denominations: &[
Denomination {
cents: 200,
singular: "2 euro coin",
plural: "2 euro coins",
},
Denomination {
cents: 100,
singular: "1 euro coin",
plural: "1 euro coins",
},
Denomination {
cents: 50,
singular: "50 cent coin",
plural: "50 cent coins",
},
Denomination {
cents: 20,
singular: "20 cent coin",
plural: "20 cent coins",
},
Denomination {
cents: 10,
singular: "10 cent coin",
plural: "10 cent coins",
},
Denomination {
cents: 5,
singular: "5 cent coin",
plural: "5 cent coins",
},
Denomination {
cents: 2,
singular: "2 cent coin",
plural: "2 cent coins",
},
Denomination {
cents: 1,
singular: "1 cent coin",
plural: "1 cent coins",
},
],
};

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn usd_denominations_are_sorted_descending() {
let denoms = USD.denominations;
for window in denoms.windows(2) {
assert!(
window[0].cents > window[1].cents,
"{} ({}) should be larger than {} ({})",
window[0].singular,
window[0].cents,
window[1].singular,
window[1].cents,
);
}
}

#[test]
fn usd_smallest_denomination_is_one_cent() {
let last = USD.denominations.last().unwrap();
assert_eq!(
last.cents, 1,
"smallest denomination must be 1 cent for exact change"
);
}

#[test]
fn eur_smallest_denomination_is_one_cent() {
let last = EUR.denominations.last().unwrap();
assert_eq!(last.cents, 1);
}
}
20 changes: 20 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CashRegisterError {
#[error("line {line}: invalid dollar amount \"{input}\"")]
InvalidAmount { line: usize, input: String },

#[error("line {line}: paid ({paid}) is less than owed ({owed})")]
Underpayment {
line: usize,
owed: String,
paid: String,
},

#[error("line {line}: {detail}")]
MalformedLine { line: usize, detail: String },

#[error("{0}")]
Io(#[from] std::io::Error),
}
Loading
Loading