, 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.