, 5 min read
Converting UNIX Timestamps to Year, Month, Day in COBOL
1. Task at hand. COBOL programs reads UNIX timestamps as input. Output should be the values of year, month, day, hour, minutes, seconds.
In C this is just gmtime()
.
gmtime
accepts time_t
and produces struct tm
:
struct tm *gmtime(const time_t *timep);
On mainframe, however, it is sometimes a little inconvienent to call a C routine from COBOL. It is easier to just code the short algorithm in COBOL.
2. Approach. P.J. Plauger's book "The Standard C Library" contains the source code for gmtime()
and localtime()
.
This code is then translated to COBOL.
The C code is as below.
/* Convert UNIX timestamp to triple (year,month,day)
Elmar Klausmeier, 01-Apr-2024
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>
// From P.J.Plauger: The Standard C Library, Prentice Hall, 1992
static const int daytab[2][12] = {
{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335 }, // leap year
{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }
};
int daysTo (int year, int mon) { // compute extra days to start of month
int days;
if (year > 0) days = (year - 1) / 4; // correct for leap year: 1801-2099
else if (year <= -4) days = 1 + (4 - year) / 4;
else days = 0;
return days + daytab[year&03 || (year==0)][mon];
}
struct tm *timeTotm (struct tm *t, time_t secsarg, int isdst) { // convert scalar time to time struct
int year, mon;
const int *pm;
long i, days;
time_t secs;
static struct tm ts;
secsarg += ((70 * 365LU) + 17) * 86400; // 70 years including 17 leap days since 1900
if (t == NULL) t = &ts;
t->tm_isdst = isdst;
for (secs=secsarg; ; secs=secsarg+3600) { // loop to correct for DST (not used here)
days = secs / 86400;
t->tm_wday = (days + 1) % 7;
for (year = days / 365; days < (i=daysTo(year,0)+365L*year); --year)
; // correct guess and recheck
days -= i;
t->tm_year = year;
t->tm_yday = days;
pm = daytab[year&03 || (year==0)];
for (mon=12; days<pm[--mon]; )
;
t->tm_mon = mon;
t->tm_mday = days - pm[mon] + 1;
secs %= 86400;
t->tm_hour = secs / 3600;
secs %= 3600;
t->tm_min = secs / 60;
t->tm_sec = secs % 60;
//if (t->tm_isdst >= 0 || (t->tm_isdst = IsDST(t)) <= 0) return t;
return t;
}
}
int main (int argc, char *argv[]) {
struct tm t;
long secs;
if (argc <= 1) return 0;
secs = atol(argv[1]);
timeTotm(&t, secs, 0);
printf("timeTotm(): year=%d, mon=%d, day=%d, hr=%d, min=%d, sec=%d\n",
1900+t.tm_year, 1+t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
return 0;
}
3. COBOL solution. Below is the COBOL code which was translated from above C code.
Fun fact: GNU Cobol crashed on some intermediate result, see cobc crashes on illegal COBOL source code file. This bug was fixed within a few hours by Simon Sobisch!
Below source code is compiled without problems.
000010 IDENTIFICATION DIVISION.
000020 PROGRAM-ID. Timestamp2date.
000030 AUTHOR. Elmar Klausmeier.
000040 DATE-WRITTEN. 02-May-2024.
000050
000060 DATA DIVISION.
000070 WORKING-STORAGE SECTION.
000080*
000090 01 year PIC S9(18) comp-5.
000100 01 mon PIC S9(18) comp-5.
000110 01 days PIC S9(18) comp-5.
000120*
000130* Local helper variables
000140 01 i PIC S9(18) comp-5.
000150 01 idays PIC S9(18) comp-5.
000160 01 daysTo PIC S9(18) comp-5.
000170 01 yearMod4 PIC S9(9) comp-5.
000180 01 leapIx PIC S9(9) comp-5.
000190 01 daysP1 PIC S9(18) comp-5.
000200*
000210 01 secs PIC S9(18) comp-5.
000220 01 secsarg PIC S9(18) comp-5.
000230*
000240*
000250* struct tm:
000260* int tm_sec; // Seconds [0, 60]
000270* int tm_min; // Minutes [0, 59]
000280* int tm_hour; // Hour [0, 23]
000290* int tm_mday; // Day of the month [1, 31]
000300* int tm_mon; // Month [0, 11] (January = 0)
000310* int tm_year; // Year minus 1900
000320* int tm_wday; // Day of the week [0, 6] (Sunday = 0)
000330* int tm_yday; // Day of the year [0, 365] (Jan/01 = 0)
000340* int tm_isdst; // Daylight savings flag
000350 01 tm_sec PIC S9(9).
000360 01 tm_min PIC S9(9).
000370 01 tm_hour PIC S9(9).
000380 01 tm_mday PIC S9(9).
000390* range: 1-12
000400 01 tm_mon PIC S9(9).
000410 01 tm_year PIC S9(9).
000420 01 tm_wday PIC S9(9).
000430 01 tm_yday PIC S9(9).
000440*
000450*
000460 01 daytabInit.
000470* Number of days for leap year
000480 05 daytab-1-1 pic s9(9) comp-5 value 0.
000490 05 daytab-1-2 pic s9(9) comp-5 value 31.
000500 05 daytab-1-3 pic s9(9) comp-5 value 60.
000510 05 daytab-1-4 pic s9(9) comp-5 value 91.
000520 05 daytab-1-5 pic s9(9) comp-5 value 121.
000530 05 daytab-1-6 pic s9(9) comp-5 value 152.
000540 05 daytab-1-7 pic s9(9) comp-5 value 182.
000550 05 daytab-1-8 pic s9(9) comp-5 value 213.
000560 05 daytab-1-9 pic s9(9) comp-5 value 244.
000570 05 daytab-1-10 pic s9(9) comp-5 value 274.
000580 05 daytab-1-11 pic s9(9) comp-5 value 305.
000590 05 daytab-1-12 pic s9(9) comp-5 value 335.
000600* Number of days for non-leap year
000610 05 daytab-2-1 pic s9(9) comp-5 value 0.
000620 05 daytab-2-2 pic s9(9) comp-5 value 31.
000630 05 daytab-2-3 pic s9(9) comp-5 value 59.
000640 05 daytab-2-4 pic s9(9) comp-5 value 90.
000650 05 daytab-2-5 pic s9(9) comp-5 value 120.
000660 05 daytab-2-6 pic s9(9) comp-5 value 151.
000670 05 daytab-2-7 pic s9(9) comp-5 value 181.
000680 05 daytab-2-8 pic s9(9) comp-5 value 212.
000690 05 daytab-2-9 pic s9(9) comp-5 value 243.
000700 05 daytab-2-10 pic s9(9) comp-5 value 273.
000710 05 daytab-2-11 pic s9(9) comp-5 value 304.
000720 05 daytab-2-12 pic s9(9) comp-5 value 334.
000730 01 daytabArr redefines daytabInit.
000740 05 filler occurs 2 times.
000750 10 filler occurs 12 times.
000760 15 daytab pic s9(9) comp-5.
000770*
000780*
000790
000800 PROCEDURE DIVISION.
000810******************************************************************
000820* A100-main
000830******************************************************************
000840* Function:
000850*
000860******************************************************************
000870 A100-main SECTION.
000880 A100-main-P.
000890
000900* initialize daytabArr.
000910* move daytabInit to daytabArr
000920* perform varying leapIx from 1 by 1 until leapIx > 2
000930* perform varying mon from 1 by 1 until mon > 12
000940* display 'daytab(' leapIx ',' mon ') = '
000950* daytab(leapIx, mon)
000960* end-perform
000970* end-perform.
000980
000990 ACCEPT secsarg FROM ARGUMENT-VALUE
001000 perform v910-timeToTm
001010 display ' tm_sec = ' tm_sec
001020 display ' tm_min = ' tm_min
001030 display ' tm_hour = ' tm_hour
001040 display ' tm_mday = ' tm_mday
001050 display ' tm_mon = ' tm_mon
001060 display ' tm_year = ' tm_year
001070 display ' tm_wday = ' tm_wday
001080 display ' tm_yday = ' tm_yday
001090
001100 STOP RUN.
001110
001120
001130* Convert UNIX timestamp to triple (year,month,day)
001140* Converted from C program
001150* From P.J.Plauger: The Standard C Library, Prentice Hall, 1992
001160
001170******************************************************************
001180* V900-daysTo
001190******************************************************************
001200* Function: compute daysTo given year and mon
001210* compute extra days to start of month
001220******************************************************************
001230 V900-daysTo SECTION.
001240 V900-daysTo-P.
001250
001260* correct for leap year: 1801-2099
001270 evaluate true
001280 when year > 0
001290 compute idays = (year - 1) / 4
001300 when year <= -4
001310 compute idays = 1 + (4 - year) / 4
001320 when other
001330 move zero to idays
001340 end-evaluate
001350
001360 compute yearMod4 = function mod(year,4)
001370 if yearMod4 not= zero or year = zero then
001380 move 2 to leapIx
001390 else
001400 move 1 to leapIx
001410 end-if
001420 compute daysTo = idays + daytab(leapIx, mon)
001430
001440 CONTINUE.
001450 END-V900-daysTo.
001460 EXIT.
001470
001480
001490******************************************************************
001500* V910-timeToTm
001510******************************************************************
001520* Function: compute tmT from secsarg (seconds since 01-Jan-1970)
001530* convert scalar time to time struct
001540******************************************************************
001550 V910-timeToTm SECTION.
001560 V910-timeToTm-P.
001570
001580* 70 years including 17 leap days since 1900
001590 compute secsarg = secsarg + ((70 * 365) + 17) * 86400;
001600 move secsarg to secs
001610
001620 compute days = secs / 86400
001630 add 1 to days giving daysP1
001640 compute tm_wday = function mod(daysP1, 7)
001650
001660 compute year = days / 365
001670 move 1 to mon
001680 perform until 1 = 0
001690 perform v900-daysTo
001700 compute i = daysTo + 365 * year
001710 if days >= i then
001720* exit perform
001730 go to v910-endloop
001740 end-if
001750* correct guess and recheck
001760 subtract 1 from year
001770 end-perform.
001780 v910-endloop.
001790
001800 subtract i from days
001810 move year to tm_year
001820 move days to tm_yday
001830
001840 compute yearMod4 = function mod(year,4)
001850 if yearMod4 not= zero or year = zero then
001860 move 2 to leapIx
001870 else
001880 move 1 to leapIx
001890 end-if
001900 move 12 to mon
001910 perform until days >= daytab(leapIx, mon)
001920 subtract 1 from mon
001930 end-perform
001940 move mon to tm_mon
001950 compute tm_mday = days - daytab(leapIx, mon) + 1
001960
001970 compute secs = function mod(secs,86400)
001980 compute tm_hour = secs / 3600;
001990 compute secs = function mod(secs,3600)
002000 compute tm_min = secs / 60;
002010 compute tm_sec = function mod(secs, 60)
002020
002030 CONTINUE.
002040 END-V910-timeToTm.
002050 EXIT.
002060
002070
4. Example output. Here are two examples. First example is Fri May 03 2024 14:16:01 GMT+0000. See https://www.unixtimestamp.com.
$ ./cobts2date 1714745761
tm_sec = +000000001
tm_min = +000000016
tm_hour = +000000014
tm_mday = +000000003
tm_mon = +000000005
tm_year = +000000124
tm_wday = +000000005
tm_yday = +000000123
Second example is Thu Dec 31 1964 22:59:59 GMT+0000.
$ ./cobts2date -157770001
tm_sec = +000000059
tm_min = +000000059
tm_hour = +000000022
tm_mday = +000000031
tm_mon = +000000012
tm_year = +000000064
tm_wday = +000000004
tm_yday = +000000365
For a list of leap years see Schaltjahr.