, 5 min read

Converting UNIX Timestamps to Year, Month, Day in COBOL

Original post is here eklausmeier.goip.de/blog/2024/05-03-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.