Dates and Times
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; an edition 4 is upcoming, in DIS ballot), GnuCOBOL 3.2
(28 July 2023, the latest release) and IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026), and was checked on
2026-10-20. Every portable example below was compiled with cobc -x -Wall (GnuCOBOL 3.2.0, fixed reference format) without a
single warning (except the two examples that are deliberately wrong and say so) and run; the output and the diagnostics shown are real. The ISO standard is paywalled and was not read: the rules
come from the Enterprise COBOL manuals and the GnuCOBOL manuals, and where the two differ the page says so.
A date in a COBOL program is not a type: it is a number or a string that the program interprets. The language gives three families of tools for working with that number, and the page follows them in turn:
-
the
ACCEPT ... FROM DATE|DAY|DAY-OF-WEEK|TIMEstatement, which is among the oldest features of the language and reads the system clock into a data item; -
FUNCTION CURRENT-DATEand the other intrinsic functions, which return four-digit years and the offset from UTC and can convert a calendar date into a number of days, add to it and convert it back; -
the ISO 8601 functions (
FORMATTED-DATE,FORMATTED-DATETIME,INTEGER-OF-FORMATTED-DATEand their relatives), which read and write the text forms that web services and JSON use.
The examples use the layouts of the Circulation library back office, in particular LOAN-DATE in LOANREC, and the page ends with the
rule that computes when a book is due and how many days late it is. The general syntax of intrinsic functions, and the table of every
function, are in Intrinsic Functions; the numeric items that hold the dates are
described in PICTURE Clause and
USAGE and Numeric Representation.
A word about reproducibility. Anything that reads the clock produces a different result every time it is run, so the examples below are of
two kinds. Those that use fixed dates (a literal, a VALUE clause or a record from a file) print the same output on any day and any
machine. The two that read the clock (accept-demo and live-clock, and the last lines of current-date) are labelled "varies": the output
shown is what the run on 2026-10-20 printed, with a container whose time zone is UTC.
ACCEPT FROM DATE, DAY, DAY-OF-WEEK and TIME
Format 2 of the ACCEPT statement (Enterprise COBOL Language Reference, ACCEPT statement; GnuCOBOL Programmer’s Guide, ACCEPT FROM DATE/TIME)
moves the date or the time kept by the system into a receiving item. Nothing is read from a device: the source is one of six
conceptual data items that cannot be declared in the program and that behave as USAGE DISPLAY numbers, moved to the receiver by the rules of
MOVE (without CORRESPONDING).
| Phrase | Implicit PICTURE |
Layout | Example for 27 April 2003 |
|---|---|---|---|
|
|
yymmdd |
|
|
|
yyyymmdd |
|
|
|
yyddd (day of the year) |
|
|
|
yyyyddd |
|
|
|
1 (Monday) to 7 (Sunday) |
a Sunday is |
|
|
hhmmsscc (hundredths of a second) |
2:41 PM is |
The YYYYMMDD and YYYYDDD forms came with COBOL 2002; Enterprise COBOL accepts them and so does GnuCOBOL. The receiver does not have to be a numeric
item: an alphanumeric item receives the digits as text, and an alphanumeric group item is allowed as the receiver. The TIME and
DATE receivers are therefore often declared as groups whose subordinate items pick the fields apart, as the first example does by redefining the time.
IDENTIFICATION DIVISION.
PROGRAM-ID. ACCEPT-DEMO.
* ACCEPT ... FROM the clock: the values change on every run.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-DATE-6 PIC 9(6).
01 WS-DATE-8 PIC 9(8).
01 WS-DAY-5 PIC 9(5).
01 WS-DAY-7 PIC 9(7).
01 WS-WEEKDAY PIC 9.
01 WS-TIME PIC 9(8).
01 WS-TIME-PARTS REDEFINES WS-TIME.
05 WS-HOUR PIC 99.
05 WS-MINUTE PIC 99.
05 WS-SECOND PIC 99.
05 WS-HUNDREDTH PIC 99.
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
ACCEPT WS-DATE-6 FROM DATE
ACCEPT WS-DATE-8 FROM DATE YYYYMMDD
ACCEPT WS-DAY-5 FROM DAY
ACCEPT WS-DAY-7 FROM DAY YYYYDDD
ACCEPT WS-WEEKDAY FROM DAY-OF-WEEK
ACCEPT WS-TIME FROM TIME
DISPLAY "DATE " WS-DATE-6
DISPLAY "DATE YYYYMMDD " WS-DATE-8
DISPLAY "DAY " WS-DAY-5
DISPLAY "DAY YYYYDDD " WS-DAY-7
DISPLAY "DAY-OF-WEEK " WS-WEEKDAY
DISPLAY "TIME " WS-TIME
DISPLAY " hour " WS-HOUR " minute " WS-MINUTE
" second " WS-SECOND " hundredths " WS-HUNDREDTH
STOP RUN.
The program prints the clock in every format it supports. This output varies with every run: it is the run of 2026-10-20 at 13:04:21 UTC,
a Tuesday (DAY-OF-WEEK 2) and the 293rd day of 2026.
DATE 261020
DATE YYYYMMDD 20261020
DAY 26293
DAY YYYYDDD 2026293
DAY-OF-WEEK 2
TIME 13095666
hour 13 minute 09 second 56 hundredths 66
The same program also compiled and ran under -std=ibm and printed the same kinds of values. Three habits make the statement safer:
-
Take the date once.
ACCEPT FROM DATEfollowed byACCEPT FROM TIMEare two reads of the clock. A job that runs at 23:59:59 can get yesterday’s date and tomorrow’s time. Reading oneFUNCTION CURRENT-DATE(next section) returns a consistent date and time. -
Prefer the four-digit forms.
ACCEPT FROM DATEandACCEPT FROM DAYreturn two-digit years, so990101and000101do not sort in time order. TheYYYYMMDDandYYYYDDDphrases, orCURRENT-DATE, return the century. -
Remember that this is local time.
DATEandTIMEare the system’s local time, which with GnuCOBOL follows theTZenvironment variable of the process and on z/OS follows the system’s time settings. A server in another time zone reports another day for the same instant.
FUNCTION CURRENT-DATE
FUNCTION CURRENT-DATE takes no arguments and returns a 21-character alphanumeric value, so a single MOVE fills a group that names the pieces. The
layout is defined by the standard and is the same in Enterprise COBOL and GnuCOBOL:
| Positions | Length | Contents |
|---|---|---|
1-4 |
4 |
year in the Gregorian calendar |
5-6 |
2 |
month, 01 to 12 |
7-8 |
2 |
day of the month, 01 to 31 |
9-10 |
2 |
hours past midnight, 00 to 23 |
11-12 |
2 |
minutes, 00 to 59 |
13-14 |
2 |
seconds, 00 to 59 |
15-16 |
2 |
hundredths of a second ( |
17 |
1 |
|
18-19 |
2 |
hours of the offset (00 to 12 behind, 00 to 13 ahead) |
20-21 |
2 |
additional minutes of the offset (00 to 59) |
Enterprise COBOL’s manual works the example of 15 February 1995, 05:14:27.81 in Eastern Standard Time, where the value is 1995021505142781-0500. The
following program redefines the 21 characters in a group, parses that literal (so that its first part prints the same every day) and then reads the live clock. The last
lines vary.
IDENTIFICATION DIVISION.
PROGRAM-ID. CURRENT-DATE-DEMO.
* Layout of FUNCTION CURRENT-DATE (21 characters).
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-STAMP.
05 WS-DATE.
10 WS-YEAR PIC 9(4).
10 WS-MONTH PIC 9(2).
10 WS-DAY PIC 9(2).
05 WS-TIME.
10 WS-HOUR PIC 9(2).
10 WS-MINUTE PIC 9(2).
10 WS-SECOND PIC 9(2).
10 WS-HUNDREDTH PIC 9(2).
05 WS-OFFSET.
10 WS-OFFSET-SIGN PIC X.
10 WS-OFFSET-HOURS PIC 9(2).
10 WS-OFFSET-MINUTES PIC 9(2).
01 WS-RUN-DATE PIC 9(8).
01 WS-LENGTH PIC 99.
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
* A fixed 21-character value: 15 Feb 1995, 05:14:27.81, UTC-5.
MOVE "1995021505142781-0500" TO WS-STAMP
DISPLAY "year " WS-YEAR " month " WS-MONTH " day " WS-DAY
DISPLAY "time " WS-HOUR ":" WS-MINUTE ":" WS-SECOND "."
WS-HUNDREDTH
DISPLAY "offset " WS-OFFSET-SIGN WS-OFFSET-HOURS ":"
WS-OFFSET-MINUTES
* The live clock: this part changes on every run.
MOVE FUNCTION CURRENT-DATE TO WS-STAMP
DISPLAY "now " WS-STAMP
MOVE FUNCTION LENGTH(FUNCTION CURRENT-DATE) TO WS-LENGTH
DISPLAY "length " WS-LENGTH
MOVE FUNCTION CURRENT-DATE(1:8) TO WS-RUN-DATE
DISPLAY "run date " WS-RUN-DATE
STOP RUN.
year 1995 month 02 day 15
time 05:14:27.81
offset -05:00
now 2026102013095712+0000
length 21
run date 20261020
Two details are visible in the output. The live line ends in +0000 because the container runs in UTC; running the same binary with TZ=America/New_York printed
-0400 at that moment (daylight saving time), so the offset really follows the time zone of the process. And the reference-modified form
FUNCTION CURRENT-DATE(1:8) gives the yyyymmdd run date directly, which is how the nightly Circulation run obtains "today". The
offset in positions 17 to 21 is the difference between local time and Greenwich mean time, which is why a value stored with it can be
reduced to UTC later; the formatted functions below return the same information in ISO 8601 form.
Integer dates: arithmetic on calendar dates
Subtracting two yyyymmdd numbers does not give a number of days, because the digits are a calendar and not a count. The standard solves this with
an integer date: the number of days since a fixed day zero, which can be added to and subtracted from, and converted back. Day 1 is Monday 1
January 1601 (in the Gregorian calendar, reckoned backwards), and the range is integer 1 (16010101) to 3,067,671 (99991231). The functions are:
| Function | Argument | Result |
|---|---|---|
|
standard date |
integer date |
|
integer date |
standard date |
|
Julian date |
integer date |
|
integer date |
Julian date |
|
an integer |
0 if valid, 1 bad year, 2 bad month, 3 bad day |
|
an integer |
0 if valid, 1 bad year, 2 bad day of the year |
The next program does the usual things: converts a date to an integer, adds 14 days (the loan period used below), converts back, gets the Julian form, finds the weekday and counts days between two dates. It prints the same output on any day because every date is fixed.
IDENTIFICATION DIVISION.
PROGRAM-ID. INTEGER-DATES.
* Integer dates: day 1 is Monday 1 January 1601.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-DATE PIC 9(8) VALUE 20261020.
01 WS-JULIAN PIC 9(7).
01 WS-INT PIC 9(7).
01 WS-OUT PIC 9(8).
01 WS-OUT-JULIAN PIC 9(7).
01 WS-WEEKDAY PIC 9.
01 WS-DAYS PIC S9(5).
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
COMPUTE WS-INT = FUNCTION INTEGER-OF-DATE(WS-DATE)
DISPLAY "integer date of " WS-DATE " = " WS-INT
COMPUTE WS-OUT = FUNCTION DATE-OF-INTEGER(WS-INT + 14)
DISPLAY "14 days later = " WS-OUT
COMPUTE WS-OUT-JULIAN = FUNCTION DAY-OF-INTEGER(WS-INT)
DISPLAY "Julian form = " WS-OUT-JULIAN
MOVE WS-OUT-JULIAN TO WS-JULIAN
COMPUTE WS-INT = FUNCTION INTEGER-OF-DAY(WS-JULIAN)
DISPLAY "back from Julian = " WS-INT
COMPUTE WS-WEEKDAY = FUNCTION MOD(WS-INT - 1, 7) + 1
DISPLAY "day of week (1=Mon) = " WS-WEEKDAY
COMPUTE WS-DAYS = FUNCTION INTEGER-OF-DATE(20261231)
- FUNCTION INTEGER-OF-DATE(20261020)
DISPLAY "days to 31 Dec 2026 = " WS-DAYS
DISPLAY "day 1 = "
FUNCTION DATE-OF-INTEGER(1)
DISPLAY "day 3067671 = "
FUNCTION DATE-OF-INTEGER(3067671)
* 29 February: only leap years have it.
DISPLAY "2028-02-28 + 1 = "
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(20280228) + 1)
DISPLAY "2100-02-28 + 1 = "
FUNCTION DATE-OF-INTEGER(
FUNCTION INTEGER-OF-DATE(21000228) + 1)
STOP RUN.
integer date of 20261020 = 0155521
14 days later = 20261103
Julian form = 2026293
back from Julian = 0155521
day of week (1=Mon) = 2
days to 31 Dec 2026 = +00072
day 1 = 16010101
day 3067671 = 99991231
2028-02-28 + 1 = 20280229
2100-02-28 + 1 = 21000301
The output shows the leap-year rule at work: 2028 has a 29 February and 2100, being a multiple of 100 and not of 400, does not. The
weekday expression MOD(integer - 1, 7) + 1 gives 1 for Monday to 7 for Sunday, the same numbering as ACCEPT FROM DAY-OF-WEEK; it is valid
because day 1 is a Monday, and it needs care in Enterprise COBOL, where the INTDATE option can move the origin (see the table below).
Validate before converting
The conversion functions require a valid argument (the Enterprise COBOL manual states the valid forms and ranges, and TEST-DATE-YYYYMMDD exists to check them). The test function returns which part is wrong, in the order year, month, day, so a rejection message can be specific. Here are
six candidates, the Julian test, and what GnuCOBOL’s INTEGER-OF-DATE does with an invalid date that nobody tested.
IDENTIFICATION DIVISION.
PROGRAM-ID. TEST-DATES.
* Validating dates before using them.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-CANDIDATES.
05 FILLER PIC 9(8) VALUE 20261020.
05 FILLER PIC 9(8) VALUE 20260230.
05 FILLER PIC 9(8) VALUE 20261320.
05 FILLER PIC 9(8) VALUE 15991231.
05 FILLER PIC 9(8) VALUE 20280229.
05 FILLER PIC 9(8) VALUE 21000229.
01 WS-TABLE REDEFINES WS-CANDIDATES.
05 WS-CANDIDATE PIC 9(8) OCCURS 6 TIMES.
01 WS-I PIC 9.
01 WS-RESULT PIC 9.
01 WS-JULIAN-RESULT PIC 9.
01 WS-BAD PIC 9(8) VALUE 20260230.
01 WS-INT PIC 9(7).
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 6
COMPUTE WS-RESULT =
FUNCTION TEST-DATE-YYYYMMDD(WS-CANDIDATE(WS-I))
DISPLAY WS-CANDIDATE(WS-I) " -> " WS-RESULT
END-PERFORM
COMPUTE WS-JULIAN-RESULT = FUNCTION TEST-DAY-YYYYDDD(2027366)
DISPLAY "Julian 2027366 -> " WS-JULIAN-RESULT
COMPUTE WS-JULIAN-RESULT = FUNCTION TEST-DAY-YYYYDDD(2028366)
DISPLAY "Julian 2028366 -> " WS-JULIAN-RESULT
* An unchecked invalid date reaches INTEGER-OF-DATE.
COMPUTE WS-INT = FUNCTION INTEGER-OF-DATE(WS-BAD)
DISPLAY "INTEGER-OF-DATE(20260230) = " WS-INT
STOP RUN.
20261020 -> 0
20260230 -> 3
20261320 -> 2
15991231 -> 1
20280229 -> 0
21000229 -> 3
Julian 2027366 -> 2
Julian 2028366 -> 0
INTEGER-OF-DATE(20260230) = 0000000
The returned codes match the manual: 15991231 is year 1 (below 1601), 20261320 is month 2 and 20260230 is day 3. The last line is the trap: GnuCOBOL does not stop for
20260230; it returns 0, which a program that computes with it treats as "no such day". The Enterprise COBOL function descriptions state the valid range and do not promise a result outside it, so the portable rule is the same in both: call
TEST-DATE-YYYYMMDD first and reject the record.
Where integer day 1 is: INTDATE
On z/OS the compiler option INTDATE(ANSI) (the default) makes day 1 January 1, 1601, as above. INTDATE(LILIAN) makes day 1 October 15, 1582, the
origin used by Language Environment’s own date services. The Programming Guide tabulates which combinations agree:
| Option | Intrinsic function | Language Environment service |
|---|---|---|
|
|
compatible with |
|
|
not compatible with |
|
|
compatible with |
|
|
compatible with |
A program that exchanges integer dates with other programs or with those services must say which origin it uses, and the difference of two integer dates (a
count of days) is the same in both. GnuCOBOL has no INTDATE option: its integer dates always start at 1600-12-31, like INTDATE(ANSI). These are z/OS facts and are
not run here.
The ISO 8601 date and time functions
The standard of 2014 added a second family that reads and writes dates as text in ISO 8601 shapes. Enterprise COBOL 6.5 lists it among the 2002/2014 standard features it implements; the outline for this section dates it to the 6.3 service stream, a release this page could not re-check, so it names no release. GnuCOBOL 3.2 implements the family too. The functions are:
| Function | Purpose |
|---|---|
|
integer date to text in a date format |
|
seconds past midnight to text in a time format |
|
both, to text in a combined format |
|
the current date and time in a combined format |
|
text in a date (or combined) format to an integer date |
|
text in a time (or combined) format to seconds past midnight |
|
0 if the text fits the format, otherwise the position of the first bad character |
|
one number: the integer date, a decimal point and the seconds as five digits |
|
the current local time of day in seconds |
The format is an alphanumeric (or national, or UTF-8) literal built from the strings in the Language Reference. Date formats: YYYYMMDD and
YYYY-MM-DD (calendar), YYYYDDD and YYYY-DDD (ordinal, the day of the year) and YYYYWwwD and YYYY-Www-D (ISO week date: year, week 01 to 52, day 1 to 7). Time formats:
hhmmss and hh:mm:ss, a fraction after the seconds written .s to .sssssssss (one to nine digits), the suffix Z for UTC, and the offset forms
hhmmss+hhmm and hh:mm:ss+hh:mm. A combined format joins a date format and a time format with an uppercase T, both
in the basic or both in the extended style: YYYY-MM-DDThh:mm:ss+hh:mm is allowed and YYYYMMDDThh:mm:ss is not.
The program below formats a fixed date and time (14:30:25 is 52,225 seconds past midnight), shows the same instant as UTC for a time zone two hours
ahead (offset 120 minutes), and parses text back. The TEST-FORMATTED-DATETIME results are positions: month 13 is wrong at character 7 of
2026-13-03, and 0 means valid.
IDENTIFICATION DIVISION.
PROGRAM-ID. FORMATTED-DEMO.
* ISO 8601 style date and time functions (Enterprise COBOL 6.x,
* GnuCOBOL 3.2). Every value here is fixed, so the output is too.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-INT PIC 9(7).
01 WS-TEXT PIC X(32).
01 WS-SECONDS PIC 9(5)V99.
01 WS-CHECK PIC 99.
01 WS-COMBINED PIC 9(7)V9(5).
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
COMPUTE WS-INT = FUNCTION INTEGER-OF-DATE(20261020)
MOVE FUNCTION FORMATTED-DATE("YYYY-MM-DD", WS-INT)
TO WS-TEXT
DISPLAY "extended calendar : " WS-TEXT
MOVE FUNCTION FORMATTED-DATE("YYYYDDD", WS-INT) TO WS-TEXT
DISPLAY "basic ordinal : " WS-TEXT
MOVE FUNCTION FORMATTED-DATE("YYYY-Www-D", WS-INT)
TO WS-TEXT
DISPLAY "extended week : " WS-TEXT
MOVE FUNCTION FORMATTED-TIME("hh:mm:ss", 52225) TO WS-TEXT
DISPLAY "time of day : " WS-TEXT
MOVE FUNCTION FORMATTED-DATETIME(
"YYYY-MM-DDThh:mm:ss", WS-INT, 52225)
TO WS-TEXT
DISPLAY "date and time : " WS-TEXT
MOVE FUNCTION FORMATTED-DATETIME(
"YYYY-MM-DDThh:mm:ssZ", WS-INT, 52225, 120)
TO WS-TEXT
DISPLAY "in UTC (+02:00) : " WS-TEXT
MOVE FUNCTION FORMATTED-DATETIME(
"YYYY-MM-DDThh:mm:ss+hh:mm", WS-INT, 52225, 120)
TO WS-TEXT
DISPLAY "with offset : " WS-TEXT
* And back again.
COMPUTE WS-INT = FUNCTION INTEGER-OF-FORMATTED-DATE(
"YYYY-MM-DD", "2026-11-03")
DISPLAY "integer of text : " WS-INT
COMPUTE WS-SECONDS = FUNCTION SECONDS-FROM-FORMATTED-TIME(
"hh:mm:ss", "14:30:25")
DISPLAY "seconds : " WS-SECONDS
COMPUTE WS-CHECK = FUNCTION TEST-FORMATTED-DATETIME(
"YYYY-MM-DD", "2026-13-03")
DISPLAY "bad month at char : " WS-CHECK
COMPUTE WS-CHECK = FUNCTION TEST-FORMATTED-DATETIME(
"YYYY-MM-DD", "2026-11-03")
DISPLAY "good date : " WS-CHECK
COMPUTE WS-COMBINED = FUNCTION COMBINED-DATETIME(
155521, 52225)
DISPLAY "combined : " WS-COMBINED
STOP RUN.
extended calendar : 2026-10-20
basic ordinal : 2026293
extended week : 2026-W43-2
time of day : 14:30:25
date and time : 2026-10-20T14:30:25
in UTC (+02:00) : 2026-10-20T12:30:25Z
with offset : 2026-10-20T14:30:25+02:00
integer of text : 0155535
seconds : 52225.00
bad month at char : 07
good date : 00
combined : 0155521.52225
Notes on the output. 2026-W43-2 is the ISO week date: week 43, Tuesday. With a Z format the result is the time converted to UTC (12:30:25 for 14:30:25 at +02:00); with an
offset format the time is shown as given and the offset is appended. COMBINED-DATETIME(155521, 52225) is the integer date, a decimal point and the seconds
padded to five digits. INTEGER-OF-FORMATTED-DATE("YYYY-MM-DD", "2026-11-03") returns 155535, fourteen days after 155521, the figure that the due date calculation below also reaches.
The functions that read the clock end the family. FORMATTED-CURRENT-DATE takes a combined format and returns the current date and time in it, and SECONDS-PAST-MIDNIGHT returns the
local time of day as a number that supports arithmetic. The next program uses both and also computes the seconds between 23:55 and 00:05, with the extra step that crossing midnight
requires. Its first three lines vary with every run.
IDENTIFICATION DIVISION.
PROGRAM-ID. LIVE-CLOCK.
* Clock-dependent functions: the output differs on every run.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-STAMP PIC X(32).
01 WS-NOW PIC 9(5)V99.
01 WS-START PIC 9(5)V99 VALUE 86100.
01 WS-END PIC 9(5)V99 VALUE 300.
01 WS-ELAPSED PIC 9(5)V99.
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
MOVE FUNCTION FORMATTED-CURRENT-DATE(
"YYYY-MM-DDThh:mm:ss.ss+hh:mm") TO WS-STAMP
DISPLAY "formatted now : " WS-STAMP
MOVE FUNCTION FORMATTED-CURRENT-DATE("YYYYMMDDThhmmssZ")
TO WS-STAMP
DISPLAY "formatted (UTC) : " WS-STAMP
COMPUTE WS-NOW = FUNCTION SECONDS-PAST-MIDNIGHT
DISPLAY "seconds past midnight : " WS-NOW
* Elapsed time across midnight: 23:55:00 to 00:05:00.
IF WS-END < WS-START
COMPUTE WS-ELAPSED = 86400 - WS-START + WS-END
ELSE
COMPUTE WS-ELAPSED = WS-END - WS-START
END-IF
DISPLAY "elapsed seconds : " WS-ELAPSED
STOP RUN.
formatted now : 2026-10-20T13:09:58.65+00:00
formatted (UTC) : 20261020T130958Z
seconds past midnight : 47398.00
elapsed seconds : 00600.00
Source: Enterprise COBOL for z/OS 6.5 Language Reference, FORMATTED-CURRENT-DATE and SECONDS-PAST-MIDNIGHT
Format literals are checked at compile time
Because the format is described by a literal, the compiler can check it. A format that is not in the tables is an error, not a runtime surprise. This deliberately wrong program uses a slash as the separator:
IDENTIFICATION DIVISION.
PROGRAM-ID. BAD-FORMAT.
* Deliberately wrong: a format literal that is not in the tables.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-TEXT PIC X(12).
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
MOVE FUNCTION FORMATTED-DATE("YYYY/MM/DD", 155521)
TO WS-TEXT
DISPLAY WS-TEXT
STOP RUN.
GnuCOBOL refuses to compile it:
bad-format.cob: in paragraph 'MAIN-PARAGRAPH':
bad-format.cob:9: error: FUNCTION 'FORMATTED-DATE' has invalid date/time format
7 | PROCEDURE DIVISION.
8 | MAIN-PARAGRAPH.
9 > MOVE FUNCTION FORMATTED-DATE("YYYY/MM/DD", 155521)
10 | TO WS-TEXT
11 | DISPLAY WS-TEXT
The Language Reference requires the first argument of FORMATTED-DATE, FORMATTED-DATETIME, FORMATTED-TIME and FORMATTED-CURRENT-DATE to be a literal. GnuCOBOL tolerates
a data item but warns, and then reads it at run time. The next program does that, and also passes integer date 0, which is outside the valid range:
IDENTIFICATION DIVISION.
PROGRAM-ID. VAR-FORMAT.
* Deliberately questionable: the format is not a literal.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-FORMAT PIC X(10) VALUE "YYYY-MM-DD".
01 WS-TEXT PIC X(10).
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
MOVE FUNCTION FORMATTED-DATE(WS-FORMAT, 155521) TO WS-TEXT
DISPLAY "[" WS-TEXT "]"
MOVE FUNCTION FORMATTED-DATE("YYYY-MM-DD", 0) TO WS-TEXT
DISPLAY "[" WS-TEXT "]"
STOP RUN.
var-format.cob: in paragraph 'MAIN-PARAGRAPH':
var-format.cob:10: warning: FUNCTION 'FORMATTED-DATE' has format in variable [-Wadditional]
8 | PROCEDURE DIVISION.
9 | MAIN-PARAGRAPH.
10 > MOVE FUNCTION FORMATTED-DATE(WS-FORMAT, 155521) TO WS-TEXT
11 | DISPLAY "[" WS-TEXT "]"
12 | MOVE FUNCTION FORMATTED-DATE("YYYY-MM-DD", 0) TO WS-TEXT
[2026-10-20]
[ ]
The second line shows what GnuCOBOL does with an integer date outside 1 to 3,067,671: the result is blank, with no message. A program that needs to know must test the integer first. Do not rely on a variable format: the portable form is the literal, which the compiler checks and which Enterprise COBOL requires.
Computing a due date and days overdue in Circulation
The Circulation rule is simple: a book is due 14 days after the checkout, and every day after the due date costs a daily fine, with a maximum per loan. The
steps are the ones the page has built: validate the loan date, convert it to an integer date, add the loan period, convert back to print, subtract from the run date’s integer
to count the days late (never below zero), and multiply. The run date is a fixed 20261105 so that the output is reproducible; a real run would replace its VALUE clause with
MOVE FUNCTION CURRENT-DATE(1:8) TO WS-AS-OF, or, better, would read it from a control card so that a rerun after a failure uses the original date.
The daily fine (0.25) and its cap (10.00) are values invented for the example.
A second sample file has five transactions chosen to hit the interesting cases: an ordinary loan, a loan from February whose due date is long past, a loan on the last day of the year (whose due date falls in the next year), a loan with an impossible date and a return.
O00010000000120261020
O00010100000220260227
O00010000000320261231
O00010200000420260230
R00010000000120261029
The record layout is LOANREC, 30 bytes: type, member, book, LOAN-DATE and nine bytes of filler.
IDENTIFICATION DIVISION.
PROGRAM-ID. OVERDUE.
* Circulation: due date and days overdue for checkouts.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT LOANTRAN ASSIGN TO "LOANTRAN2"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-LOAN-STATUS.
DATA DIVISION.
FILE SECTION.
FD LOANTRAN.
COPY LOANREC.
WORKING-STORAGE SECTION.
01 WS-LOAN-STATUS PIC XX.
88 LOAN-OK VALUE "00".
88 LOAN-EOF VALUE "10".
01 WS-LOAN-DAYS PIC 9(3) VALUE 14.
01 WS-DAILY-FINE PIC 9V99 VALUE 0.25.
01 WS-MAX-FINE PIC 99V99 VALUE 10.00.
* Fixed "today", so that the output below is reproducible.
* A real run would use FUNCTION CURRENT-DATE(1:8) instead.
01 WS-AS-OF PIC 9(8) VALUE 20261105.
01 WS-AS-OF-INT PIC 9(7).
01 WS-LOAN-INT PIC 9(7).
01 WS-DUE-INT PIC 9(7).
01 WS-DUE-DATE PIC 9(8).
01 WS-OVERDUE PIC 9(5).
01 WS-FINE PIC 99V99.
01 WS-FINE-OUT PIC Z9.99.
01 WS-OVERDUE-OUT PIC ZZ9.
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
COMPUTE WS-AS-OF-INT = FUNCTION INTEGER-OF-DATE(WS-AS-OF)
DISPLAY "Circulation run date " WS-AS-OF
OPEN INPUT LOANTRAN
PERFORM READ-LOAN
PERFORM UNTIL LOAN-EOF
EVALUATE TRUE
WHEN NOT LOAN-CHECKOUT
DISPLAY "member " LOAN-MEMBER-ID " book "
LOAN-BOOK-ID " returned " LOAN-DATE
WHEN FUNCTION TEST-DATE-YYYYMMDD(LOAN-DATE) NOT = 0
DISPLAY "member " LOAN-MEMBER-ID " book "
LOAN-BOOK-ID " REJECTED, bad date "
LOAN-DATE
WHEN OTHER
PERFORM CHECK-OVERDUE
END-EVALUATE
PERFORM READ-LOAN
END-PERFORM
CLOSE LOANTRAN
STOP RUN.
READ-LOAN.
READ LOANTRAN
AT END SET LOAN-EOF TO TRUE
END-READ.
CHECK-OVERDUE.
COMPUTE WS-LOAN-INT = FUNCTION INTEGER-OF-DATE(LOAN-DATE)
COMPUTE WS-DUE-INT = WS-LOAN-INT + WS-LOAN-DAYS
MOVE FUNCTION DATE-OF-INTEGER(WS-DUE-INT) TO WS-DUE-DATE
IF WS-AS-OF-INT > WS-DUE-INT
COMPUTE WS-OVERDUE = WS-AS-OF-INT - WS-DUE-INT
ELSE
MOVE ZERO TO WS-OVERDUE
END-IF
COMPUTE WS-FINE = FUNCTION MIN(WS-OVERDUE * WS-DAILY-FINE,
WS-MAX-FINE)
MOVE WS-OVERDUE TO WS-OVERDUE-OUT
MOVE WS-FINE TO WS-FINE-OUT
DISPLAY "member " LOAN-MEMBER-ID " book " LOAN-BOOK-ID
" out " LOAN-DATE " due " WS-DUE-DATE
" overdue " WS-OVERDUE-OUT " days, fine "
WS-FINE-OUT.
Circulation run date 20261105
member 000100 book 000001 out 20261020 due 20261103 overdue 2 days, fine 0.50
member 000101 book 000002 out 20260227 due 20260313 overdue 237 days, fine 10.00
member 000100 book 000003 out 20261231 due 20270114 overdue 0 days, fine 0.00
member 000102 book 000004 REJECTED, bad date 20260230
member 000100 book 000001 returned 20261029
Reading the output. Member 100’s first loan (20 October) is due 3 November and is two days late on 5 November. The February loan has been out for 237 days past due, and the
fine stops at the cap. The loan of 31 December 2026 is due 14 January 2027: adding 14 to the integer date handled the year change that
20261231 + 14 could never have produced. The loan dated 20260230 was rejected by TEST-DATE-YYYYMMDD instead of becoming a silent 0. The program is plain
enough to run on z/OS with the same logic; only the file would be USERID.CIRC.LOANTRAN and the program would take its run date from PARM or a control card
(see ACCEPT, DISPLAY and Console I/O).
The mistake this replaces
The wrong way is to subtract the two yyyymmdd numbers. It looks plausible, and it is right only inside one month. This deliberately wrong program does it next to the right way:
IDENTIFICATION DIVISION.
PROGRAM-ID. DATE-SUBTRACTION.
* Deliberately wrong: subtracting yyyymmdd numbers.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-LOAN PIC 9(8) VALUE 20261020.
01 WS-AS-OF PIC 9(8) VALUE 20261105.
01 WS-WRONG PIC S9(8).
01 WS-RIGHT PIC S9(8).
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
COMPUTE WS-WRONG = WS-AS-OF - WS-LOAN
COMPUTE WS-RIGHT = FUNCTION INTEGER-OF-DATE(WS-AS-OF)
- FUNCTION INTEGER-OF-DATE(WS-LOAN)
DISPLAY "yyyymmdd subtraction : " WS-WRONG
DISPLAY "integer dates : " WS-RIGHT
STOP RUN.
yyyymmdd subtraction : +00000085
integer dates : +00000016
From 20 October to 5 November is 16 days, and the subtraction of the raw numbers says 85, because 20261105 - 20261020 is a difference between two
digit strings that skips the whole gap between 31 and 100 in the month position. The compiler reports nothing, and a fine computed from it is wrong
without anyone being told. Dates are converted to integer dates before they are subtracted or compared for distance (a plain IF DATE-A > DATE-B on two
yyyymmdd values is fine for ordering).
Two-digit years and windowing (legacy)
Everything above uses four-digit years. Old programs, and records that still carry a yymmdd field, do not. Before the year 2000 the usual
way to save two digits per date was ACCEPT ... FROM DATE, and the Y2K work of the late 1990s was largely the job of finding such fields. The two
practical ways out were to expand the field to four digits (the permanent fix) or to window it: keep two digits and decide the century from a pivot. The standard
has intrinsic functions for the second approach, which are the only part of the old Y2K tooling that is still current:
| Function | Argument | Returns |
|---|---|---|
|
two-digit year |
four-digit year |
|
six-digit date |
|
|
five-digit Julian date |
|
The window argument, default 50, is added to the year at execution time to give the last year of a 100-year interval into which the two-digit year must fall. The IBM manual’s table makes the sliding
rule concrete: in 2002, 851003 with window 120 is 20851003 (interval 2023 to 2122), with window -20 it is 18851003 (1883 to 1982) and with window 10 it is 19851003 (1913 to 2012). Because the interval
moves with the clock, the same data gives different results in different years; the result is only stable when the data is regularly rewritten. GnuCOBOL’s functions accept a third argument,
the "year of execution" (an extension, with no counterpart in the Enterprise COBOL syntax), which lets this program fix the year so that its output is repeatable. The first
four lines reproduce the IBM table, and the last two reproduce the YEAR-TO-YYYY table.
IDENTIFICATION DIVISION.
PROGRAM-ID. WINDOWING.
* Legacy: expanding two-digit years with a sliding window.
* The third argument fixes the "year of execution" so the
* output does not depend on the date the program is run.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-YY PIC 99.
01 WS-YYYY PIC 9(4).
01 WS-YYMMDD PIC 9(6).
01 WS-YYYYMMDD PIC 9(8).
01 WS-YYDDD PIC 9(5).
01 WS-YYYYDDD PIC 9(7).
01 WS-PIVOT PIC 99 VALUE 50.
01 WS-YY-A PIC 99 VALUE 99.
01 WS-YY-B PIC 99 VALUE 0.
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
MOVE 851003 TO WS-YYMMDD
COMPUTE WS-YYYYMMDD =
FUNCTION DATE-TO-YYYYMMDD(WS-YYMMDD, 120, 2002)
DISPLAY "851003, window +120, in 2002 : " WS-YYYYMMDD
COMPUTE WS-YYYYMMDD =
FUNCTION DATE-TO-YYYYMMDD(WS-YYMMDD, -20, 2002)
DISPLAY "851003, window -20, in 2002 : " WS-YYYYMMDD
COMPUTE WS-YYYYMMDD =
FUNCTION DATE-TO-YYYYMMDD(WS-YYMMDD, 10, 2002)
DISPLAY "851003, window +10, in 2002 : " WS-YYYYMMDD
MOVE 10004 TO WS-YYDDD
COMPUTE WS-YYYYDDD =
FUNCTION DAY-TO-YYYYDDD(WS-YYDDD, -20, 2002)
DISPLAY "10004 Julian, -20, in 2002 : " WS-YYYYDDD
MOVE 26 TO WS-YY
COMPUTE WS-YYYY = FUNCTION YEAR-TO-YYYY(WS-YY, 50, 2026)
DISPLAY "26 with the default window 50 in 2026 : " WS-YYYY
MOVE 76 TO WS-YY
COMPUTE WS-YYYY = FUNCTION YEAR-TO-YYYY(WS-YY, 50, 2026)
DISPLAY "76 with the default window 50 in 2026 : " WS-YYYY
MOVE 77 TO WS-YY
COMPUTE WS-YYYY = FUNCTION YEAR-TO-YYYY(WS-YY, 50, 2026)
DISPLAY "77 with the default window 50 in 2026 : " WS-YYYY
* The same idea written by hand: a fixed pivot year.
IF WS-YY-A < WS-YY-B
DISPLAY "99 sorts before 00 : true"
ELSE
DISPLAY "99 sorts before 00 : false (two digits only)"
END-IF
MOVE WS-YY-B TO WS-YY
IF WS-YY < WS-PIVOT
COMPUTE WS-YYYY = 2000 + WS-YY
ELSE
COMPUTE WS-YYYY = 1900 + WS-YY
END-IF
DISPLAY "00 with a hand-made pivot of 50 : " WS-YYYY
COMPUTE WS-YYYY = FUNCTION YEAR-TO-YYYY(4, 23, 1995)
DISPLAY "IBM table: 4, +23, in 1995 : " WS-YYYY
COMPUTE WS-YYYY = FUNCTION YEAR-TO-YYYY(98, -15, 2008)
DISPLAY "IBM table: 98, -15, in 2008 : " WS-YYYY
STOP RUN.
851003, window +120, in 2002 : 20851003
851003, window -20, in 2002 : 18851003
851003, window +10, in 2002 : 19851003
10004 Julian, -20, in 2002 : 1910004
26 with the default window 50 in 2026 : 2026
76 with the default window 50 in 2026 : 2076
77 with the default window 50 in 2026 : 1977
99 sorts before 00 : false (two digits only)
00 with a hand-made pivot of 50 : 2000
IBM table: 4, +23, in 1995 : 2004
IBM table: 98, -15, in 2008 : 1898
Source: Enterprise COBOL for z/OS 6.5 Language Reference, DATE-TO-YYYYMMDD, DAY-TO-YYYYDDD and YEAR-TO-YYYY
The default window of 50 maps 26 to 2026, 76 to 2076 and 77 to 1977 when run in 2026 (the interval is 1977 to 2076); the same program run in 2027 would map 77 to 2077. The last block of the
program also shows the two things that go wrong with two digits: 99 compares as greater than 00, so two-digit dates do not sort or compare in time order, and the hand-made IF with a fixed pivot of 50
works until the pivot is wrong for the data, which is why a fixed pivot is old code that needs a note in the program rather than a pattern to copy.
The Millennium Language Extensions: removed
Old code you may meet. Earlier Enterprise COBOL releases (up to V4) shipped the Millennium Language Extensions: the DATE FORMAT clause on a data item (for example
DATE FORMAT YYXXXX for a yymmdd-like field), the DATEPROC compiler option and YEARWINDOW, with which the compiler windowed such fields automatically during comparisons and moves. IBM removed them in Enterprise COBOL V5 and V6, and
searching the 6.5 Language Reference and Programming Guide for DATEPROC, YEARWINDOW and the DATE FORMAT clause finds nothing. A program that uses them does not compile on 6.5; the migration is one of:
-
widen the field to
PIC 9(8)orPIC 9(7)with a four-digit year and convert the stored data once (the preferred one); -
keep the stored
yymmddand callDATE-TO-YYYYMMDDorYEAR-TO-YYYYwhere the program needs a full date, as the program above does; -
leave a note beside the field naming the window and the planned year when it must be revisited.
The details of the V4 options are not documented for 6.5 and are not repeated here; the description above is from memory of the V4 documentation and is not re-verified.
Differences between the standard, Enterprise COBOL and GnuCOBOL
| Feature | ISO/IEC 1989 | Enterprise COBOL 6.5 | GnuCOBOL 3.2 |
|---|---|---|---|
|
long-standing (COBOL 85 or earlier) |
yes |
yes |
|
COBOL 2002 |
yes |
yes |
|
|
yes; |
yes; day 1 fixed at 1601-01-01 |
|
COBOL 2014 |
yes; the format must be a literal |
yes; a variable format is accepted with a warning |
|
COBOL 2002 |
yes, two arguments |
yes, plus an optional third (year of execution) |
Millennium Language Extensions ( |
not in the standard |
removed in V5 and V6 |
not implemented |
|
extension |
not listed |
present |
Where the book is outdated
The book consulted for this section (Taulli, Modern Mainframe Development, 2022) is a survey of mainframe work and is used here only for the topic list and the Y2K narrative; nothing is copied from it. The points to carry into new code are:
-
Two-digit years are not a design to follow. Teaching material that shows
ACCEPT ... FROM DATEand then stores or compares the six digits is preserving a Y2K-era habit; new code usesFUNCTION CURRENT-DATE(1:8)orACCEPT ... FROM DATE YYYYMMDDand a four-digit year in the record layout. -
The Millennium Language Extensions that the Y2K literature describes no longer exist in the current compiler (above); windowing survives only as the three intrinsic functions.
-
The ISO 8601 functions are newer than most introductory books and than COBOL 2002; they are the way to produce and read the text dates of JSON and REST payloads, which the data-interchange pages of this section depend on.
-
The book’s Enterprise COBOL baseline (6.3) predates 6.4 and 6.5; the date functions above are in the 6.5 reference, which is the version this page follows.