Raja
Raja

Reputation: 523

Datetime comparison in Unix

I had two datetime strings like below...

2014-09-03T02:23:09Z and 2014-09-03T03:24:57Z

Now, I have to assign the two datetimes to some variables and compare them, like this:

d1=2014-09-03T02:23:09Z
d2=2014-09-03T03:24:57Z

if (d1 < d2 ) 

I browsed many websites but couldn't find a solution. I tried the approach below but it doesn't work.

#set -vx

date1=`date -d "2014-09-03T04:27:23Z" +"%Y-%m-%dT%TZ"`
date2=`date -d "2014-09-03T02:23:09Z" +"%Y-%m-%dT%TZ"`
date3=`date -d "2014-09-03T05:23:09Z" +"%Y-%m-%dT%TZ"`

#date1=`date -d "2014-09-03T04:27:23Z"`
#date2=`date -d "2014-09-03T02:23:09Z"`
#date3=`date -d "2014-09-03T05:23:09Z"`


if [ ${date3} -lt  ${date1} ] ;  then
echo "working"
else
echo "gone"
fi

if [ ${date2} -gt ${date1} ] ;  then
echo "working"
else
echo "gone"
fi

I am facing the below error:

sh compare.sh

date: invalid date `2014-09-03T04:27:23Z'
date: invalid date `2014-09-03T02:23:09Z'
date: invalid date `2014-09-03T05:23:09Z'
working
working

Can someone please guide me on how to do datetime comparison in Unix?

Upvotes: 0

Views: 2182

Answers (1)

Jonathan Leffler
Jonathan Leffler

Reputation: 753990

Values in the ISO 8601 date format can be compared as strings when they're all in the same time zone (Z for Zulu or UTC in the example); that's one of the primary merits of the format.

The problem with if [ "${date3}" < "${date1}" ] notation is that you're doing input redirection in the middle of your test — which is not what you intend.

If you have Bash, you can use [[ in place of [:

if [[ "${date3}" < "${date1}" ]]

Alternatively, your test (or [) command may accept:

if [ "${date3}" '<' "${date1}" ]

The quotes around the comparison prevent it being interpreted as I/O redirection. (This works on Mac OS X 10.9.4, but that's using bash again. However, /bin/[ accepts it too, provided I omit the trailing ] — it must be because it is not invoked as [ that it requires the absence of ].) POSIX test is not required to support > or < as operators — only = and != are required for string comparison.

The classic command for the task is expr:

if [ $(expr "${date3}" '<' "${date1}") == 1 ]

This is probably the most portable mechanism (7th Edition Unix had expr for this task). It is also the clumsiest notation.


Updated question

I'm not clear that there is anything wrong with your date expressions per se, but the fact that they give you date: invalid date '2014-09-03T04:27:23Z' errors suggests that the version of date available to you does not support the notation. It works OK on Ubuntu 14.04 with the version from date --version starting date (GNU coreutils) 8.21. Here is a script that works for me when run by bash (but not when run by sh — because on Ubuntu that is a symlink to dash rather than bash). I used the -u (UTC) option so that the input and output times agreed — beware the dreaded time zone.

Note that if you are using GNU date, you can specify +%s to get the time value in seconds since The Epoch (which is 1970-01-01 00:00:00 +00:00) as a pure number, and those can be compared numerically.

dtcmp.sh

date1=`date -u -d "2014-09-03T04:27:23Z" +"%Y-%m-%dT%TZ"`
date2=`date -u -d "2014-09-03T02:23:09Z" +"%Y-%m-%dT%TZ"`
date3=`date -u -d "2014-09-03T05:23:09Z" +"%Y-%m-%dT%TZ"`

if [ ${date3} -lt ${date1} ]
then echo "true  (claim: [ $date3 -lt $date1 ])"
else echo "false (claim: [ $date3 -lt $date1 ])"
fi

if [ ${date2} -gt ${date1} ]
then echo "true  (claim: [ $date2 -gt $date1 ])"
else echo "false (claim: [ $date2 -gt $date1 ])"
fi

if [ ${date3} '<' ${date1} ]
then echo "true  (claim: [ $date3 '<' $date1 ])"
else echo "false (claim: [ $date3 '<' $date1 ])"
fi

if [ ${date2} '>' ${date1} ]
then echo "true  (claim: [ $date2 '>' $date1 ])"
else echo "false (claim: [ $date2 '>' $date1 ])"
fi

if [[ ${date3} < ${date1} ]]
then echo "true  (claim: [[ $date3 < $date1 ]])"
else echo "false (claim: [[ $date3 < $date1 ]])"
fi

if [[ ${date2} > ${date1} ]]
then echo "true  (claim: [[ $date3 > $date1 ]])"
else echo "false (claim: [[ $date3 > $date1 ]])"
fi

if [ $(expr ${date3} '>' ${date1}) = 1 ]
then echo "true  (claim: [ \$(expr $date3 '>' $date1) = 1 ])"
else echo "false (claim: [ \$(expr $date3 '>' $date1) = 1 ])"
fi

if [ $(expr ${date2} '>' ${date1}) = 1 ]
then echo "true  (claim: [ \$(expr $date2 '>'$date1) = 1 ])"
else echo "false (claim: [ \$(expr $date2 '>' $date1) = 1 ])"
fi

Sample outputs

bash

$ bash dtcmp.sh
dtcmp.sh: line 6: [: 2014-09-03T05:23:09Z: integer expression expected
false (claim: [ 2014-09-03T05:23:09Z -lt 2014-09-03T04:27:23Z ])
dtcmp.sh: line 11: [: 2014-09-03T02:23:09Z: integer expression expected
false (claim: [ 2014-09-03T02:23:09Z -gt 2014-09-03T04:27:23Z ])
false (claim: [ 2014-09-03T05:23:09Z '<' 2014-09-03T04:27:23Z ])
false (claim: [ 2014-09-03T02:23:09Z '>' 2014-09-03T04:27:23Z ])
false (claim: [[ 2014-09-03T05:23:09Z < 2014-09-03T04:27:23Z ]])
false (claim: [[ 2014-09-03T05:23:09Z > 2014-09-03T04:27:23Z ]])
true  (claim: [ $(expr 2014-09-03T05:23:09Z '>' 2014-09-03T04:27:23Z) = 1 ])
false (claim: [ $(expr 2014-09-03T02:23:09Z '>' 2014-09-03T04:27:23Z) = 1 ])
$ 

dash

$ dash dtcmp.sh
dtcmp.sh: 6: [: Illegal number: 2014-09-03T05:23:09Z
false (claim: [ 2014-09-03T05:23:09Z -lt 2014-09-03T04:27:23Z ])
dtcmp.sh: 11: [: Illegal number: 2014-09-03T02:23:09Z
false (claim: [ 2014-09-03T02:23:09Z -gt 2014-09-03T04:27:23Z ])
false (claim: [ 2014-09-03T05:23:09Z '<' 2014-09-03T04:27:23Z ])
false (claim: [ 2014-09-03T02:23:09Z '>' 2014-09-03T04:27:23Z ])
dtcmp.sh: 26: dtcmp.sh: [[: not found
false (claim: [[ 2014-09-03T05:23:09Z < 2014-09-03T04:27:23Z ]])
dtcmp.sh: 31: dtcmp.sh: [[: not found
false (claim: [[ 2014-09-03T05:23:09Z > 2014-09-03T04:27:23Z ]])
true  (claim: [ $(expr 2014-09-03T05:23:09Z '>' 2014-09-03T04:27:23Z) = 1 ])
false (claim: [ $(expr 2014-09-03T02:23:09Z '>' 2014-09-03T04:27:23Z) = 1 ])
$

ksh

$ ksh dtcmp.sh
dtcmp.sh[6]: [: 2014-09-03T05:23:09Z: arithmetic syntax error
false (claim: [ 2014-09-03T05:23:09Z -lt 2014-09-03T04:27:23Z ])
dtcmp.sh[11]: [: 2014-09-03T02:23:09Z: arithmetic syntax error
false (claim: [ 2014-09-03T02:23:09Z -gt 2014-09-03T04:27:23Z ])
dtcmp.sh[16]: [: <: unknown operator
false (claim: [ 2014-09-03T05:23:09Z '<' 2014-09-03T04:27:23Z ])
false (claim: [ 2014-09-03T02:23:09Z '>' 2014-09-03T04:27:23Z ])
false (claim: [[ 2014-09-03T05:23:09Z < 2014-09-03T04:27:23Z ]])
false (claim: [[ 2014-09-03T05:23:09Z > 2014-09-03T04:27:23Z ]])
true  (claim: [ $(expr 2014-09-03T05:23:09Z '>' 2014-09-03T04:27:23Z) = 1 ])
false (claim: [ $(expr 2014-09-03T02:23:09Z '>' 2014-09-03T04:27:23Z) = 1 ])
$ 

zsh

$ zsh dtcmp.sh
dtcmp.sh:[:6: integer expression expected: 2014-09-03T05:23:09Z
false (claim: [ 2014-09-03T05:23:09Z -lt 2014-09-03T04:27:23Z ])
dtcmp.sh:[:11: integer expression expected: 2014-09-03T02:23:09Z
false (claim: [ 2014-09-03T02:23:09Z -gt 2014-09-03T04:27:23Z ])
dtcmp.sh:16: condition expected: <
false (claim: [ 2014-09-03T05:23:09Z '<' 2014-09-03T04:27:23Z ])
dtcmp.sh:21: condition expected: >
false (claim: [ 2014-09-03T02:23:09Z '>' 2014-09-03T04:27:23Z ])
false (claim: [[ 2014-09-03T05:23:09Z < 2014-09-03T04:27:23Z ]])
false (claim: [[ 2014-09-03T05:23:09Z > 2014-09-03T04:27:23Z ]])
true  (claim: [ $(expr 2014-09-03T05:23:09Z '>' 2014-09-03T04:27:23Z) = 1 ])
false (claim: [ $(expr 2014-09-03T02:23:09Z '>' 2014-09-03T04:27:23Z) = 1 ])
$ 

Summary

  • The shells all agree that the pure numeric comparison of ISO 8601 date strings does not work.
  • Only bash accepts and works correctly with the '<' and '>' operators in plain test aka [ — the other shells either do not accept it or accept it but produce the wrong answer (which is worse!).
  • For the shells that support [[, the results agree and are correct.
  • All the shells work correctly with the expr notation.

For maximum portability, use expr. For maximum sanity, use [[.

Upvotes: 6

Related Questions