Case Study 1: The Case of the Vanishing Percentage

"The program did exactly what I told it to. That was the problem."

Executive Summary

A colleague hands you a short Fortran utility that summarizes the day's temperature readings from the plate rig. It compiles without a single warning, runs without error, and reports that zero percent of the readings exceeded the alarm threshold — on a day when three of the four readings clearly did. There is no crash to debug and no error message to search for; the program is confidently, silently wrong. This case study is a guided autopsy. You will reproduce the symptom, trace it to not one but two instances of the integer-division trap (§3.4) hiding in plain sight, and port the utility to correct, modern, double-precision Fortran. By the end you will be able to look at any arithmetic-heavy routine and spot the places where a missing decimal point silently changes the answer — the single most valuable diagnostic skill this chapter teaches.

Skills applied: reading the type of an expression, not just its algebra (§3.3); recognizing the integer-division trap in context (§3.4); choosing real(dp) and converting operands deliberately (§3.2); the implicit none habit as a first line of defense (§3.1, and Chapter 2).

Background

The rig logs a handful of temperature readings (in whole degrees Celsius) over a run, and the group wants a one-line daily summary: the mean temperature, and the percentage of readings above an alarm threshold of 200 °C. Your colleague wrote this:

program plate_stats
  implicit none
  integer :: total, n, above
  real    :: mean, pct

  total = 210 + 225 + 195 + 240   ! the four readings, summed
  n     = 4                        ! how many readings
  above = 3                        ! how many exceeded 200 C

  mean = total / n
  pct  = above / n * 100

  print '(a, f8.3)', 'mean temperature   = ', mean
  print '(a, f8.3)', 'percent above 200  = ', pct
end program plate_stats

The algebra is beyond reproach. The sum of the four readings is 870; divided by 4 the mean is 217.5; three of the four exceed 200, so the percentage is 75. A hand calculation takes ten seconds and everyone agrees on the answer. Then you run the program.

Phase 1 — Reproduce the Symptom

Never trust a bug report you have not reproduced. Compile and run exactly what the colleague ran:

$ gfortran -std=f2018 -Wall plate_stats.f90 -o stats && ./stats
mean temperature   =  217.000
percent above 200  =    0.000

Two things are wrong, and they are wrong in different ways, which is a clue. The percentage is dramatically wrong — 0.000 where we expect 75.000, off by everything. The mean is subtly wrong — 217.000 where we expect 217.500, off by exactly one half. A dramatic error and a subtle error in the same tiny program, both from clean-compiling code with -Wall silent, is the signature of the integer-division trap: the compiler cannot warn you, because total / n is perfectly legal Fortran that simply does not mean what the author intended.

The discipline: resist the urge to "just add some decimal points until it works." Diagnose each wrong number independently and understand why it is wrong, or you will fix one and leave the other — or fix both by luck and never learn to see the trap coming.

Phase 2 — Diagnose the Dramatic Error

Start with the percentage, because a factor-of-infinity error is easier to see than a factor of 1.002. The line is:

pct = above / n * 100

Read it the way Fortran does — left to right at equal precedence, one operation at a time, each in the type of its operands. above is integer, n is integer, so above / n is integer division: 3 / 4 = 0, because the true quotient 0.75 is truncated toward zero. Now the expression is 0 * 100, which is 0, still an integer. Only at the final assignment is that 0 widened to the real value 0.0 and stored in pct. The real type of pct never had a chance; the fraction was destroyed three operations earlier, on the right-hand side, before the = was ever reached.

This is the central lesson of §3.4 made concrete: the type of a subexpression is decided by its own operands, not by where the result is eventually going. A real variable on the left cannot reach back into the right-hand side and rescue a quotient that was already computed in integer arithmetic.

Phase 3 — Diagnose the Subtle Error

Now the mean, which is more dangerous precisely because it is subtle. The line is:

mean = total / n

Again both operands are integers: total is 870, n is 4, so total / n is integer division, 870 / 4 = 217 (the true 217.5 truncated toward zero), then widened to 217.0 on assignment. The program reports 217.000.

Here is why this one is the more insidious of the two. An answer of 0 percent is obviously wrong and gets caught. An answer of 217.000 looks completely plausible — it is close to the truth, it has the right magnitude, it even has three confident decimal places — and it would sail through a code review, into a report, and possibly into a published table, carrying an error of half a degree that no one ever questions. Subtle wrong answers are the expensive ones. The integer-division trap specializes in them.

⚠️ Common Pitfall: The presence of a decimal point in the output (217.000) tells you nothing about whether real arithmetic was used. The f8.3 descriptor will happily print any real value, including one that was computed in integer arithmetic and widened at the last moment. Never let formatted output lull you into assuming the computation behind it was real.

Phase 4 — Port It to Correct Modern Fortran

Now fix it properly — not by sprinkling decimals until the numbers look right, but by making every division that should be real actually real, and by upgrading to the double precision this book uses everywhere. The corrected utility:

program plate_stats
  implicit none
  integer, parameter :: dp = selected_real_kind(15, 307)
  integer  :: total, n, above
  real(dp) :: mean, pct

  total = 210 + 225 + 195 + 240
  n     = 4
  above = 3

  mean = real(total, dp) / real(n, dp)
  pct  = real(above, dp) / real(n, dp) * 100.0_dp

  print '(a, f8.3)', 'mean temperature   = ', mean
  print '(a, f8.3)', 'percent above 200  = ', pct
end program plate_stats
$ gfortran -std=f2018 -Wall plate_stats.f90 -o stats && ./stats
mean temperature   =  217.500
percent above 200  =   75.000

Every change is deliberate. The counters total, n, and above stay integers — they are genuinely whole numbers, and integer is the honest, exact type for them. The change is at the point of division: real(n, dp) converts the denominator to a real(dp) before the / happens, which makes the whole division real-valued and preserves the fraction. Writing the literal as 100.0_dp rather than 100 keeps the type real all the way through. And mean and pct are now real(dp), so that when this utility grows up to average thousands of non-integer readings, it will carry fifteen digits instead of seven.

Phase 5 — Sanity Check, and a Word on Precision

Verify the fix against the hand calculation you did in the Background — because a fix you have not checked is just a different guess. The sum is $210 + 225 + 195 + 240 = 870$; the mean is $870 / 4 = 217.5$; the percentage is $3 / 4 \times 100 = 75$. Both match the program's new output exactly. The bug is closed, and you understand why it was ever open.

One last question worth asking, since we upgraded to real(dp) almost reflexively: did this program need double precision? For four small readings, honestly, no — single precision carries seven digits and 217.5 has four. But the habit is the point. This utility will be copied, extended, and pointed at a log of a million readings accumulated over a long run, and at that scale the seven-digit ceiling of single precision starts to matter (Exercise 3.22 estimates when). Choosing real(dp) now costs nothing and removes a latent bug from every future version. That is the §3.2 discipline: precision is a decision you make on purpose, and "double by default, single when you have measured that you can afford it" is the decision that ages well.

Discussion Questions

  1. -Wall caught nothing here. Explain precisely why the compiler could not warn about total / n, and contrast that with a mistake it would catch (for example, assigning a character to an integer). What does this tell you about the limits of relying on compiler warnings?
  2. The mean was wrong by 0.5 and the percentage was wrong by 75. Which error is more dangerous in practice, and why? How does your answer change if this code feeds a safety alarm versus a status dashboard?
  3. A teammate proposes a blanket rule: "declare everything real(dp), even counters, so this can never happen." Argue against it — what is lost, in correctness and in meaning, by making an array index or a loop counter a real number?

Your Turn: Extensions

  • Option A. Add a third statistic: the reading range, max - min of the four values, using the intrinsic max and min. Predict its output, then confirm it. (The readings are 210, 225, 195, 240.)
  • Option B. Generalize the utility to read the count n and the readings at run time rather than hardcoding them. You will need I/O you have not formally met yet — sketch the arithmetic changes now, and return to finish it after Chapter 7.
  • Option C. Introduce the subtle bug on purpose into a fresh copy, then write a one-sentence code-review comment that would catch it. What phrasing makes a reviewer look at operand types, not just algebra?

Key Takeaways

  • The integer-division trap produces two flavors of wrong answer from clean-compiling code: the dramatic (a fraction collapsing to 0) and the subtle (a quotient losing its fractional part). The subtle one is the more dangerous, because it looks right.
  • The type of an expression is fixed by its operands, not by the variable it is assigned to. A real on the left never rescues integer division on the right.
  • Fix by converting operands before the division (real(k, dp)) and by writing floating-point literals with a decimal point and _dp — not by adding decimals until the output looks plausible.
  • Compiler warnings are necessary, not sufficient. -Wall cannot see a logic error that is expressed in entirely legal code; reading the arithmetic yourself is the only defense.