Thursday, October 27, 2022

[SOLVED] Convert a date string to EPOCH time with POSIX tools

Issue

The goal is to convert the date format %Y-%m-%s %H:%M:%S %z to seconds since EPOCH with POSIX tools.

I currently have a problem with the formula from the POSIX's href="https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_16" rel="nofollow noreferrer">Seconds Since the Epoch: I get a different result than GNU/BSD date.

Here's my code, simplified for testing purposes:

#!/bin/bash
  
TZ=UTC date $'+  date: %Y-%m-%d %T %z \nexpect: %s \n %Y %j %H %M %S %z' |
awk '
    NR <= 2 { print; next }
    {
        # $0: "YYYY jjj HH MM SS zzzzz"
        $1 -= 1900
        epoch = $5 + $4*60 + $3*3600 + int($2 + ($1-70)*365 + ($1-69)/4 - ($1-1)/100 + ($1+299)/400)*86400
        print "result:", epoch
    }
'
  date: 2022-10-21 22:02:56 +0000 
expect: 1666389776 
result: 1666476176

What am I doing wrong?


Solution

By reading the specification thoroughly I found two problems:

  • tm_yday value should be date '+%j' minus one:

days since January 1 of the year (tm_year)

  • Integer arithmetic is needed:

The divisions in the formula are integer divisions

The following code now works; I also added the support for timezones in %z format:

#!/bin/bash

date $'+  date: %Y-%m-%d %T %z \nexpect: %s \n %Y %j %H %M %S %z' |
awk '
    NR <= 2 { print; next }
    {
        # $0: "YYYY jjj HH MM SS zzzzz"
        tm_year = $1 - 1900
        tm_yday = $2 - 1
        tm_hour = $3
        tm_min  = $4
        tm_sec  = $5
        zoffset = \
            int(substr($6,1,1) "1") * \
            (substr($6,2,2)*3600 + substr($6,4,2)*60)

        epoch = \
            tm_sec + tm_min*60 + tm_hour*3600 + tm_yday*86400 + \
            (tm_year-70)*31536000 + int((tm_year-69)/4)*86400 - \
            int((tm_year-1)/100)*86400 + int((tm_year+299)/400)*86400 - \
            zoffset

        print "result:", epoch
    }
'
  date: 2022-10-22 01:19:23 +0200 
expect: 1666394363 
result: 1666394363

ASIDE

Here's a sample implementation of GNU's mktime function for POSIX awk. It converts a date in the format %Y %m %d %H %M %S %z to seconds since EPOCH:

function mktime(datespec,    a) {
    if ( !_mktime_init ) {
        _mktime_init = 1
        split("0 31 59 90 120 151 181 212 243 273 304 334",_mktime_moff," ")
    }
    # datespec: "YYYY mm dd HH MM SS zzzzz"
    split(datespec,a," ")
    a[7] = \
        int(substr(a[7],1,1) "1") * \
        (substr(a[7],2,2)*3600 + substr(a[7],4,2)*60)
    a[3] += \
        -1 + _mktime_moff[a[2]+0] + \
        (a[2] > 2 && ((a[1]%4 == 0 && a[1]%100 != 0) || a[1]%400 == 0))
    a[1] -= 1900

    return \
        a[6] + a[5]*60 + a[4]*3600 + a[3]*86400 + \
        (a[1]-70)*31536000 + int((a[1]-69)/4)*86400 - \
        int((a[1]-1)/100)*86400 + int((a[1]+299)/400)*86400 - \
        a[7]
}

Sorry, I know that using meaningful variable names makes it easier to read the code but I made it compact for my personal ease of copy/paste and integration into my scripts.



Answered By - Fravadona
Answer Checked By - Marie Seifert (WPSolving Admin)