> "If you want to understand why COBOL developers are careful, methodical, and slightly paranoid about testing, ask them about Y2K. We rewrote the world's date processing in two years — and it worked." — James Okafor, Team Lead, MedClaim
In This Chapter
- 21.1 The Y2K Problem: Lessons Learned
- 21.2 Date Formats in COBOL
- 21.3 FUNCTION CURRENT-DATE
- 21.4 Date Arithmetic with Intrinsic Functions
- 21.5 Date Validation Techniques
- 21.6 Language Environment Date Services
- 21.7 Date Aging and Difference Calculations
- 21.8 End-of-Month and Month-End Processing
- 21.9 Timezone Handling
- 21.9 Business Day Calculations
- 21.10 Counting Business Days Between Two Dates
- 21.11 Date Processing in Batch Windows
- 21.12 Date Formatting and Display
- 21.11 Date Ranges and Overlap Detection
- 21.12 GlobalBank Case Study: Statement Period Calculations
- 21.11 MedClaim Case Study: Timely Filing Validation
- 21.12 Y2K Historical Timeline
- 21.13 The Student Mainframe Lab
- 21.14 Complete Worked Example: Date Processing Utility Subprogram
- 21.16 Date Processing Testing Strategies
- 21.17 Common Date Processing Mistakes
- 21.15 Chapter Summary
Chapter 21: Date and Time Processing
"If you want to understand why COBOL developers are careful, methodical, and slightly paranoid about testing, ask them about Y2K. We rewrote the world's date processing in two years — and it worked." — James Okafor, Team Lead, MedClaim
Date and time processing is one of the most error-prone areas in all of programming. Leap years, month-end boundaries, timezone conversions, daylight saving time transitions, fiscal year calculations, business day exclusions — every one of these has been the root cause of production failures in financial and healthcare systems. And underlying all of these is the defining event in COBOL's modern history: the Y2K crisis.
In this chapter, we build a comprehensive understanding of date processing in COBOL. We start with the historical context of Y2K and the lessons it taught. We then cover COBOL's date formats, the intrinsic functions for date arithmetic, Language Environment date services, validation techniques, business day calculations, and timezone handling. Throughout, we apply these concepts to realistic problems at GlobalBank (transaction dates, statement periods, aging reports) and MedClaim (filing deadlines, claim aging, timely filing validation).
21.1 The Y2K Problem: Lessons Learned
Historical Context
From its creation in 1959 until the late 1990s, the vast majority of COBOL programs stored years as two digits. A date like January 15, 1985 was stored as 850115. This was not laziness — it was practical: storage was extraordinarily expensive in the 1960s and 1970s, and two bytes per date field across millions of records added up. No one designing systems in 1965 imagined they would still be running in 2000.
As the year 2000 approached, the problem became clear: a 2-digit year of "00" would be interpreted as 1900, not 2000. Date comparisons would fail (is "00" greater than or less than "99"?), date arithmetic would produce negative results, and sorting by date would produce reversed sequences.
The Scale of Y2K
The Y2K remediation effort was staggering: - Estimated global cost: $300-600 billion - Lines of COBOL code analyzed: Over 200 billion - COBOL programmers recruited: Thousands of retired programmers returned to the workforce - Timeline: Most organizations began in 1996-1997 and worked through December 31, 1999
Two Approaches: Windowing vs. Expansion
Two primary strategies emerged:
Date windowing preserved the 2-digit year but applied a "pivot year" rule: if the year is >= 50, it belongs to the 1900s; if < 50, it belongs to the 2000s. This was faster to implement but created a new expiration date (2050).
*> Windowing approach
IF WS-2DIGIT-YEAR >= 50
MOVE 19 TO WS-CENTURY
ELSE
MOVE 20 TO WS-CENTURY
END-IF
STRING WS-CENTURY WS-2DIGIT-YEAR
DELIMITED BY SIZE INTO WS-4DIGIT-YEAR
Date expansion converted all date fields from 2 digits to 4 digits. This was the permanent fix but required changing file layouts, database schemas, copybooks, program logic, and every interface — a massive undertaking.
*> Before Y2K:
01 WS-DATE PIC 9(6). *> YYMMDD
*> After Y2K expansion:
01 WS-DATE PIC 9(8). *> YYYYMMDD
🔴 Critical Lesson: Date windowing is still in production at many organizations. Some systems use pivot years of 2040 or 2050. If you encounter windowing code, it has an expiration date. Plan for expansion before the window closes.
Legacy of Y2K
The Y2K crisis left several lasting impacts on COBOL development:
- 4-digit years are now standard. Every new COBOL program uses YYYYMMDD.
- Date validation became mandatory. Never trust a date without validating it.
- Testing discipline improved. Y2K forced rigorous testing practices.
- COBOL's importance was confirmed. The crisis proved that COBOL systems are essential infrastructure.
- Documentation matters. Many Y2K bugs were found in undocumented date assumptions.
21.2 Date Formats in COBOL
COBOL programs encounter several date formats. Understanding each is essential.
Gregorian Date: YYYYMMDD
The standard format for modern COBOL programs:
01 WS-GREG-DATE.
05 WS-GD-YEAR PIC 9(4).
05 WS-GD-MONTH PIC 9(2).
05 WS-GD-DAY PIC 9(2).
Julian Date: YYYYDDD
The year followed by the day-of-year (1-366):
01 WS-JULIAN-DATE.
05 WS-JD-YEAR PIC 9(4).
05 WS-JD-DAY PIC 9(3).
Julian dates are used in some mainframe scheduling systems and file naming conventions.
Packed Date
Dates stored in COMP-3 (packed decimal) format to save storage:
01 WS-PACKED-DATE PIC 9(8) COMP-3.
*> Occupies 5 bytes instead of 8
Integer Date (Lilian Date)
The number of days since a base date. COBOL's intrinsic functions use the Gregorian calendar epoch (day 1 = January 1, 1601):
01 WS-INT-DATE PIC 9(7).
COMPUTE WS-INT-DATE =
FUNCTION INTEGER-OF-DATE(20240615)
*> Returns a large integer representing June 15, 2024
IBM Lilian Date
IBM's Language Environment uses October 15, 1582 (the start of the Gregorian calendar) as day 1. This differs from the COBOL intrinsic function epoch. Be aware of which system you are using.
CICS ABSTIME
CICS uses a packed decimal timestamp representing the number of milliseconds since January 1, 1900:
01 WS-ABSTIME PIC S9(15) COMP-3.
Timestamps and Date-Time Formats
Full timestamps include both date and time:
01 WS-TIMESTAMP.
05 WS-TS-DATE PIC 9(8). *> YYYYMMDD
05 WS-TS-TIME PIC 9(6). *> HHMMSS
05 WS-TS-FRAC PIC 9(6). *> Microseconds
DB2 timestamps use the format YYYY-MM-DD-HH.MM.SS.FFFFFF.
21.3 FUNCTION CURRENT-DATE
The CURRENT-DATE function is the primary way to obtain the current date and time in COBOL. It returns a 21-character alphanumeric string:
01 WS-CURRENT-DT.
05 WS-CD-DATE.
10 WS-CD-YEAR PIC 9(4). *> Pos 1-4
10 WS-CD-MONTH PIC 9(2). *> Pos 5-6
10 WS-CD-DAY PIC 9(2). *> Pos 7-8
05 WS-CD-TIME.
10 WS-CD-HOURS PIC 9(2). *> Pos 9-10
10 WS-CD-MINUTES PIC 9(2). *> Pos 11-12
10 WS-CD-SECONDS PIC 9(2). *> Pos 13-14
10 WS-CD-HUNDREDTHS PIC 9(2). *> Pos 15-16
05 WS-CD-GMT-OFFSET.
10 WS-CD-GMT-SIGN PIC X(1). *> Pos 17
10 WS-CD-GMT-HOURS PIC 9(2). *> Pos 18-19
10 WS-CD-GMT-MINS PIC 9(2). *> Pos 20-21
PROCEDURE DIVISION.
MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DT
DISPLAY "TODAY: " WS-CD-YEAR "-"
WS-CD-MONTH "-" WS-CD-DAY
DISPLAY "TIME: " WS-CD-HOURS ":"
WS-CD-MINUTES ":" WS-CD-SECONDS
DISPLAY "GMT OFFSET: " WS-CD-GMT-SIGN
WS-CD-GMT-HOURS ":" WS-CD-GMT-MINS
Positions Explained
| Positions | Content | Example |
|---|---|---|
| 1-4 | Year (4 digits) | 2024 |
| 5-6 | Month (01-12) | 06 |
| 7-8 | Day (01-31) | 15 |
| 9-10 | Hours (00-23) | 14 |
| 11-12 | Minutes (00-59) | 30 |
| 13-14 | Seconds (00-59) | 45 |
| 15-16 | Hundredths of seconds | 12 |
| 17 | GMT sign (+/-) | - |
| 18-19 | GMT offset hours | 05 |
| 20-21 | GMT offset minutes | 00 |
A value of 202406151430451200500 means June 15, 2024 at 14:30:45.12, 5 hours behind GMT (US Eastern Daylight Time).
Using CURRENT-DATE for Timing
01 WS-START-TIME PIC X(21).
01 WS-END-TIME PIC X(21).
01 WS-ELAPSED PIC 9(7).
MOVE FUNCTION CURRENT-DATE TO WS-START-TIME
PERFORM 3000-PROCESS-BATCH
MOVE FUNCTION CURRENT-DATE TO WS-END-TIME
*> Compute elapsed seconds (simplified)
COMPUTE WS-ELAPSED =
(FUNCTION NUMVAL(WS-END-TIME(9:2)) * 3600
+ FUNCTION NUMVAL(WS-END-TIME(11:2)) * 60
+ FUNCTION NUMVAL(WS-END-TIME(13:2)))
- (FUNCTION NUMVAL(WS-START-TIME(9:2)) * 3600
+ FUNCTION NUMVAL(WS-START-TIME(11:2)) * 60
+ FUNCTION NUMVAL(WS-START-TIME(13:2)))
DISPLAY "ELAPSED: " WS-ELAPSED " SECONDS"
21.4 Date Arithmetic with Intrinsic Functions
COBOL's date arithmetic relies on converting calendar dates to integer day numbers, performing arithmetic on the integers, and converting back.
INTEGER-OF-DATE: Calendar to Integer
01 WS-DATE-1 PIC 9(8) VALUE 20240101.
01 WS-INT-1 PIC 9(7).
COMPUTE WS-INT-1 =
FUNCTION INTEGER-OF-DATE(WS-DATE-1)
The function accepts a date in YYYYMMDD format and returns an integer. Day 1 in the COBOL system is January 1, 1601.
DATE-OF-INTEGER: Integer to Calendar
01 WS-NEW-DATE PIC 9(8).
COMPUTE WS-NEW-DATE =
FUNCTION DATE-OF-INTEGER(WS-INT-1 + 90)
*> Adds 90 days to the original date
Days Between Two Dates
01 WS-DATE-FROM PIC 9(8) VALUE 20240101.
01 WS-DATE-TO PIC 9(8) VALUE 20240615.
01 WS-DAYS-DIFF PIC S9(5).
COMPUTE WS-DAYS-DIFF =
FUNCTION INTEGER-OF-DATE(WS-DATE-TO)
- FUNCTION INTEGER-OF-DATE(WS-DATE-FROM)
DISPLAY "DAYS BETWEEN: " WS-DAYS-DIFF
*> DAYS BETWEEN: 166
Adding/Subtracting Days
*> Add 30 days
COMPUTE WS-FUTURE-DATE =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(WS-START-DATE)
+ 30)
*> Subtract 7 days
COMPUTE WS-WEEK-AGO =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(WS-TODAY)
- 7)
Julian Date Functions
For Julian dates (YYYYDDD), use INTEGER-OF-DAY and DAY-OF-INTEGER:
COMPUTE WS-JULIAN-INT =
FUNCTION INTEGER-OF-DAY(2024166)
*> Day 166 of 2024
COMPUTE WS-JULIAN-DATE =
FUNCTION DAY-OF-INTEGER(WS-JULIAN-INT)
Converting Between Gregorian and Julian
*> Gregorian to Julian
COMPUTE WS-JULIAN =
FUNCTION DAY-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(WS-GREG-DATE))
*> Julian to Gregorian
COMPUTE WS-GREG =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DAY(WS-JULIAN-DATE))
21.5 Date Validation Techniques
Invalid dates are a constant source of production problems. COBOL does not automatically validate dates — you must do it explicitly.
Basic Validation with Intrinsic Functions
The simplest validation approach exploits the fact that INTEGER-OF-DATE causes a runtime error for invalid dates. By wrapping it in a defensive check:
01 WS-VALID-FLAG PIC X(1).
88 WS-DATE-VALID VALUE "Y".
88 WS-DATE-INVALID VALUE "N".
VALIDATE-DATE.
SET WS-DATE-VALID TO TRUE
* Check numeric
IF WS-INPUT-DATE IS NOT NUMERIC
SET WS-DATE-INVALID TO TRUE
EXIT PARAGRAPH
END-IF
* Check year range
IF WS-INPUT-DATE(1:4) < "1900"
OR WS-INPUT-DATE(1:4) > "2099"
SET WS-DATE-INVALID TO TRUE
EXIT PARAGRAPH
END-IF
* Check month range
IF WS-INPUT-DATE(5:2) < "01"
OR WS-INPUT-DATE(5:2) > "12"
SET WS-DATE-INVALID TO TRUE
EXIT PARAGRAPH
END-IF
* Check day range (basic)
IF WS-INPUT-DATE(7:2) < "01"
OR WS-INPUT-DATE(7:2) > "31"
SET WS-DATE-INVALID TO TRUE
EXIT PARAGRAPH
END-IF
* Check month/day validity using a table
PERFORM VALIDATE-MONTH-DAY
.
Comprehensive Month-Day Validation
01 DAYS-IN-MONTH-TABLE.
05 FILLER PIC 99 VALUE 31. *> Jan
05 FILLER PIC 99 VALUE 28. *> Feb (non-leap)
05 FILLER PIC 99 VALUE 31. *> Mar
05 FILLER PIC 99 VALUE 30. *> Apr
05 FILLER PIC 99 VALUE 31. *> May
05 FILLER PIC 99 VALUE 30. *> Jun
05 FILLER PIC 99 VALUE 31. *> Jul
05 FILLER PIC 99 VALUE 31. *> Aug
05 FILLER PIC 99 VALUE 30. *> Sep
05 FILLER PIC 99 VALUE 31. *> Oct
05 FILLER PIC 99 VALUE 30. *> Nov
05 FILLER PIC 99 VALUE 31. *> Dec
01 DAYS-TABLE REDEFINES DAYS-IN-MONTH-TABLE.
05 MAX-DAYS PIC 99 OCCURS 12 TIMES.
01 WS-INPUT-YEAR PIC 9(4).
01 WS-INPUT-MONTH PIC 9(2).
01 WS-INPUT-DAY PIC 9(2).
01 WS-MAX-DAY PIC 99.
VALIDATE-MONTH-DAY.
MOVE WS-INPUT-DATE(1:4) TO WS-INPUT-YEAR
MOVE WS-INPUT-DATE(5:2) TO WS-INPUT-MONTH
MOVE WS-INPUT-DATE(7:2) TO WS-INPUT-DAY
MOVE MAX-DAYS(WS-INPUT-MONTH) TO WS-MAX-DAY
* Adjust February for leap year
IF WS-INPUT-MONTH = 2
PERFORM CHECK-LEAP-YEAR
IF WS-IS-LEAP-YEAR
MOVE 29 TO WS-MAX-DAY
END-IF
END-IF
IF WS-INPUT-DAY > WS-MAX-DAY
SET WS-DATE-INVALID TO TRUE
END-IF
.
CHECK-LEAP-YEAR.
SET WS-NOT-LEAP-YEAR TO TRUE
IF FUNCTION MOD(WS-INPUT-YEAR, 4) = ZERO
SET WS-IS-LEAP-YEAR TO TRUE
IF FUNCTION MOD(WS-INPUT-YEAR, 100)
= ZERO
SET WS-NOT-LEAP-YEAR TO TRUE
IF FUNCTION MOD(WS-INPUT-YEAR, 400)
= ZERO
SET WS-IS-LEAP-YEAR TO TRUE
END-IF
END-IF
END-IF
.
⚠️ The Leap Year Rule: A year is a leap year if it is divisible by 4, EXCEPT if it is divisible by 100, EXCEPT if it is also divisible by 400. Thus: 2024 is a leap year (divisible by 4), 1900 was NOT a leap year (divisible by 100 but not 400), and 2000 WAS a leap year (divisible by 400). Getting this wrong is the most common date validation bug.
TEST-DATE-YYYYMMDD (IBM Extension)
IBM Enterprise COBOL provides the FUNCTION TEST-DATE-YYYYMMDD intrinsic function:
IF FUNCTION TEST-DATE-YYYYMMDD(WS-INPUT-DATE)
= ZERO
SET WS-DATE-VALID TO TRUE
ELSE
SET WS-DATE-INVALID TO TRUE
END-IF
This handles all validation including leap year checking in a single call. However, it is an IBM extension not available on all compilers.
21.6 Language Environment Date Services
On IBM z/OS systems, the Language Environment (LE) provides callable date services that offer additional capabilities beyond the COBOL intrinsic functions.
CEECBLDY — Convert Date to Lilian
CALL "CEECBLDY" USING
WS-DATE-STRING
WS-DATE-FORMAT
WS-LILIAN-DATE
WS-FEEDBACK-CODE
CEEDATM — Convert Lilian to Formatted Date
CALL "CEEDATM" USING
WS-LILIAN-DATE
WS-OUTPUT-FORMAT
WS-FORMATTED-DATE
WS-FEEDBACK-CODE
Common LE Date Format Strings
| Format | Meaning | Example |
|---|---|---|
| YYYYMMDD | Standard numeric | 20240615 |
| YYYY-MM-DD | ISO with dashes | 2024-06-15 |
| MM/DD/YYYY | US format | 06/15/2024 |
| DD-MMM-YYYY | International | 15-JUN-2024 |
| YYYYDDD | Julian | 2024167 |
💡 When to Use LE Date Services: Use intrinsic functions for basic date arithmetic (they are faster and more portable). Use LE date services when you need formatted output (e.g., "June 15, 2024"), when you need to parse dates in various input formats, or when interfacing with other LE-compatible languages.
21.7 Date Aging and Difference Calculations
Aging calculations determine how old something is — a claim, an account receivable, a pending transaction. They are fundamental to financial and healthcare systems.
Simple Aging
COMPUTE-AGE-IN-DAYS.
MOVE FUNCTION CURRENT-DATE(1:8)
TO WS-TODAY
COMPUTE WS-AGE-DAYS =
FUNCTION INTEGER-OF-DATE(WS-TODAY)
- FUNCTION INTEGER-OF-DATE(WS-ITEM-DATE)
.
Aging Buckets
Both GlobalBank and MedClaim use aging buckets to categorize overdue items:
01 WS-AGING-BUCKETS.
05 WS-BUCKET-CURRENT PIC 9(9)V99.
05 WS-BUCKET-30 PIC 9(9)V99.
05 WS-BUCKET-60 PIC 9(9)V99.
05 WS-BUCKET-90 PIC 9(9)V99.
05 WS-BUCKET-OVER-90 PIC 9(9)V99.
01 WS-AGING-COUNTS.
05 WS-COUNT-CURRENT PIC 9(7).
05 WS-COUNT-30 PIC 9(7).
05 WS-COUNT-60 PIC 9(7).
05 WS-COUNT-90 PIC 9(7).
05 WS-COUNT-OVER-90 PIC 9(7).
ASSIGN-AGING-BUCKET.
EVALUATE TRUE
WHEN WS-AGE-DAYS <= 30
ADD WS-ITEM-AMOUNT
TO WS-BUCKET-CURRENT
ADD 1 TO WS-COUNT-CURRENT
WHEN WS-AGE-DAYS <= 60
ADD WS-ITEM-AMOUNT
TO WS-BUCKET-30
ADD 1 TO WS-COUNT-30
WHEN WS-AGE-DAYS <= 90
ADD WS-ITEM-AMOUNT
TO WS-BUCKET-60
ADD 1 TO WS-COUNT-60
WHEN WS-AGE-DAYS <= 120
ADD WS-ITEM-AMOUNT
TO WS-BUCKET-90
ADD 1 TO WS-COUNT-90
WHEN OTHER
ADD WS-ITEM-AMOUNT
TO WS-BUCKET-OVER-90
ADD 1 TO WS-COUNT-OVER-90
END-EVALUATE
.
Month-Based Aging
Some aging calculations require month differences rather than day counts:
COMPUTE-MONTH-AGE.
MOVE WS-TODAY(1:4) TO WS-TODAY-YEAR
MOVE WS-TODAY(5:2) TO WS-TODAY-MONTH
MOVE WS-ITEM-DATE(1:4) TO WS-ITEM-YEAR
MOVE WS-ITEM-DATE(5:2) TO WS-ITEM-MONTH
COMPUTE WS-MONTH-AGE =
(WS-TODAY-YEAR - WS-ITEM-YEAR) * 12
+ (WS-TODAY-MONTH - WS-ITEM-MONTH)
.
21.8 End-of-Month and Month-End Processing
Month-end processing is one of the most critical date operations in financial systems. Determining the last day of a month, the first day of the next month, and handling the transition between months correctly affects everything from interest calculations to regulatory reporting.
Computing the Last Day of Any Month
The most reliable way to find the last day of a month is to compute the first day of the next month and subtract one day:
01 WS-LAST-DAY-RESULT PIC 9(8).
01 WS-FIRST-NEXT PIC 9(8).
01 WS-WORK-YEAR PIC 9(4).
01 WS-WORK-MONTH PIC 9(2).
COMPUTE-LAST-DAY-OF-MONTH.
* Input: WS-WORK-YEAR and WS-WORK-MONTH
* Output: WS-LAST-DAY-RESULT
* Build first day of next month
IF WS-WORK-MONTH = 12
COMPUTE WS-FIRST-NEXT =
(WS-WORK-YEAR + 1) * 10000 + 0101
ELSE
COMPUTE WS-FIRST-NEXT =
WS-WORK-YEAR * 10000
+ (WS-WORK-MONTH + 1) * 100 + 01
END-IF
* Subtract one day
COMPUTE WS-LAST-DAY-RESULT =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(
WS-FIRST-NEXT) - 1)
.
This approach is elegant because it works for all months and all years without any lookup tables or leap year checks — the intrinsic functions handle everything.
End-of-Month Interest Calculation
GlobalBank calculates daily interest and posts it on the last day of each month. The calculation must use the actual number of days in the month for the daily rate:
COMPUTE-MONTHLY-INTEREST.
* Determine days in current month
MOVE WS-ACCT-YEAR TO WS-WORK-YEAR
MOVE WS-ACCT-MONTH TO WS-WORK-MONTH
PERFORM COMPUTE-LAST-DAY-OF-MONTH
COMPUTE WS-DAYS-IN-MONTH =
WS-LAST-DAY-RESULT -
(WS-WORK-YEAR * 10000
+ WS-WORK-MONTH * 100) + 1
* Hmm, that's tricky. Better approach:
COMPUTE WS-FIRST-OF-MONTH =
WS-WORK-YEAR * 10000
+ WS-WORK-MONTH * 100 + 01
COMPUTE WS-DAYS-IN-MONTH =
FUNCTION INTEGER-OF-DATE(
WS-LAST-DAY-RESULT)
- FUNCTION INTEGER-OF-DATE(
WS-FIRST-OF-MONTH) + 1
* Daily interest = balance * annual rate / 365
COMPUTE WS-DAILY-RATE =
WS-ANNUAL-RATE / 365
* Monthly interest = balance * daily rate * days
COMPUTE WS-MONTHLY-INTEREST =
WS-ACCT-BALANCE * WS-DAILY-RATE
* WS-DAYS-IN-MONTH
DISPLAY "MONTH: " WS-WORK-MONTH
" DAYS: " WS-DAYS-IN-MONTH
" INTEREST: " WS-MONTHLY-INTEREST
.
💡 Actual/365 vs. 30/360: The calculation above uses the Actual/365 day count convention — actual days in the month divided by 365 days in the year. Some financial products use the 30/360 convention where every month is assumed to have 30 days and the year has 360 days. The convention used must match the product's legal documentation. GlobalBank uses Actual/365 for savings accounts and 30/360 for mortgages.
Month-End Batch Scheduling
At GlobalBank, different processes run on different month-end dates:
01 WS-ME-DATES.
05 WS-ME-LAST-BUS-DAY PIC 9(8).
05 WS-ME-LAST-CAL-DAY PIC 9(8).
05 WS-ME-FIRST-NEXT PIC 9(8).
COMPUTE-MONTH-END-DATES.
* Last calendar day of month
PERFORM COMPUTE-LAST-DAY-OF-MONTH
MOVE WS-LAST-DAY-RESULT TO WS-ME-LAST-CAL-DAY
* Last business day of month
* (back up from last calendar day until
* we find a business day)
MOVE WS-ME-LAST-CAL-DAY TO WS-ME-LAST-BUS-DAY
MOVE WS-ME-LAST-BUS-DAY TO WS-INPUT-DATE
PERFORM IS-WEEKEND
PERFORM IS-HOLIDAY
PERFORM UNTIL WS-IS-BUSINESS-DAY
AND WS-NOT-HOLIDAY
COMPUTE WS-ME-LAST-BUS-DAY =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(
WS-ME-LAST-BUS-DAY) - 1)
MOVE WS-ME-LAST-BUS-DAY TO WS-INPUT-DATE
PERFORM IS-WEEKEND
PERFORM IS-HOLIDAY
END-PERFORM
* First day of next month
IF WS-WORK-MONTH = 12
COMPUTE WS-ME-FIRST-NEXT =
(WS-WORK-YEAR + 1) * 10000 + 0101
ELSE
COMPUTE WS-ME-FIRST-NEXT =
WS-WORK-YEAR * 10000
+ (WS-WORK-MONTH + 1) * 100 + 01
END-IF
DISPLAY "LAST CAL DAY: " WS-ME-LAST-CAL-DAY
DISPLAY "LAST BUS DAY: " WS-ME-LAST-BUS-DAY
DISPLAY "FIRST NEXT MO: " WS-ME-FIRST-NEXT
.
⚖️ Theme: Defensive Programming. Month-end processing is when most date-related production failures occur. The last day of February, the transition from December to January, and months where the last day falls on a weekend are all high-risk scenarios. Maria Chen's team runs a "month-end rehearsal" on the 25th of each month, processing the full month-end batch against a copy of production data to catch any date-related issues before the actual month-end.
Period-Over-Period Comparison Dates
For reporting, you often need "same day last month" or "same day last year." These are trickier than they seem because months have different lengths:
COMPUTE-SAME-DAY-LAST-MONTH.
* Input: WS-REFERENCE-DATE (YYYYMMDD)
* Output: WS-LAST-MONTH-DATE
MOVE WS-REFERENCE-DATE(1:4)
TO WS-WORK-YEAR
MOVE WS-REFERENCE-DATE(5:2)
TO WS-WORK-MONTH
MOVE WS-REFERENCE-DATE(7:2)
TO WS-WORK-DAY
* Go back one month
IF WS-WORK-MONTH = 1
MOVE 12 TO WS-WORK-MONTH
SUBTRACT 1 FROM WS-WORK-YEAR
ELSE
SUBTRACT 1 FROM WS-WORK-MONTH
END-IF
* Adjust day if it exceeds last day of that month
PERFORM COMPUTE-LAST-DAY-OF-MONTH
MOVE WS-LAST-DAY-RESULT(7:2)
TO WS-MAX-DAY
IF WS-WORK-DAY > WS-MAX-DAY
MOVE WS-MAX-DAY TO WS-WORK-DAY
END-IF
COMPUTE WS-LAST-MONTH-DATE =
WS-WORK-YEAR * 10000
+ WS-WORK-MONTH * 100
+ WS-WORK-DAY
.
This correctly handles the case where the reference date is March 31 and "same day last month" would be February 31 (which does not exist) — it adjusts to February 28 (or 29 in a leap year).
21.9 Timezone Handling
In a global banking system, transactions occur across time zones. COBOL's CURRENT-DATE provides the GMT offset, enabling timezone calculations.
Extracting the GMT Offset
MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DT
*> WS-CD-GMT-SIGN is "+" or "-"
*> WS-CD-GMT-HOURS is the hour offset
*> WS-CD-GMT-MINS is the minute offset
EVALUATE WS-CD-GMT-SIGN
WHEN "-"
COMPUTE WS-GMT-OFFSET-MINS =
(WS-CD-GMT-HOURS * 60
+ WS-CD-GMT-MINS) * -1
WHEN "+"
COMPUTE WS-GMT-OFFSET-MINS =
WS-CD-GMT-HOURS * 60
+ WS-CD-GMT-MINS
END-EVALUATE
Converting Local Time to GMT
CONVERT-TO-GMT.
* Get local time
MOVE FUNCTION CURRENT-DATE TO WS-CURRENT-DT
* Convert to minutes since midnight
COMPUTE WS-LOCAL-MINS =
WS-CD-HOURS * 60 + WS-CD-MINUTES
* Subtract GMT offset to get GMT
COMPUTE WS-GMT-MINS =
WS-LOCAL-MINS - WS-GMT-OFFSET-MINS
* Handle day boundary
IF WS-GMT-MINS < 0
ADD 1440 TO WS-GMT-MINS
*> Subtract 1 day from date
COMPUTE WS-GMT-DATE =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(
WS-TODAY) - 1)
ELSE IF WS-GMT-MINS >= 1440
SUBTRACT 1440 FROM WS-GMT-MINS
*> Add 1 day to date
COMPUTE WS-GMT-DATE =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(
WS-TODAY) + 1)
ELSE
MOVE WS-TODAY TO WS-GMT-DATE
END-IF
COMPUTE WS-GMT-HOURS =
FUNCTION INTEGER-PART(
WS-GMT-MINS / 60)
COMPUTE WS-GMT-MINUTES =
FUNCTION MOD(WS-GMT-MINS, 60)
.
⚖️ The Modernization Spectrum: Timezone handling is a perfect example of how COBOL programs must interact with modern global systems. While COBOL's CURRENT-DATE provides the raw offset, production systems often interface with timezone databases (the IANA/Olson database) through web service calls or shared data areas. The COBOL program provides the business logic; the timezone infrastructure provides the geographic intelligence.
21.9 Business Day Calculations
Financial systems frequently need to determine business days — excluding weekends and holidays.
Day-of-Week Calculation
Using INTEGER-OF-DATE and MOD to determine the day of the week:
01 WS-DOW PIC 9.
88 WS-IS-MONDAY VALUE 1.
88 WS-IS-TUESDAY VALUE 2.
88 WS-IS-WEDNESDAY VALUE 3.
88 WS-IS-THURSDAY VALUE 4.
88 WS-IS-FRIDAY VALUE 5.
88 WS-IS-SATURDAY VALUE 6.
88 WS-IS-SUNDAY VALUE 7.
GET-DAY-OF-WEEK.
* COBOL integer dates: day 1 (1601-01-01)
* was a Monday
COMPUTE WS-DOW =
FUNCTION MOD(
FUNCTION INTEGER-OF-DATE(WS-DATE)
- 1, 7) + 1
.
💡 Calibration Note: The day-of-week calculation depends on what day of the week corresponds to the epoch. Verify by testing with a known date (e.g., January 1, 2024 was a Monday). Adjust the formula if your compiler's epoch differs.
Checking for Weekends
IS-WEEKEND.
PERFORM GET-DAY-OF-WEEK
IF WS-IS-SATURDAY OR WS-IS-SUNDAY
SET WS-IS-NON-BUSINESS-DAY TO TRUE
ELSE
SET WS-IS-BUSINESS-DAY TO TRUE
END-IF
.
Holiday Table
01 HOLIDAY-TABLE-VALUES.
05 FILLER PIC 9(8) VALUE 20240101. *> New Year
05 FILLER PIC 9(8) VALUE 20240115. *> MLK Day
05 FILLER PIC 9(8) VALUE 20240219. *> Presidents
05 FILLER PIC 9(8) VALUE 20240527. *> Memorial
05 FILLER PIC 9(8) VALUE 20240619. *> Juneteenth
05 FILLER PIC 9(8) VALUE 20240704. *> Independence
05 FILLER PIC 9(8) VALUE 20240902. *> Labor Day
05 FILLER PIC 9(8) VALUE 20241014. *> Columbus
05 FILLER PIC 9(8) VALUE 20241111. *> Veterans
05 FILLER PIC 9(8) VALUE 20241128. *> Thanksgiving
05 FILLER PIC 9(8) VALUE 20241225. *> Christmas
01 HOLIDAY-TABLE REDEFINES HOLIDAY-TABLE-VALUES.
05 HOLIDAY-DATE PIC 9(8) OCCURS 11 TIMES
ASCENDING KEY IS HOLIDAY-DATE
INDEXED BY HOL-IDX.
IS-HOLIDAY.
SET WS-NOT-HOLIDAY TO TRUE
SEARCH ALL HOLIDAY-DATE
AT END
SET WS-NOT-HOLIDAY TO TRUE
WHEN HOLIDAY-DATE(HOL-IDX) = WS-DATE
SET WS-IS-HOLIDAY TO TRUE
END-SEARCH
.
Adding Business Days
ADD-BUSINESS-DAYS.
* Add WS-BUS-DAYS business days to WS-START-DATE
* Result in WS-RESULT-DATE
MOVE WS-START-DATE TO WS-RESULT-DATE
MOVE ZERO TO WS-BUS-DAYS-COUNTED
PERFORM UNTIL WS-BUS-DAYS-COUNTED >=
WS-BUS-DAYS-TO-ADD
* Advance one calendar day
COMPUTE WS-RESULT-DATE =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(
WS-RESULT-DATE) + 1)
* Check if this is a business day
MOVE WS-RESULT-DATE TO WS-DATE
PERFORM IS-WEEKEND
IF WS-IS-BUSINESS-DAY
PERFORM IS-HOLIDAY
IF WS-NOT-HOLIDAY
ADD 1 TO WS-BUS-DAYS-COUNTED
END-IF
END-IF
END-PERFORM
.
Quarter and Fiscal Year Calculations
DETERMINE-QUARTER.
COMPUTE WS-QUARTER =
FUNCTION INTEGER(
(WS-INPUT-MONTH - 1) / 3) + 1
.
DETERMINE-FISCAL-QUARTER.
* Fiscal year starts October 1
IF WS-INPUT-MONTH >= 10
COMPUTE WS-FISCAL-QUARTER =
FUNCTION INTEGER(
(WS-INPUT-MONTH - 10) / 3) + 1
COMPUTE WS-FISCAL-YEAR =
WS-INPUT-YEAR + 1
ELSE
COMPUTE WS-FISCAL-QUARTER =
FUNCTION INTEGER(
(WS-INPUT-MONTH + 2) / 3)
MOVE WS-INPUT-YEAR TO WS-FISCAL-YEAR
END-IF
.
21.10 Counting Business Days Between Two Dates
While the previous section showed how to add business days to a date, another common requirement is counting business days between two dates. This is critical for SLA calculations (how many business days has a claim been open?), regulatory reporting (report must be filed within 5 business days), and financial settlement (T+2 settlement counting).
The Algorithm
Counting business days between two dates is straightforward but requires care:
01 WS-BUS-DAY-COUNT PIC 9(5).
01 WS-WALK-DATE PIC 9(8).
01 WS-WALK-INT PIC 9(7).
01 WS-END-INT PIC 9(7).
01 WS-WALK-DOW PIC 9.
COUNT-BUSINESS-DAYS.
* Count business days from WS-START-DATE
* to WS-END-DATE (inclusive of start, exclusive
* of end, per financial convention)
MOVE ZERO TO WS-BUS-DAY-COUNT
COMPUTE WS-WALK-INT =
FUNCTION INTEGER-OF-DATE(WS-START-DATE)
COMPUTE WS-END-INT =
FUNCTION INTEGER-OF-DATE(WS-END-DATE)
PERFORM UNTIL WS-WALK-INT >= WS-END-INT
* Convert back to calendar date for checks
COMPUTE WS-WALK-DATE =
FUNCTION DATE-OF-INTEGER(WS-WALK-INT)
* Check day of week
COMPUTE WS-WALK-DOW =
FUNCTION MOD(WS-WALK-INT - 1, 7) + 1
IF WS-WALK-DOW NOT = 6
AND WS-WALK-DOW NOT = 7
* Weekday — check if holiday
MOVE WS-WALK-DATE TO WS-DATE
PERFORM IS-HOLIDAY
IF WS-NOT-HOLIDAY
ADD 1 TO WS-BUS-DAY-COUNT
END-IF
END-IF
ADD 1 TO WS-WALK-INT
END-PERFORM
.
💡 Performance Note: For date ranges spanning many years, this day-by-day walk can be slow. An optimized version first calculates complete weeks (each contributing 5 business days), then walks only the remaining partial week. James Okafor's production version at MedClaim uses this optimization:
COUNT-BUSINESS-DAYS-FAST.
* Optimized: calculate whole weeks, walk partial
COMPUTE WS-TOTAL-DAYS =
WS-END-INT - WS-WALK-INT
COMPUTE WS-COMPLETE-WEEKS =
FUNCTION INTEGER(WS-TOTAL-DAYS / 7)
COMPUTE WS-BUS-DAY-COUNT =
WS-COMPLETE-WEEKS * 5
COMPUTE WS-REMAINING-DAYS =
WS-TOTAL-DAYS - (WS-COMPLETE-WEEKS * 7)
* Walk the remaining days (0-6 iterations max)
COMPUTE WS-WALK-INT =
WS-WALK-INT + (WS-COMPLETE-WEEKS * 7)
PERFORM WS-REMAINING-DAYS TIMES
COMPUTE WS-WALK-DOW =
FUNCTION MOD(WS-WALK-INT - 1, 7) + 1
IF WS-WALK-DOW NOT = 6
AND WS-WALK-DOW NOT = 7
ADD 1 TO WS-BUS-DAY-COUNT
END-IF
ADD 1 TO WS-WALK-INT
END-PERFORM
* Subtract holidays (requires sorted holiday table)
PERFORM SUBTRACT-HOLIDAYS-IN-RANGE
.
SLA Monitoring at MedClaim
MedClaim uses business day counting for regulatory compliance. Insurance companies must respond to clean claims within specific timeframes measured in business days:
01 WS-SLA-REC.
05 WS-SLA-CLM-ID PIC X(12).
05 WS-SLA-RECV-DATE PIC 9(8).
05 WS-SLA-STATUS PIC X(1).
88 SLA-PENDING VALUE "P".
88 SLA-COMPLETED VALUE "C".
05 WS-SLA-BUS-DAYS PIC 9(3).
05 WS-SLA-LIMIT PIC 9(3).
05 WS-SLA-PCT-USED PIC 9(3).
CHECK-CLAIM-SLA.
* How many business days since we received this claim?
MOVE WS-SLA-RECV-DATE TO WS-START-DATE
MOVE WS-TODAY TO WS-END-DATE
PERFORM COUNT-BUSINESS-DAYS
MOVE WS-BUS-DAY-COUNT TO WS-SLA-BUS-DAYS
* Calculate percentage of SLA consumed
IF WS-SLA-LIMIT > ZERO
COMPUTE WS-SLA-PCT-USED =
(WS-SLA-BUS-DAYS * 100) / WS-SLA-LIMIT
END-IF
* Flag claims approaching SLA deadline
EVALUATE TRUE
WHEN WS-SLA-PCT-USED >= 100
DISPLAY "*** SLA BREACHED *** "
WS-SLA-CLM-ID
" BUS DAYS: " WS-SLA-BUS-DAYS
" LIMIT: " WS-SLA-LIMIT
WHEN WS-SLA-PCT-USED >= 80
DISPLAY "SLA WARNING: "
WS-SLA-CLM-ID
" AT " WS-SLA-PCT-USED "% OF LIMIT"
WHEN OTHER
CONTINUE
END-EVALUATE
.
📊 Metrics: MedClaim's SLA monitoring batch runs nightly, checking approximately 45,000 pending claims. Sarah Kim's team tracks three SLA tiers: urgent (5 business days), standard (15 business days), and complex (30 business days). The business day calculation accuracy is critical — an off-by-one error in 2019 caused 200 claims to be reported as SLA-compliant when they were actually one day over, resulting in $47,000 in regulatory penalties.
T+2 Settlement Date Calculation
Securities transactions settle on a T+2 basis (trade date plus two business days). GlobalBank's settlement system uses this calculation daily:
CALC-SETTLEMENT-DATE.
* T+2: Settlement is 2 business days after trade
MOVE WS-TRADE-DATE TO WS-START-DATE
MOVE 2 TO WS-BUS-DAYS-TO-ADD
PERFORM ADD-BUSINESS-DAYS
MOVE WS-RESULT-DATE TO WS-SETTLE-DATE
* Display for confirmation
DISPLAY "TRADE DATE: " WS-TRADE-DATE
DISPLAY "SETTLEMENT DATE: " WS-SETTLE-DATE
.
When the trade date is a Friday, T+2 settlement falls on Tuesday. When Monday is a holiday, T+2 from Friday becomes Wednesday. These seemingly simple rules require the full business day calculation machinery.
🔗 Cross-Reference: The holiday table used in business day calculations is a table handling problem (Chapter 18). In production, the holiday table is loaded from a file or database at program initialization, allowing it to be updated without recompiling the program.
Floating Holidays
The holiday table above uses fixed dates. But some holidays float — Thanksgiving is the fourth Thursday in November, Memorial Day is the last Monday in May, and Easter follows a complex lunar calendar. Computing floating holidays requires additional logic:
COMPUTE-THANKSGIVING.
* Fourth Thursday in November for given year
* Step 1: Find day-of-week for November 1
COMPUTE WS-NOV-1 =
WS-INPUT-YEAR * 10000 + 1101
COMPUTE WS-NOV-1-DOW =
FUNCTION MOD(
FUNCTION INTEGER-OF-DATE(WS-NOV-1)
- 1, 7) + 1
* Step 2: Calculate first Thursday (DOW = 4)
IF WS-NOV-1-DOW <= 4
COMPUTE WS-FIRST-THU-DAY =
4 - WS-NOV-1-DOW + 1
ELSE
COMPUTE WS-FIRST-THU-DAY =
11 - WS-NOV-1-DOW + 1
END-IF
* Step 3: Fourth Thursday = first + 21
COMPUTE WS-THANKSGIVING-DAY =
WS-FIRST-THU-DAY + 21
COMPUTE WS-THANKSGIVING =
WS-INPUT-YEAR * 10000
+ 1100 + WS-THANKSGIVING-DAY
.
COMPUTE-MEMORIAL-DAY.
* Last Monday in May for given year
* Step 1: Find day-of-week for May 31
COMPUTE WS-MAY-31 =
WS-INPUT-YEAR * 10000 + 0531
COMPUTE WS-MAY-31-DOW =
FUNCTION MOD(
FUNCTION INTEGER-OF-DATE(WS-MAY-31)
- 1, 7) + 1
* Step 2: Back up to Monday (DOW = 1)
IF WS-MAY-31-DOW = 1
MOVE WS-MAY-31 TO WS-MEMORIAL-DAY
ELSE
COMPUTE WS-MEMORIAL-DAY =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(WS-MAY-31)
- WS-MAY-31-DOW + 1)
END-IF
.
💡 Production Tip: Sarah Kim maintains a floating holiday calculator at MedClaim that generates the entire year's holiday table on January 1 (or at program initialization). The generated table is stored in a flat file so that all programs can load it without recomputing. This approach ensures consistency across all programs and allows operations staff to add ad hoc holidays (company closure for weather, etc.) by editing the file.
21.11 Date Processing in Batch Windows
Production batch systems have unique date processing requirements that stem from the timing of batch execution. Understanding these requirements is essential for any COBOL programmer working on batch systems.
The Business Date vs. System Date Problem
Batch programs that run near midnight face a fundamental problem: the system date may change during execution. A batch that starts at 11:45 PM on January 15 and runs until 12:30 AM on January 16 will see two different dates from FUNCTION CURRENT-DATE.
The standard solution is to establish a business date at the start of the batch and use it consistently:
01 WS-BUSINESS-DATE PIC 9(8).
01 WS-SYSTEM-DATE PIC 9(8).
ESTABLISH-BUSINESS-DATE.
* Option 1: Accept business date as a parameter
ACCEPT WS-BUSINESS-DATE FROM JCL-PARM
IF WS-BUSINESS-DATE = SPACES
OR WS-BUSINESS-DATE = ZEROES
* Option 2: Default to system date
MOVE FUNCTION CURRENT-DATE(1:8)
TO WS-BUSINESS-DATE
END-IF
* Validate the business date
COMPUTE WS-DATE-TEST =
FUNCTION TEST-DATE-YYYYMMDD(
WS-BUSINESS-DATE)
IF WS-DATE-TEST NOT = ZERO
DISPLAY "INVALID BUSINESS DATE: "
WS-BUSINESS-DATE
MOVE 16 TO RETURN-CODE
STOP RUN
END-IF
DISPLAY "BUSINESS DATE ESTABLISHED: "
WS-BUSINESS-DATE
.
⚠️ Critical Production Pattern: James Okafor's first rule of batch programming: "The business date is a parameter, not a system call. Every batch program at MedClaim accepts a business date through the JCL PARM or a control file. This allows us to rerun a batch for a previous date without changing system clocks, which is essential for error recovery and audit requirements."
Date Rollover and Rerun Handling
When a batch fails and must be rerun, the business date parameter ensures consistent results:
* In JCL:
* //STEP01 EXEC PGM=CLMAGING,PARM='20240315'
*
* This allows:
* 1. Normal run: PARM=today's date
* 2. Rerun for yesterday: PARM=yesterday's date
* 3. Month-end rerun: PARM=last day of month
* 4. Audit recreation: PARM=any historical date
Batch Date Continuity Checking
Production batch systems verify that they are processing the correct date in sequence:
01 WS-LAST-PROC-DATE PIC 9(8).
01 WS-EXPECTED-DATE PIC 9(8).
01 WS-DAY-GAP PIC S9(3).
CHECK-DATE-CONTINUITY.
* Read last successfully processed date from
* control file
READ CONTROL-FILE INTO WS-LAST-PROC-DATE
* Compute expected date (next business day)
MOVE WS-LAST-PROC-DATE TO WS-START-DATE
MOVE 1 TO WS-BUS-DAYS-TO-ADD
PERFORM ADD-BUSINESS-DAYS
MOVE WS-RESULT-DATE TO WS-EXPECTED-DATE
* Compare with today's business date
IF WS-BUSINESS-DATE NOT = WS-EXPECTED-DATE
COMPUTE WS-DAY-GAP =
FUNCTION INTEGER-OF-DATE(
WS-BUSINESS-DATE)
- FUNCTION INTEGER-OF-DATE(
WS-EXPECTED-DATE)
IF WS-DAY-GAP > 0
DISPLAY "WARNING: GAP OF " WS-DAY-GAP
" DAYS SINCE LAST PROCESSING"
DISPLAY "LAST PROCESSED: "
WS-LAST-PROC-DATE
DISPLAY "EXPECTED: "
WS-EXPECTED-DATE
DISPLAY "TODAY: "
WS-BUSINESS-DATE
* Depending on policy, this may be a
* warning or a fatal error
ELSE
DISPLAY "NOTE: REPROCESSING DATE "
WS-BUSINESS-DATE
END-IF
END-IF
.
This continuity check catches the common production problem of accidentally skipping a day's processing or running the same day twice. At GlobalBank, Maria Chen's team treats a gap of more than one business day as a fatal error requiring manual intervention: "Better to stop and investigate than to process three days' worth of transactions with potentially stale reference data."
End-of-Period Date Determination
Batch systems frequently need to determine whether the current business date is the last business day of a period (month, quarter, year) because additional processing runs on those dates:
01 WS-PERIOD-FLAGS.
05 WS-IS-MONTH-END PIC X VALUE "N".
88 IS-MONTH-END VALUE "Y".
05 WS-IS-QUARTER-END PIC X VALUE "N".
88 IS-QUARTER-END VALUE "Y".
05 WS-IS-YEAR-END PIC X VALUE "N".
88 IS-YEAR-END VALUE "Y".
DETERMINE-PERIOD-END.
MOVE "N" TO WS-IS-MONTH-END
WS-IS-QUARTER-END
WS-IS-YEAR-END
* Get next business day
MOVE WS-BUSINESS-DATE TO WS-START-DATE
MOVE 1 TO WS-BUS-DAYS-TO-ADD
PERFORM ADD-BUSINESS-DAYS
* WS-RESULT-DATE now has next business day
* If next business day is in a different month,
* today is month-end
IF WS-RESULT-DATE(5:2) NOT =
WS-BUSINESS-DATE(5:2)
SET IS-MONTH-END TO TRUE
* Check quarter end (month 3, 6, 9, 12)
EVALUATE WS-BUSINESS-DATE(5:2)
WHEN "03" WHEN "06"
WHEN "09" WHEN "12"
SET IS-QUARTER-END TO TRUE
END-EVALUATE
* Check year end
IF WS-BUSINESS-DATE(5:2) = "12"
SET IS-YEAR-END TO TRUE
END-IF
END-IF
IF IS-MONTH-END
DISPLAY "*** MONTH-END PROCESSING ***"
END-IF
IF IS-QUARTER-END
DISPLAY "*** QUARTER-END PROCESSING ***"
END-IF
IF IS-YEAR-END
DISPLAY "*** YEAR-END PROCESSING ***"
END-IF
.
✅ Best Practice: Notice that this code does not hard-code "the last calendar day of the month." Instead, it checks whether the next business day falls in a different month. This correctly handles the case where the last calendar day (e.g., December 31) falls on a weekend — in that case, the last business day of the month might be December 29 (Friday), and that is when month-end processing should run.
21.12 Date Formatting and Display
Production reports require dates in various human-readable formats. While LE date services (CEEDATM) can format dates on z/OS, you can also build portable formatters using reference modification and month name tables.
Format: Month DD, YYYY
01 WS-FORMATTED-DATE PIC X(20).
01 WS-FMT-PTR PIC 9(3).
FORMAT-WRITTEN-DATE.
* Input: WS-INPUT-DATE (YYYYMMDD)
MOVE SPACES TO WS-FORMATTED-DATE
MOVE 1 TO WS-FMT-PTR
MOVE WS-INPUT-DATE(5:2) TO WS-INPUT-MONTH
* Month name
MOVE FUNCTION TRIM(
MONTH-NAME(WS-INPUT-MONTH) TRAILING)
TO WS-FORMATTED-DATE(WS-FMT-PTR:)
ADD FUNCTION LENGTH(
FUNCTION TRIM(
MONTH-NAME(WS-INPUT-MONTH) TRAILING))
TO WS-FMT-PTR
* Space
MOVE " " TO WS-FORMATTED-DATE(WS-FMT-PTR:1)
ADD 1 TO WS-FMT-PTR
* Day (without leading zero)
IF WS-INPUT-DATE(7:1) = "0"
MOVE WS-INPUT-DATE(8:1) TO
WS-FORMATTED-DATE(WS-FMT-PTR:1)
ADD 1 TO WS-FMT-PTR
ELSE
MOVE WS-INPUT-DATE(7:2) TO
WS-FORMATTED-DATE(WS-FMT-PTR:2)
ADD 2 TO WS-FMT-PTR
END-IF
* ", "
MOVE ", " TO WS-FORMATTED-DATE(WS-FMT-PTR:2)
ADD 2 TO WS-FMT-PTR
* Year
MOVE WS-INPUT-DATE(1:4) TO
WS-FORMATTED-DATE(WS-FMT-PTR:4)
* Result: "June 15, 2024"
.
Format: MM/DD/YYYY (US Format)
FORMAT-US-DATE.
* Input: WS-INPUT-DATE (YYYYMMDD)
* Output: WS-US-FORMAT (MM/DD/YYYY)
STRING WS-INPUT-DATE(5:2) "/"
WS-INPUT-DATE(7:2) "/"
WS-INPUT-DATE(1:4)
DELIMITED BY SIZE
INTO WS-US-FORMAT
END-STRING
.
Format: DD-Mon-YYYY (International)
01 MONTH-ABBREV-VALS.
05 FILLER PIC X(3) VALUE "Jan".
05 FILLER PIC X(3) VALUE "Feb".
05 FILLER PIC X(3) VALUE "Mar".
05 FILLER PIC X(3) VALUE "Apr".
05 FILLER PIC X(3) VALUE "May".
05 FILLER PIC X(3) VALUE "Jun".
05 FILLER PIC X(3) VALUE "Jul".
05 FILLER PIC X(3) VALUE "Aug".
05 FILLER PIC X(3) VALUE "Sep".
05 FILLER PIC X(3) VALUE "Oct".
05 FILLER PIC X(3) VALUE "Nov".
05 FILLER PIC X(3) VALUE "Dec".
01 MONTH-ABBREVS REDEFINES MONTH-ABBREV-VALS.
05 MONTH-ABBR PIC X(3) OCCURS 12 TIMES.
FORMAT-INTL-DATE.
MOVE WS-INPUT-DATE(5:2) TO WS-INPUT-MONTH
STRING WS-INPUT-DATE(7:2) "-"
MONTH-ABBR(WS-INPUT-MONTH) "-"
WS-INPUT-DATE(1:4)
DELIMITED BY SIZE
INTO WS-INTL-FORMAT
END-STRING
* Result: "15-Jun-2024"
.
Elapsed Time Formatting
For batch processing reports, elapsed time is displayed in hours, minutes, and seconds:
01 WS-ELAPSED-SECS PIC 9(7).
01 WS-ELAPSED-DISPLAY PIC X(12).
01 WS-EL-HOURS PIC 99.
01 WS-EL-MINS PIC 99.
01 WS-EL-SECS PIC 99.
FORMAT-ELAPSED-TIME.
COMPUTE WS-EL-HOURS =
FUNCTION INTEGER-PART(
WS-ELAPSED-SECS / 3600)
COMPUTE WS-EL-MINS =
FUNCTION INTEGER-PART(
FUNCTION MOD(WS-ELAPSED-SECS, 3600)
/ 60)
COMPUTE WS-EL-SECS =
FUNCTION MOD(WS-ELAPSED-SECS, 60)
STRING WS-EL-HOURS ":" WS-EL-MINS
":" WS-EL-SECS
DELIMITED BY SIZE
INTO WS-ELAPSED-DISPLAY
END-STRING
* Result: "02:35:17"
.
🧪 Try It Yourself: Date Format Library
Build a reusable date formatting library as a set of paragraphs that can be COPYed into any program. Include formatters for: 1. ISO format: 2024-06-15 2. US format: 06/15/2024 3. European format: 15/06/2024 4. Written format: June 15, 2024 5. Julian format: 2024-167 6. Compact format: 20240615
Accept a format code parameter and route to the correct formatter.
21.11 Date Ranges and Overlap Detection
Many business processes need to work with date ranges — policy effective periods, promotional windows, coverage dates. Detecting overlaps between ranges is a common and error-prone operation.
Range Overlap Logic
Two date ranges [start1, end1] and [start2, end2] overlap if and only if:
start1 <= end2 AND start2 <= end1
In COBOL:
01 WS-RANGE-1.
05 WS-R1-START PIC 9(8).
05 WS-R1-END PIC 9(8).
01 WS-RANGE-2.
05 WS-R2-START PIC 9(8).
05 WS-R2-END PIC 9(8).
01 WS-OVERLAP-FLAG PIC X(1).
88 WS-RANGES-OVERLAP VALUE "Y".
88 WS-NO-OVERLAP VALUE "N".
CHECK-DATE-OVERLAP.
IF WS-R1-START <= WS-R2-END
AND WS-R2-START <= WS-R1-END
SET WS-RANGES-OVERLAP TO TRUE
ELSE
SET WS-NO-OVERLAP TO TRUE
END-IF
.
Computing the Overlap Period
When ranges overlap, you may need to compute the overlapping period:
COMPUTE-OVERLAP-PERIOD.
PERFORM CHECK-DATE-OVERLAP
IF WS-RANGES-OVERLAP
* Overlap start = later of the two starts
IF WS-R1-START >= WS-R2-START
MOVE WS-R1-START
TO WS-OVERLAP-START
ELSE
MOVE WS-R2-START
TO WS-OVERLAP-START
END-IF
* Overlap end = earlier of the two ends
IF WS-R1-END <= WS-R2-END
MOVE WS-R1-END TO WS-OVERLAP-END
ELSE
MOVE WS-R2-END TO WS-OVERLAP-END
END-IF
* Overlap days
COMPUTE WS-OVERLAP-DAYS =
FUNCTION INTEGER-OF-DATE(
WS-OVERLAP-END)
- FUNCTION INTEGER-OF-DATE(
WS-OVERLAP-START) + 1
ELSE
MOVE ZERO TO WS-OVERLAP-DAYS
END-IF
.
📊 MedClaim Application: Date range overlap detection is critical for coordination of benefits (COB). When a patient has two insurance policies, MedClaim must determine whether the coverage periods overlap and, if so, which policy is primary for the date of service. James Okafor's adjudication engine performs this check on every claim with a COB indicator.
Date Range Validation
Validate that a date range makes logical sense:
VALIDATE-DATE-RANGE.
SET WS-RANGE-VALID TO TRUE
* Both dates must be valid
MOVE WS-R1-START TO WS-INPUT-DATE
PERFORM VALIDATE-DATE
IF WS-DATE-INVALID
SET WS-RANGE-INVALID TO TRUE
MOVE "INVALID START DATE" TO WS-ERR-MSG
EXIT PARAGRAPH
END-IF
MOVE WS-R1-END TO WS-INPUT-DATE
PERFORM VALIDATE-DATE
IF WS-DATE-INVALID
SET WS-RANGE-INVALID TO TRUE
MOVE "INVALID END DATE" TO WS-ERR-MSG
EXIT PARAGRAPH
END-IF
* End must be >= Start
IF WS-R1-END < WS-R1-START
SET WS-RANGE-INVALID TO TRUE
MOVE "END DATE BEFORE START DATE"
TO WS-ERR-MSG
EXIT PARAGRAPH
END-IF
* Range should not exceed maximum span
COMPUTE WS-RANGE-DAYS =
FUNCTION INTEGER-OF-DATE(WS-R1-END)
- FUNCTION INTEGER-OF-DATE(WS-R1-START)
IF WS-RANGE-DAYS > 366
SET WS-RANGE-WARNING TO TRUE
MOVE "RANGE EXCEEDS 1 YEAR"
TO WS-WARN-MSG
END-IF
.
21.12 GlobalBank Case Study: Statement Period Calculations
GlobalBank generates monthly statements on different cycle dates for different account groups. The statement must cover exactly one month, handle month-end boundaries correctly, and include accurate transaction aging.
*============================================================
* GlobalBank Statement Period Calculator
* Determines statement start/end dates based on
* cycle day and accounts for month-end adjustments.
*============================================================
01 WS-CYCLE-DAY PIC 99.
01 WS-STMT-START PIC 9(8).
01 WS-STMT-END PIC 9(8).
01 WS-PREV-MONTH PIC 9(2).
01 WS-PREV-YEAR PIC 9(4).
COMPUTE-STATEMENT-PERIOD.
* Statement ends on cycle day of current month
* Statement starts on cycle day of previous month
* Current month end date
MOVE WS-CD-YEAR TO WS-STMT-END(1:4)
MOVE WS-CD-MONTH TO WS-STMT-END(5:2)
* Adjust cycle day for short months
IF WS-CYCLE-DAY > MAX-DAYS(WS-CD-MONTH)
MOVE MAX-DAYS(WS-CD-MONTH)
TO WS-STMT-END(7:2)
ELSE
MOVE WS-CYCLE-DAY TO WS-STMT-END(7:2)
END-IF
* Previous month start date
IF WS-CD-MONTH = 1
MOVE 12 TO WS-PREV-MONTH
COMPUTE WS-PREV-YEAR = WS-CD-YEAR - 1
ELSE
COMPUTE WS-PREV-MONTH =
WS-CD-MONTH - 1
MOVE WS-CD-YEAR TO WS-PREV-YEAR
END-IF
MOVE WS-PREV-YEAR TO WS-STMT-START(1:4)
MOVE WS-PREV-MONTH TO WS-STMT-START(5:2)
* Adjust for short previous month
IF WS-CYCLE-DAY >
MAX-DAYS(WS-PREV-MONTH)
MOVE MAX-DAYS(WS-PREV-MONTH)
TO WS-STMT-START(7:2)
ELSE
MOVE WS-CYCLE-DAY
TO WS-STMT-START(7:2)
END-IF
* Add 1 day to start (day after previous close)
COMPUTE WS-STMT-START =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(
WS-STMT-START) + 1)
DISPLAY "STATEMENT PERIOD: "
WS-STMT-START " TO " WS-STMT-END
.
📊 Production Reality: Derek Washington found 14 bugs in the legacy statement period calculation during his first review. Most involved February and month-end boundaries. The intrinsic function approach reduced the code from 120 lines to 40 lines and eliminated all month-boundary bugs.
21.11 MedClaim Case Study: Timely Filing Validation
Healthcare regulations require claims to be filed within a specific number of days from the date of service. The timely filing limit varies by payer (insurance company), typically ranging from 90 to 365 days. Claims filed after the deadline are denied.
01 WS-TF-FIELDS.
05 WS-TF-SERVICE-DATE PIC 9(8).
05 WS-TF-RECEIVED-DATE PIC 9(8).
05 WS-TF-LIMIT-DAYS PIC 9(3).
05 WS-TF-ACTUAL-DAYS PIC 9(5).
05 WS-TF-DEADLINE PIC 9(8).
05 WS-TF-STATUS PIC X(1).
88 WS-TF-TIMELY VALUE "T".
88 WS-TF-UNTIMELY VALUE "U".
88 WS-TF-WARNING VALUE "W".
88 WS-TF-INVALID VALUE "I".
VALIDATE-TIMELY-FILING.
* Validate service date
PERFORM VALIDATE-DATE
IF WS-DATE-INVALID
SET WS-TF-INVALID TO TRUE
MOVE "INVALID SERVICE DATE"
TO WS-DENIAL-REASON
EXIT PARAGRAPH
END-IF
* Calculate days since service
COMPUTE WS-TF-ACTUAL-DAYS =
FUNCTION INTEGER-OF-DATE(
WS-TF-RECEIVED-DATE)
- FUNCTION INTEGER-OF-DATE(
WS-TF-SERVICE-DATE)
* Validate not in the future
IF WS-TF-ACTUAL-DAYS < ZERO
SET WS-TF-INVALID TO TRUE
MOVE "SERVICE DATE IN FUTURE"
TO WS-DENIAL-REASON
EXIT PARAGRAPH
END-IF
* Calculate filing deadline
COMPUTE WS-TF-DEADLINE =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(
WS-TF-SERVICE-DATE)
+ WS-TF-LIMIT-DAYS)
* Determine status
EVALUATE TRUE
WHEN WS-TF-ACTUAL-DAYS <= WS-TF-LIMIT-DAYS
SET WS-TF-TIMELY TO TRUE
WHEN WS-TF-ACTUAL-DAYS <=
WS-TF-LIMIT-DAYS + 30
SET WS-TF-WARNING TO TRUE
MOVE "APPROACHING TIMELY FILING LIMIT"
TO WS-WARNING-MSG
WHEN OTHER
SET WS-TF-UNTIMELY TO TRUE
MOVE "TIMELY FILING LIMIT EXCEEDED"
TO WS-DENIAL-REASON
END-EVALUATE
DISPLAY "SERVICE DATE: " WS-TF-SERVICE-DATE
DISPLAY "RECEIVED: " WS-TF-RECEIVED-DATE
DISPLAY "DAYS ELAPSED: " WS-TF-ACTUAL-DAYS
DISPLAY "DEADLINE: " WS-TF-DEADLINE
DISPLAY "STATUS: " WS-TF-STATUS
.
MedClaim Aging Report
PRODUCE-AGING-REPORT.
INITIALIZE WS-AGING-BUCKETS
INITIALIZE WS-AGING-COUNTS
MOVE FUNCTION CURRENT-DATE(1:8) TO WS-TODAY
PERFORM UNTIL WS-CLAIM-EOF
COMPUTE WS-AGE-DAYS =
FUNCTION INTEGER-OF-DATE(WS-TODAY)
- FUNCTION INTEGER-OF-DATE(
WS-CLAIM-SERVICE-DATE)
MOVE WS-CLAIM-AMOUNT TO WS-ITEM-AMOUNT
PERFORM ASSIGN-AGING-BUCKET
READ CLAIM-FILE INTO WS-CLAIM-REC
AT END SET WS-CLAIM-EOF TO TRUE
END-READ
END-PERFORM
DISPLAY "CLAIM AGING REPORT"
DISPLAY "===================="
DISPLAY "CURRENT (0-30): "
WS-COUNT-CURRENT " CLAIMS "
WS-BUCKET-CURRENT
DISPLAY "31-60 DAYS: "
WS-COUNT-30 " CLAIMS "
WS-BUCKET-30
DISPLAY "61-90 DAYS: "
WS-COUNT-60 " CLAIMS "
WS-BUCKET-60
DISPLAY "91-120 DAYS: "
WS-COUNT-90 " CLAIMS "
WS-BUCKET-90
DISPLAY "OVER 120 DAYS: "
WS-COUNT-OVER-90 " CLAIMS "
WS-BUCKET-OVER-90
.
21.12 Y2K Historical Timeline
Understanding Y2K's timeline helps appreciate the institutional discipline that date handling requires:
| Year | Event |
|---|---|
| 1958 | COBOL designed with 2-digit year fields (storage was $1M per megabyte) |
| 1978 | First papers published warning about the "Year 2000 problem" |
| 1985 | COBOL-85 standard does not address the 2-digit year issue |
| 1989 | Intrinsic functions added to COBOL-85 (amendment), including date functions with 4-digit years |
| 1993 | Peter de Jager publishes "Doomsday 2000" in ComputerWorld, raising mainstream awareness |
| 1996 | Major organizations begin Y2K remediation projects |
| 1997 | COBOL programmers come out of retirement; rates reach $150-200/hour |
| 1998 | Most critical systems remediated; testing begins |
| 1999 | Final testing and certification; contingency plans prepared |
| 2000-01-01 | The millennium arrives with minimal failures, validating the $300B+ remediation |
| 2002 | COBOL 2002 standard formalizes 4-digit year requirements |
🔵 Theme: Legacy != Obsolete. Y2K demonstrated that COBOL systems are critical infrastructure. The successful remediation proved that COBOL code can be systematically analyzed, modified, and tested at industrial scale. The techniques developed during Y2K — automated code scanning, date field identification, regression testing — remain in use today for modernization projects.
21.13 The Student Mainframe Lab
🧪 Try It Yourself: Date Utility Suite
Build a comprehensive date utility program with the following functions: 1. Date Validator: Accept a date string and validate it completely (numeric, range, leap year) 2. Date Calculator: Accept two dates and compute the difference in days, weeks, and months 3. Business Day Calculator: Accept a start date and number of business days; compute the result date (excluding weekends and a hardcoded holiday table) 4. Aging Calculator: Accept a date and compute which aging bucket it falls into (current, 30, 60, 90, 120+) 5. Date Formatter: Accept a YYYYMMDD date and display it in 5 different formats (ISO, US, European, written, Julian)
Make each function a separate paragraph callable from a menu.
🧪 Try It Yourself: Y2K Simulator
Write a program that simulates the Y2K problem: 1. Store 10 dates using 2-digit years (PIC 99 for year) 2. Attempt to sort them and observe the incorrect ordering 3. Implement the windowing fix (pivot year = 50) 4. Implement the expansion fix (convert to 4-digit years) 5. Compare the results of all three approaches
This exercise demonstrates WHY Y2K was a problem and HOW each fix addresses it.
21.14 Complete Worked Example: Date Processing Utility Subprogram
In production environments, date processing logic should be centralized in a callable subprogram. This avoids the inconsistency problems described in the MedClaim case study, where the adjudication system and aging report used different date calculations. Here is a production-quality date utility subprogram that can be called from any program in the system.
IDENTIFICATION DIVISION.
PROGRAM-ID. DTUTIL.
*============================================================
* Date Processing Utility Subprogram
* Centralized date operations for validation,
* arithmetic, aging, and day-of-week calculation.
*
* CALL 'DTUTIL' USING DU-FUNCTION DU-INPUT
* DU-OUTPUT DU-RC
*============================================================
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-TODAY PIC 9(8).
01 WS-TODAY-INT PIC 9(7).
01 WS-INIT-FLAG PIC X VALUE "N".
88 WS-IS-INIT VALUE "Y".
01 DIM-VALUES.
05 FILLER PIC 99 VALUE 31.
05 FILLER PIC 99 VALUE 28.
05 FILLER PIC 99 VALUE 31.
05 FILLER PIC 99 VALUE 30.
05 FILLER PIC 99 VALUE 31.
05 FILLER PIC 99 VALUE 30.
05 FILLER PIC 99 VALUE 31.
05 FILLER PIC 99 VALUE 31.
05 FILLER PIC 99 VALUE 30.
05 FILLER PIC 99 VALUE 31.
05 FILLER PIC 99 VALUE 30.
05 FILLER PIC 99 VALUE 31.
01 DIM-TABLE REDEFINES DIM-VALUES.
05 DAYS-IN-MONTH PIC 99 OCCURS 12 TIMES.
01 WS-W-YEAR PIC 9(4).
01 WS-W-MONTH PIC 9(2).
01 WS-W-DAY PIC 9(2).
01 WS-W-MAX-DAY PIC 99.
01 WS-LEAP-FL PIC X.
88 WS-LEAP VALUE "Y".
LINKAGE SECTION.
01 LS-FUNC PIC X(4).
* VALD = Validate DIFF = Days between
* ADDP = Add days AGEM = Age from today
* WDAY = Day of week (1=Mon..7=Sun)
01 LS-INPUT.
05 LS-IN-DATE-1 PIC 9(8).
05 LS-IN-DATE-2 PIC 9(8).
05 LS-IN-DAYS PIC S9(5).
01 LS-OUTPUT.
05 LS-OUT-DATE PIC 9(8).
05 LS-OUT-DAYS PIC S9(5).
05 LS-OUT-DOW PIC 9.
01 LS-RC PIC S9(4) COMP.
* 0=OK, 8=Invalid, 12=Bad function
PROCEDURE DIVISION USING LS-FUNC LS-INPUT
LS-OUTPUT LS-RC.
0000-MAIN.
MOVE 0 TO LS-RC
IF NOT WS-IS-INIT
MOVE FUNCTION CURRENT-DATE(1:8)
TO WS-TODAY
COMPUTE WS-TODAY-INT =
FUNCTION INTEGER-OF-DATE(WS-TODAY)
MOVE "Y" TO WS-INIT-FLAG
END-IF
EVALUATE LS-FUNC
WHEN "VALD" PERFORM 1000-VALIDATE
WHEN "DIFF" PERFORM 2000-DIFF
WHEN "ADDP" PERFORM 3000-ADD
WHEN "AGEM" PERFORM 4000-AGING
WHEN "WDAY" PERFORM 5000-DOW
WHEN OTHER MOVE 12 TO LS-RC
END-EVALUATE
GOBACK.
1000-VALIDATE.
IF LS-IN-DATE-1 NOT NUMERIC
MOVE 8 TO LS-RC
EXIT PARAGRAPH
END-IF
MOVE LS-IN-DATE-1(1:4) TO WS-W-YEAR
MOVE LS-IN-DATE-1(5:2) TO WS-W-MONTH
MOVE LS-IN-DATE-1(7:2) TO WS-W-DAY
IF WS-W-YEAR < 1900 OR > 2099
MOVE 8 TO LS-RC EXIT PARAGRAPH
END-IF
IF WS-W-MONTH < 01 OR > 12
MOVE 8 TO LS-RC EXIT PARAGRAPH
END-IF
MOVE DAYS-IN-MONTH(WS-W-MONTH)
TO WS-W-MAX-DAY
IF WS-W-MONTH = 2
PERFORM 8000-LEAP-CHECK
IF WS-LEAP MOVE 29 TO WS-W-MAX-DAY
END-IF
END-IF
IF WS-W-DAY < 01 OR > WS-W-MAX-DAY
MOVE 8 TO LS-RC
END-IF.
2000-DIFF.
COMPUTE LS-OUT-DAYS =
FUNCTION INTEGER-OF-DATE(LS-IN-DATE-2)
- FUNCTION INTEGER-OF-DATE(LS-IN-DATE-1).
3000-ADD.
COMPUTE LS-OUT-DATE =
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(
LS-IN-DATE-1) + LS-IN-DAYS).
4000-AGING.
COMPUTE LS-OUT-DAYS =
WS-TODAY-INT -
FUNCTION INTEGER-OF-DATE(LS-IN-DATE-1).
5000-DOW.
COMPUTE LS-OUT-DOW =
FUNCTION MOD(
FUNCTION INTEGER-OF-DATE(
LS-IN-DATE-1) - 1, 7) + 1.
8000-LEAP-CHECK.
MOVE "N" TO WS-LEAP-FL
IF FUNCTION MOD(WS-W-YEAR, 4) = 0
MOVE "Y" TO WS-LEAP-FL
IF FUNCTION MOD(WS-W-YEAR, 100) = 0
MOVE "N" TO WS-LEAP-FL
IF FUNCTION MOD(WS-W-YEAR, 400) = 0
MOVE "Y" TO WS-LEAP-FL
END-IF
END-IF
END-IF.
Calling the Utility
Any program can use the centralized date logic:
*> In the calling program:
01 WS-DU-FUNC PIC X(4).
01 WS-DU-INPUT.
05 WS-DU-IN-1 PIC 9(8).
05 WS-DU-IN-2 PIC 9(8).
05 WS-DU-IN-DAYS PIC S9(5).
01 WS-DU-OUTPUT.
05 WS-DU-OUT-DATE PIC 9(8).
05 WS-DU-OUT-DAYS PIC S9(5).
05 WS-DU-OUT-DOW PIC 9.
01 WS-DU-RC PIC S9(4) COMP.
*> Validate a date
MOVE "VALD" TO WS-DU-FUNC
MOVE 20240229 TO WS-DU-IN-1
CALL "DTUTIL" USING WS-DU-FUNC WS-DU-INPUT
WS-DU-OUTPUT WS-DU-RC
IF WS-DU-RC = 0
DISPLAY "DATE IS VALID"
END-IF
*> Compute days between two dates
MOVE "DIFF" TO WS-DU-FUNC
MOVE 20240101 TO WS-DU-IN-1
MOVE 20240615 TO WS-DU-IN-2
CALL "DTUTIL" USING WS-DU-FUNC WS-DU-INPUT
WS-DU-OUTPUT WS-DU-RC
DISPLAY "DAYS BETWEEN: " WS-DU-OUT-DAYS
*> Compute aging
MOVE "AGEM" TO WS-DU-FUNC
MOVE 20240301 TO WS-DU-IN-1
CALL "DTUTIL" USING WS-DU-FUNC WS-DU-INPUT
WS-DU-OUTPUT WS-DU-RC
DISPLAY "AGE IN DAYS: " WS-DU-OUT-DAYS
This subprogram pattern ensures that every program in the system uses identical date logic. When a date-related bug is found, it is fixed in one place (DTUTIL) and all programs benefit immediately. The subprogram caches the "today" date on its first call, ensuring consistent aging calculations throughout a batch run even if the batch runs past midnight.
✅ Best Practice: James Okafor's rule: "Every COBOL shop should have exactly one date utility subprogram, used by every program. No exceptions. When I see date arithmetic inline in a program, I know I am looking at a future bug."
21.16 Date Processing Testing Strategies
Date processing code is notoriously difficult to test because many bugs only manifest at specific calendar boundaries. A comprehensive testing strategy must deliberately target these boundaries.
Critical Test Dates
Every date processing program should be tested with the following categories of dates:
* Test Category 1: Leap year boundaries
* 20240229 — Valid (2024 is a leap year)
* 20230229 — Invalid (2023 is not a leap year)
* 20000229 — Valid (divisible by 400)
* 19000229 — Invalid (divisible by 100, not 400)
* Test Category 2: Month boundaries
* 20240131 — January 31 (next day is February)
* 20240228 — Feb 28 in non-leap year
* 20240229 — Feb 29 in leap year
* 20240331 — March 31 (next day is April 1)
* 20240430 — April 30 (30-day month end)
* Test Category 3: Year boundaries
* 20231231 — Last day of year
* 20240101 — First day of year
* Test Category 4: Edge cases
* 16010101 — COBOL epoch (earliest valid date)
* 99991231 — Latest valid date
* 00000000 — Zero date (invalid)
* 99999999 — Maximum value (invalid)
⚠️ The February 29 Test: Tomás Rivera requires every MedClaim date program to be tested with February 29 of both leap and non-leap years. "In my career, I have seen at least a dozen production bugs that only appeared on February 29. One of them went undetected for four years — the exact length of the leap year cycle — because no one thought to test with that date."
Automated Date Boundary Testing
Build a test harness that exercises your date routines across all boundary conditions:
TEST-DATE-BOUNDARIES.
* Test leap year detection
MOVE 20240101 TO WS-TEST-DATE
PERFORM TEST-ONE-DATE
MOVE 20240229 TO WS-TEST-DATE
PERFORM TEST-ONE-DATE
MOVE 20230229 TO WS-TEST-DATE
PERFORM TEST-ONE-DATE-EXPECT-INVALID
MOVE 20000229 TO WS-TEST-DATE
PERFORM TEST-ONE-DATE
MOVE 19000229 TO WS-TEST-DATE
PERFORM TEST-ONE-DATE-EXPECT-INVALID
* Test month-end transitions
PERFORM VARYING WS-TEST-MONTH FROM 1 BY 1
UNTIL WS-TEST-MONTH > 12
* Test last day of each month
MOVE WS-MONTH-END-DAY(WS-TEST-MONTH)
TO WS-TEST-DAY
COMPUTE WS-TEST-DATE =
20240000 + WS-TEST-MONTH * 100
+ WS-TEST-DAY
PERFORM TEST-ONE-DATE
* Test day after last day (invalid for 28/30
* day months)
ADD 1 TO WS-TEST-DAY
COMPUTE WS-TEST-DATE =
20240000 + WS-TEST-MONTH * 100
+ WS-TEST-DAY
PERFORM TEST-ONE-DATE-CHECK-VALIDITY
END-PERFORM
* Test date arithmetic across year boundary
MOVE 20231231 TO WS-START-DATE
MOVE 20240101 TO WS-END-DATE
PERFORM COUNT-BUSINESS-DAYS
IF WS-BUS-DAY-COUNT NOT = ZERO
DISPLAY "YEAR BOUNDARY TEST: PASS"
END-IF
* Test large date spans
MOVE 20200101 TO WS-START-DATE
MOVE 20240101 TO WS-END-DATE
PERFORM COUNT-BUSINESS-DAYS
DISPLAY "4 YEAR SPAN: " WS-BUS-DAY-COUNT
" BUSINESS DAYS"
.
✅ Best Practice: Maria Chen's GlobalBank team runs date boundary tests as part of every program's regression suite. The test data file contains 200+ date combinations that cover every known edge case. New developers are required to add test cases for any date bug they discover.
21.17 Common Date Processing Mistakes
Mistake 1: Not Validating Dates
*> DANGEROUS - no validation
COMPUTE WS-DAYS =
FUNCTION INTEGER-OF-DATE(WS-INPUT-DATE)
*> If WS-INPUT-DATE is 20240231 (Feb 31), this abends!
Mistake 2: Hardcoding Year Assumptions
*> WRONG - breaks after 2099
IF WS-YEAR(1:2) = "20"
PERFORM PROCESS-CURRENT-CENTURY
Mistake 3: Ignoring Timezone in Batch
*> RISKY - near midnight, date may differ from business date
MOVE FUNCTION CURRENT-DATE(1:8) TO WS-BUSINESS-DATE
*> If batch runs at 11:55 PM EST, some transactions may
*> be dated tomorrow in UTC
Mistake 4: Subtracting Dates Directly
*> WRONG - does not account for different month lengths
COMPUTE WS-DAYS = WS-DATE-2 - WS-DATE-1
*> 20240301 - 20240228 = 73, not 2!
*> CORRECT
COMPUTE WS-DAYS =
FUNCTION INTEGER-OF-DATE(WS-DATE-2)
- FUNCTION INTEGER-OF-DATE(WS-DATE-1)
Mistake 5: Assuming 30 Days per Month
*> WRONG for financial calculations
COMPUTE WS-MONTHS = WS-DAYS / 30
*> Correct approach: compare year/month components
21.15 Chapter Summary
Date and time processing requires disciplined attention to detail that reflects COBOL's core strength: reliability in critical business systems. In this chapter, you have learned:
- Y2K was real, expensive, and instructive. The $300+ billion remediation effort proved both COBOL's criticality and the importance of careful date handling. The lessons — always use 4-digit years, always validate dates, always test boundary conditions — remain essential today.
- COBOL supports multiple date formats: Gregorian (YYYYMMDD), Julian (YYYYDDD), packed dates, integer dates, and timestamps. Choose the right format for each use case.
- FUNCTION CURRENT-DATE provides the current date, time, and GMT offset in a 21-character string. It is the starting point for most date operations.
- INTEGER-OF-DATE and DATE-OF-INTEGER enable date arithmetic by converting between calendar dates and integer day numbers. Never subtract calendar dates directly.
- Date validation is mandatory. Use comprehensive checks (numeric, range, month/day, leap year) or compiler-specific functions like TEST-DATE-YYYYMMDD.
- Language Environment date services provide additional formatting and conversion capabilities on z/OS systems.
- Aging calculations use date subtraction to categorize items into buckets (current, 30, 60, 90, 120+ days).
- Timezone handling uses the GMT offset from CURRENT-DATE, with awareness of day-boundary crossings.
- Business day calculations combine day-of-week determination (via INTEGER-OF-DATE and MOD) with holiday table lookups.
- Quarter and fiscal year calculations use month-based arithmetic with adjustments for non-calendar fiscal years.
Every production COBOL system processes dates. Getting date handling right is not glamorous work, but it is essential work — the kind of work that, done well, is invisible, and done poorly, makes headlines.
"The Y2K crisis taught an entire generation of programmers that dates are not just data — they are the heartbeat of every business system. Treat them with respect." — Maria Chen, Senior Developer, GlobalBank