Date arithmetic for COBOL because every mainframe shop on earth has reinvented this at least twice and at least one of those implementations gets leap years wrong.
Two copybooks. Twenty-three tests. No dependencies. Just COPY it into your program and stop arguing about whether 1900 was a leap year (it wasn't, but Excel thinks it was, and that's a story for another day).
Fourteen date operations covering everything from basic day-of-week calculation through to ISO 8601 week numbers and the day count conventions that bond traders argue about at dinner parties. All of it works in integer arithmetic with COMP-3 fields because floating point has no place near a date calculation and certainly no place near your money.
The Julian Day Number algorithm comes from Fliegel and Van Flandern (1968) for the forward direction and Richards (2013) for the reverse, which is the same lineage NASA used for trajectory calculations. Your batch job probably doesn't need that level of rigour but it certainly doesn't hurt.
Put the copybooks somewhere your compiler can find them and then do this:
WORKING-STORAGE SECTION.
COPY DATEUTIL.
PROCEDURE DIVISION.
MOVE 2026 TO WS-DT-YYYY
MOVE 03 TO WS-DT-MM
MOVE 25 TO WS-DT-DD
PERFORM DU-DAY-OF-WEEK
DISPLAY "Day of week: " WS-DAY-OF-WEEK
MOVE 5 TO WS-DAY-COUNT
PERFORM DU-ADD-BUSINESS-DAYS
DISPLAY "5 biz days later: "
WS-DTR-YYYY "/" WS-DTR-MM "/" WS-DTR-DD
STOP RUN.
COPY DATECALC.The COPY DATEUTIL goes in your WORKING-STORAGE SECTION and brings in all the data fields. The COPY DATECALC goes after your own paragraphs in the PROCEDURE DIVISION and brings in all the calculation paragraphs. Set the input fields, PERFORM the paragraph, read the output fields. If you have written COBOL before this will feel completely natural and if you haven't then I admire your bravery.
| Paragraph | Input | Output | What It Does |
|---|---|---|---|
DU-CHECK-LEAP-YEAR |
WS-DT-YYYY |
WS-IS-LEAP-YEAR |
Checks all four leap year rules including the century exception that most people forget about |
DU-VALIDATE-DATE |
WS-DT-YYYY/MM/DD |
WS-IS-VALID-DATE |
Validates month bounds, day bounds, and leap February correctly |
DU-DAY-OF-WEEK |
WS-DT-YYYY/MM/DD |
WS-DAY-OF-WEEK |
Returns 1 for Monday through 7 for Sunday via Julian Day Number |
DU-DAY-OF-YEAR |
WS-DT-YYYY/MM/DD |
WS-DAY-OF-YEAR |
Returns 1 through 366 because sometimes you just need to know |
| Paragraph | Input | Output | What It Does |
|---|---|---|---|
DU-IS-BUSINESS-DAY |
WS-DT-YYYY/MM/DD |
WS-IS-BUSINESS-DAY |
Monday through Friday returns yes, weekends return no. Does not check public holidays because that is your country's problem and not a copybook's |
DU-ADD-BUSINESS-DAYS |
WS-DT-YYYY/MM/DD, WS-DAY-COUNT |
WS-DATE-RESULT |
Adds N business days, skipping weekends. Handles negative values for going backwards |
DU-BUSINESS-DAYS-CT |
WS-DATE-WORK, WS-DATE-WORK-2 |
WS-BUSINESS-DAYS-CT |
Counts the Monday through Friday days between two dates |
| Paragraph | Input | Output | What It Does |
|---|---|---|---|
DU-DAYS-BETWEEN |
WS-DATE-WORK, WS-DATE-WORK-2 |
WS-DAYS-BETWEEN |
Signed difference in days between two dates, negative if the first is later |
DU-ADD-DAYS |
WS-DT-YYYY/MM/DD, WS-DAY-COUNT |
WS-DATE-RESULT |
Adds or subtracts calendar days, correctly rolling across month and year boundaries |
| Paragraph | Input | Output | What It Does |
|---|---|---|---|
DU-DAY-COUNT-FRAC |
WS-DATE-WORK, WS-DATE-WORK-2, WS-DAY-COUNT |
WS-DAY-COUNT-FRAC |
Year fraction using your choice of day count convention |
DU-LAST-DAY-OF-MONTH |
WS-DT-YYYY, WS-DT-MM |
WS-LAST-DAY-OF-MONTH |
Returns 28, 29, 30, or 31 as appropriate |
DU-IS-END-OF-MONTH |
WS-DT-YYYY/MM/DD |
WS-IS-END-OF-MONTH |
Whether the given date is the last day of its month |
DU-QUARTER |
WS-DT-YYYY, WS-DT-MM |
WS-QUARTER, WS-QUARTER-START, WS-QUARTER-END |
Fiscal quarter number plus the start and end dates of that quarter |
DU-ISO-WEEK |
WS-DT-YYYY/MM/DD |
WS-ISO-WEEK, WS-ISO-YEAR |
ISO 8601 week number and year, which is the standard that occasionally puts January 1st in the previous year's week 53 just to keep things interesting |
Set WS-DAY-COUNT before calling DU-DAY-COUNT-FRAC:
| Code | Convention | Denominator | Used By |
|---|---|---|---|
| 1 | ACT/ACT | 365 or 366 | US Treasury bonds |
| 2 | ACT/360 | 360 | Money markets, LIBOR |
| 3 | ACT/365 | 365 (fixed) | UK gilts, Japanese bonds |
| 4 | 30/360 | 360 | US corporate bonds, mortgages |
| 5 | 30/365 | 365 | Rare but it exists and someone will need it |
The test suite runs on GnuCOBOL and should run on any COBOL-85 compliant compiler including IBM Enterprise COBOL on z/OS and Micro Focus. On GnuCOBOL:
cobc -x -I src -o testdate test/TESTDATE.cbl
./testdateYou should see 23 tests pass and 0 tests fail. If you see anything else then something has gone wrong and I would like to hear about it.
src/
DATEUTIL.cpy Data definitions (WORKING-STORAGE)
DATECALC.cpy Calculation paragraphs (PROCEDURE DIVISION)
test/
TESTDATE.cbl 23-test verification suite
Tested on GnuCOBOL 3.x. The copybooks use COBOL-85 syntax with FUNCTION intrinsics (MOD, INTEGER) which are supported by every modern COBOL compiler. The only thing that might trip you up is if your compiler doesn't support FUNCTION MOD, in which case you have a compiler from before 1985 and honestly that's impressive in its own right.
Should work on z/OS with IBM Enterprise COBOL, z390, Micro Focus, and anything else that implements the COBOL-85 standard. If you run into compatibility issues please open an issue and tell me what compiler you're using so I can add a workaround.
I got tired of seeing the same date arithmetic bugs in every mainframe codebase I touched. February 29th in non-leap years being accepted as valid. Day-of-week calculations that are off by one because someone used the wrong flavour of Zeller's congruence. Business day logic that counts Saturday as a working day in some obscure code path that only triggers on the last day of the quarter during a leap year.
The Mesopotamians managed a calendar with twelve months of thirty days each five thousand years ago and they didn't even have COMP-3 fields. We can do better than this.
Apache 2.0.