AI can turn a COBOL program into Java that compiles on the first try. A clean compile tells you the syntax is valid. It says nothing about whether the Java produces the same numbers as the COBOL, and in financial code the numbers are the whole job.

This post walks through one small interest calculation, shows where a faithful-looking conversion goes wrong, and builds the test that catches it. The expected values in that test come from the legacy system itself, which is the step most teams skip.

The legacy program

Here is the rule in COBOL, with declarations trimmed for space:

 01 ACCT-BALANCE PIC S9(7)V99. 01 WS-BAL PIC 9(7)V99. 01 WS-RATE PIC 9V999. 01 WS-INTEREST PIC 9(7)V99. COMPUTE-INTEREST. MOVE ACCT-BALANCE TO WS-BAL. IF WS-BAL > 10000 COMPUTE WS-RATE = 0.035 ELSE COMPUTE WS-RATE = 0.025 END-IF. COMPUTE WS-INTEREST ROUNDED = WS-BAL * WS-RATE / 12. 

Balances above 10,000 earn 3.5% a year and everything else earns 2.5%. The monthly interest is rounded to two decimals.

Two details sit in the declarations instead of the logic. ACCT-BALANCE is signed (the S in the picture clause) and WS-BAL is not. Moving a signed value into an unsigned field stores its absolute value, so a balance of -50.00 becomes 50.00. Also, ROUNDED rounds halves away from zero. Without it, COBOL truncates the result.

A conversion that looks right

Here is a reasonable Java version:

public static BigDecimal monthlyInterest(BigDecimal balance) { BigDecimal rate = balance.compareTo(new BigDecimal("10000")) > 0 ? new BigDecimal("0.035") : new BigDecimal("0.025"); return balance.multiply(rate) .divide(BigDecimal.valueOf(12), 2, RoundingMode.HALF_UP); } 

It uses BigDecimal instead of double, keeps the threshold and both rates, and rounds to two places. A reviewer would approve it. For a balance of -50.00 it returns -0.10, while the COBOL program returns 0.10.

The old behavior may be a bug, or it may be a rule that production depends on. The converted code cannot tell you, and neither can a test generated from the Java. The person who owns the business rule has to decide, and the decision should be written down before cutover.

Take the expected values from the legacy system

Run the original program and record its output for a set of inputs. Those recordings become the expected values. Choose inputs around every boundary: the threshold, zero, a negative balance, the largest value the field can hold, and values where rounding and truncation give different answers.

The values below were worked out from the rules above to keep the post short. On a real project, record them by running the legacy program on the same inputs.

balance,expected 500.00,1.04 800.00,1.67 10000.00,20.83 10000.01,29.17 0.00,0.00 -50.00,0.10 

Some of these rows are there for a reason. 800.00 gives 1.67 rounded and 1.66 truncated, so it catches a missing rounding rule. 10000.00 is not above the threshold, so it stays on the lower rate. 10000.01 is the first balance above it and gives 29.17 (29.16 if truncated).

The test reads that file with JUnit 5:

class MonthlyInterestParityTest { @ParameterizedTest @CsvFileSource(resources = "/golden/interest.csv", numLinesToSkip = 1) void matchesLegacyOutput(String balance, String expected) { BigDecimal actual = InterestCalculator.monthlyInterest(new BigDecimal(balance)); assertEquals(new BigDecimal(expected), actual); } } 

assertEquals on a BigDecimal compares the scale as well as the value, which is what you want here, because 0.1 and 0.10 are different in a fixed-width record.

Run it against the first Java version and one row fails: -50.00 expects 0.10 and gets -0.10. After the business owner confirms the old behavior has to stay, the fix is small:

public static BigDecimal monthlyInterest(BigDecimal signedBalance) { // Legacy behavior, kept on purpose: the sign is dropped. BigDecimal balance = signedBalance.abs(); BigDecimal rate = balance.compareTo(new BigDecimal("10000")) > 0 ? new BigDecimal("0.035") : new BigDecimal("0.025"); return balance.multiply(rate) .divide(BigDecimal.valueOf(12), 2, RoundingMode.HALF_UP); } 

More places a conversion can drift

The sign rule is one case. These are others worth a golden row:

  • Truncation versus rounding. Without ROUNDED, COBOL truncates. Java makes you choose a rounding mode, and using HALF_UP everywhere moves results by a cent in cases like 800.00.
  • Field size. If a result does not fit the receiving field and there is no ON SIZE ERROR clause, many compilers silently drop the high-order digits, though the behavior depends on the compiler. Java returns the larger number. Test the maximum value the field can hold.
  • Intermediate precision. The number of digits kept between a multiply and a divide can change the last digit, and compilers differ. Compare against the output of the compiler you actually run in production.
  • Character order. EBCDIC and ASCII sort differently. In EBCDIC, digits sort after letters, the opposite of ASCII, so a comparison or a sorted report can change order after conversion.

Where AI fits

AI is useful for most of this work. It drafts the Java, lists boundary inputs by reading the code, and writes the test harness. It cannot supply the expected values. A test generated from the converted code will happily expect -0.10.

Google's published account of an AI-assisted migration points the same way. About 80% of the code changes in it were fully AI-authored, and engineers still reviewed each change before it landed. The model handled volume, while people and tests handled correctness. The paper is How is Google using AI for internal code migrations?.

A checklist you can reuse

  1. Record legacy outputs for boundary inputs before you touch the Java.
  2. Keep them in a file the tests read. Never generate expected values from the converted code.
  3. Run both versions on the same inputs and compare field by field.
  4. Sort every difference into a regression or an intended change, and give each one an owner.
  5. Repeat on real traffic in a parallel run before cutover.

We wrote up the wider process, from code analysis through a cutover readiness checklist, in our guide to legacy application modernization with AI. The parity test above is the validate stage of that process.

If a conversion has surprised you with a quirk like these, add it in the comments. I would like to collect more of them.