The Utility Billing Loop That Skips a Customer
A student is writing the billing module for a small water utility. The specification says: for each of the customers on the route, read the previous and current meter readings, compute consumption, apply a standing charge of $12.00, and print a bill; apply a rate of $0.45 per unit for the first 100 units, $0.60 for units 101 to 200, and $0.85 above 200; at the end print the average bill. The student's pseudocode declares rate as an integer, writes WHILE count <= numberOfCustomers with count starting at 0, accumulates total, and finishes with average = total / count. There is no check that the current reading is greater than the previous one, and no rounding before printing. A test run with three customers printed four bills, the fourth containing nonsense values, and the average was wrong. A test route with zero customers crashed.
1. Identify and explain each defect in the student's algorithm, including the ones the test run did not expose.
Read the worked answer
Defects visible in the test run
- Off-by-one loop bound.
countstarts at 0 and the condition iscount <= numberOfCustomers, so the body runs n+1 times. With three customers it reads a fourth, non-existent record - the nonsense bill - and then exits. - Average divides by the wrong count. After the loop
countequals n+1, andtotalcontains the garbage bill, so the average is wrong twice over. - Division by zero. With zero customers the loop still executes once and then divides by a zero count; the design needs an explicit guard for an empty route.
Defects the test run did not expose
- Integer rate. Declaring
rateas an integer truncates 0.45, 0.60 and 0.85 to 0, so every bill is just the standing charge. Consumption and money should be real (or integer cents), never integer units. - No validation of readings. If the current reading is lower than the previous one - a replaced meter, a transposed digit, an estimated reading - consumption is negative and the bill becomes a credit. The program must detect and flag rather than compute.
- No rounding policy. Repeated floating-point addition produces values like 47.300000000000004. Round each bill to two decimal places at the point of printing, and state whether totals use rounded or unrounded values.
- Ambiguous tier semantics. The spec does not say whether the rate applies to all units or progressively. The student's code applies the rate to the whole consumption; if the intention is marginal tiers, three separate calculations are needed.
- No handling of non-numeric or missing input, and no error path for a customer who should be skipped.
Diagnosis discipline: a test that shows one symptom usually hides others; the boundary values in the next task are what surface them.
2. Rewrite the algorithm in pseudocode so that it is correct and defensively handles bad readings and an empty route.
Read the worked answer
SET total ← 0
SET billed ← 0
READ numberOfCustomers
IF numberOfCustomers <= 0 THEN
OUTPUT "No customers on this route"
ELSE
FOR i ← 1 TO numberOfCustomers
READ customerName, previous, current
IF current < previous THEN
OUTPUT customerName, "Invalid reading: current below previous, bill withheld"
ELSE IF current or previous is not numeric THEN
OUTPUT customerName, "Invalid input, bill withheld"
ELSE
SET used ← current - previous
IF used <= 100 THEN
SET rate ← 0.45
ELSE IF used <= 200 THEN
SET rate ← 0.60
ELSE
SET rate ← 0.85
ENDIF
SET bill ← ROUND(used * rate + 12.00, 2)
OUTPUT customerName, used, bill
SET total ← total + bill
SET billed ← billed + 1
ENDIF
NEXT i
IF billed = 0 THEN
OUTPUT "No bills produced"
ELSE
OUTPUT "Average bill: ", ROUND(total / billed, 2)
ENDIF
ENDIF
Reasoning for the changes
- A counted
FOR i ← 1 TO nloop removes the off-by-one error by construction; the bound is stated once. billedcounts only accepted bills, so invalid records do not distort the average, and the empty-route case is handled before any division.- Validation precedes calculation: a meter reading that goes backwards is a data problem, and producing a negative bill silently hides it from the office.
- Rates are real numbers; rounding happens once, at output, so printed bills and the printed average agree.
- The tier logic still applies one rate to all units. If the utility intends marginal tiers, replace the single calculation with three: charge the first 100 units at 0.45, units 101–200 at 0.60, and the remainder at 0.85, summing the parts. That is a requirements decision to confirm with the client, not something the programmer should assume.
3. Design a set of test data for the corrected algorithm, and justify why each case is included.
Read the worked answer
Boundary and typical cases (single customer route unless stated)
| Previous | Current | Units | Purpose | Expected result |
|---|---|---|---|---|
| 1000 | 1050 | 50 | Ordinary case inside the lowest band | 50 × 0.45 + 12.00 = 34.50 |
| 1000 | 1099 | 99 | Just below the first boundary | 56.55 |
| 1000 | 1100 | 100 | Exactly on the first boundary | 57.00 |
| 1000 | 1101 | 101 | Just above the first boundary | 72.60 |
| 1000 | 1200 | 200 | Exactly on the second boundary | 132.00 |
| 1000 | 1201 | 201 | Just above the second boundary | 182.85 |
| 1000 | 1000 | 0 | Zero consumption; standing charge only | 12.00 |
| 1500 | 1400 | negative | Current below previous (meter replacement or typo) | Bill withheld, message printed |
| - | - | non-numeric | Text in a reading field | Bill withheld, message printed |
| route with 0 customers | Empty input | "No customers on this route", no division | ||
| route with 3 valid + 1 invalid | Average must ignore the rejected record | Average of the three valid bills | ||
| 1000.5 | 1100.25 | fractional | Real readings and rounding check | Bill rounded to 2 dp |
Justification
- Boundary values (99, 100, 101, 200, 201) are where conditional logic changes branch. Errors cluster at the comparison operator -
<=versus<- and a test at 150 would never reveal it. - Zero units proves the standing charge is applied independently of consumption.
- Invalid readings test the error path, which is the part most often written but never executed.
- Empty route tests the division-by-zero guard.
- Mixed valid and invalid records checks that
billedrather thannumberOfCustomersdrives the average - the defect the original test run hid. - Fractional readings check rounding. When reporting, state the expected output for each row before running, so the test is a comparison rather than an interpretation of whatever appears.