Capstone: CLI tip calculator

Station 1.C 40 min Interchange: $argv & number_format & round() → packages

End of the line for Unit 1. Everything so far has been one idea at a time. This capstone joins them up into a small but real program: a command-line tip calculator for splitting a restaurant bill.

$ tip 48.50 good 3
Bill     £48.50
Tip       £6.06  (12.5%)
Total    £54.56
Each     £18.19  (3 people, 1p over)

It takes the bill, the tip (a percentage, or poor, good or great), and optionally how many people are paying. If anything's wrong, it prints a usage message instead of guessing.

How it's put together

There are two packages, as in 1.11:

  • tip (in tip/tip.go) does the sums: parsing a bill, picking a percentage, working out the tip, splitting the total and formatting money. It knows nothing about the command line.
  • main (in main.go) reads the arguments, calls tip, and lays out the result. The layout is already written. You add the argument checks.
flowchart TB
  accTitle: How the tip calculator runs
  accDescr: main passes the arguments to run. run checks there are two or three of them, then parses the bill with tip.ParsePounds, the tip with tip.Percent, and, if given, the number of people with strconv.Atoi. Any problem returns the usage message straight away. Otherwise run works out the tip, the total and each person's share, and returns the receipt for main to print.
  a["run(os.Args[1:])"] --> n["2 or 3 arguments?"]
  n --> b["tip.ParsePounds"] --> p["tip.Percent"] --> ppl["people: 1 to 50?"]
  ppl --> calc["tip.Tip, tip.Split"] --> out(["the receipt"])
  n & b & p & ppl -.->|"no"| u(["usage"])

The work lives in run, not main. run takes the arguments and returns the output as a string, so the tests can call it with any arguments they like. main is one line: it passes os.Args[1:], which is PHP's $argv without the script name, and prints whatever comes back.

Laravel corner

In Laravel this would be an Artisan command with $signature = 'tip {bill} {tip} {people=1}', and Artisan (built on Symfony Console) would check the arguments and print the usage for you. Go's standard library has the flag package for -name=value options (lesson 7.5), and Cobra is the usual choice for big CLIs. For three positional arguments, a few ifs in run are all you need, and that's what you'll write.

What you'll use

Nearly every station in Unit 1 turns up somewhere:

You'll need From
Money in pence as an int, formatted with %d.%02d 1.6
float64(bill) before multiplying, and math.Round 1.3
Rounding up with whole-number division 0.6
Functions that return a value and an ok 1.2
A switch on the tip word 1.10
Trimming £ and % with the strings package 1.5
Early returns for every bad argument 1.8
Exported functions in a second package 1.11

There's one small new thing: turning text into numbers. strconv.ParseFloat and strconv.Atoi return the number and an error, and the Reference panel under the task shows the one-line check you need. Errors get a whole unit of their own later (Unit 4). For now, an error just means "that wasn't a number".

Tip

Work through it a function at a time. The tests are grouped by function, so TestFormat and TestTip can go green while TestRun is still red. Start at the bottom of tip/tip.go, where the functions are simplest, and work up. Leave run until last.

Mind the gap

Where PHP is better: number_format($pence / 100, 2) and round($x, 2) make money formatting one call in PHP. Go makes you choose between pence and floats, and say exactly how to round. That's more typing. The payoff is that every sum is exact, and the split never leaves the table a penny short.

When it's all green, Unit 1 is done. Unit 2 takes PHP's do-everything array apart into slices, maps and structs.

Build tip 48.50 good 3. Fill in the five functions in tip/tip.go, then the argument checks at the top of run in main.go. The doc comments are the spec, and the tests are grouped by function, so you can go one at a time.

Run passes no arguments, so a finished program prints its usage. Submit calls run with real arguments.

Reference
$ go doc strconv.ParseFloat
func ParseFloat(s string, bitSize int) (float64, error)
    Use bitSize 64 for a float64.

$ go doc strconv.Atoi
func Atoi(s string) (int, error)

$ go doc strings.TrimPrefix
func TrimPrefix(s, prefix string) string
    TrimPrefix returns s without the provided leading prefix string.
    If s doesn't start with prefix, s is returned unchanged.

$ go doc strings.TrimSuffix
func TrimSuffix(s, suffix string) string

$ go doc strings.ToLower
func ToLower(s string) string

Each package you use needs an import, as in 1.3. tip/tip.go will need fmt, math, strconv and strings, and main.go needs strconv as well. Press Format and gofmt sorts the import block for you.

The parse functions return an error as their second value (Unit 4 is all about these). For now, all you need is this: if err != nil, the string wasn't a number.

n, err := strconv.Atoi(s)
if err != nil {
	// not a number
}
Ctrl+Enter Running on go1.26…

Run prints whatever main() prints. Submit runs the hidden test suite.