Skip to content
experience

The truncation that underpaid interest for eleven years

Walt Hendricks@walt_mainframe

June 20, 2026 · 3 min read

I have been asked a few times on here what the worst bug of my career was. It was not mine, but I was the one who found where it lived, so I will tell it.

In 2009 a retired schoolteacher walked into one of our branches with a passbook and a pocket calculator. She had been computing her own savings interest every month since 1998, out of habit, and for eleven years our figure had been a few cents lower than hers. Some months one cent. Some months nine. The branch manager, to his great credit, did not send her away. He sent the passbook to us.

It took me four days. In 1998, during the Y2K remediation, a programmer had reworked the daily accrual paragraph for the date logic. The old code accumulated the daily interest in a work field with six decimal places and rounded once, at month end. The new code stored each day's interest in a field with two decimal places before adding it to the monthly total.

      * WORKING-STORAGE, AS CHANGED IN 1998
       01  WS-DAILY-INT    PIC S9(7)V99    COMP-3.
       01  WS-MONTH-INT    PIC S9(7)V99    COMP-3.

      * PROCEDURE DIVISION
           COMPUTE WS-DAILY-INT = WS-BALANCE * WS-RATE / 365
           ADD WS-DAILY-INT TO WS-MONTH-INT

For the young people reading: COBOL does exactly what the PICTURE clause tells it. COMPUTE truncates. It does not round unless you write ROUNDED. So every single day, every account lost whatever fraction of a cent fell past the second decimal. Thirty days a month. Thirty-eight thousand passbook accounts. Eleven years. It came to a little over half a million dollars, a fraction of a cent at a time.

The fix was three lines:

       01  WS-DAILY-INT    PIC S9(7)V9(6)  COMP-3.
       01  WS-MONTH-INT    PIC S9(7)V9(6)  COMP-3.
       01  WS-POST-INT     PIC S9(7)V99    COMP-3.

           COMPUTE WS-DAILY-INT = WS-BALANCE * WS-RATE / 365
               ON SIZE ERROR PERFORM 9100-ACCRUAL-OVERFLOW
           END-COMPUTE
           ADD WS-DAILY-INT TO WS-MONTH-INT
      *    AT MONTH END, ROUND ONCE
           COMPUTE WS-POST-INT ROUNDED = WS-MONTH-INT

The remediation was not three lines. It was eight months, thirty-eight thousand letters, a refund with interest on the interest, and a conversation with the examiners that I was not invited to and did not want to be.

What I took from it, and still tell anyone who will listen:

  • Know what your language does with the digits it cannot keep. COBOL truncates. Other languages do other things. None of them ask you first.
  • Size errors are silent unless you say ON SIZE ERROR. Say it.
  • Any change that touches a money field needs a reconciliation against an independent calculation. Not the same program run twice. A different method.
  • When a customer says your number is wrong, assume for one day that she is right.

The schoolteacher got her refund and a letter from the president of the bank. She wrote back to say her figures had been correct all along, which they had. I kept a copy of that letter in my desk until we moved buildings.

Get the best of Dynamo Geeks weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

12

Sign in to join the conversation.

Sign in to comment
S. Ramanathan@ramanathan_retd
Dear Walt, This brought back memories. We had the same class of problem in our savings bank product in 2001, though ours was caught by a branch manager in Madurai and not by a customer. ROUNDED is the most important word in COBOL and the least typed. With regards, Ramanathan
4 net votes
Walt Hendricks@walt_mainframe
Mr. Ramanathan, A branch manager who reads the numbers is worth more than any test suite. Ours had stopped reading them in 1994.
4 net votes
Frank Ostrowski@frank_cpp98
This is why I trust integers and nothing else. Store cents. Store millicents if you must. Never let a language decide when to throw away a digit. Regards.
11 net votes
Walt Hendricks@walt_mainframe
Son, COMP-3 IS integers. Packed decimal with an implied point. The language did exactly what the PICTURE clause told it to. The programmer told it wrong. Lower your voice.
4 net votes
Kazuhiro Yamamoto@yamamoto_kaz
Thank you very much for this story. In our migration project we found the same truncation in 3 programs. Our Java team used double first and the result was different in the 4th decimal, and the customer did not accept. Now we use BigDecimal, and we must copy the truncation exactly with RoundingMode.DOWN, because the customer wants the same result as COBOL, even when the COBOL result is wrong. This is a very sad point. I am sorry for my poor English.
3 net votes
Walt Hendricks@walt_mainframe
Mr. Yamamoto, Your English is fine, and you have described every COBOL migration I have ever seen. Bug-for-bug compatibility is a real requirement. Write it in the design document so nobody 'fixes' it without a business decision.
3 net votes
Graham Whitfield@greybeard_graham
The retired schoolteacher with a calculator is the best audit control ever devised. We had a pensioner who rang every quarter about 3 cents. He was right twice. Cheers, G.
11 net votes
Rodel Bautista@rodel_java
Read this on the MRT. Our bank has a passbook product too. Going to look at the accrual program tomorrow. Ay naku.
5 net votes
Marc-André Bélanger@mabelanger
Good day, Mr. Hendricks. Our federal benefits system has a similar truncation in a COBOL program that feeds the Java forms I maintain. The business rule now says the truncation is the rule. Regards, Marc-André
5 net votes
Walt Hendricks@walt_mainframe
Marc-André, Then it is the rule. Make sure it is written down somewhere other than the code.
7 net votes
Arjun Mehta@arjunmehta
Found this through search. In lending we round per instalment and still get a 1 paisa reconciliation diff between our ledger and the bank statement every month. Do one thing: store the unrounded accrual separately and round only at posting, exactly as Walt did.
7 net votes