11

Is it possible to use bash to subtract variables containing 24-hour time?

#!/bin/bash
var1="23:30" # 11:30pm
var2="20:00" # 08:00pm

echo "$(expr $var1 - $var2)"

Running it produces the following error.

./test 
expr: non-integer argument

I need the output to appear in decimal form, for example:

./test 
3.5
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255

5 Answers5

18

The date command is pretty flexible about its input. You can use that to your advantage:

#!/bin/bash
var1="23:30"
var2="20:00"

# Convert to epoch time and calculate difference.
difference=$(( $(date -d "$var1" "+%s") - $(date -d "$var2" "+%s") ))

# Divide the difference by 3600 to calculate hours.
echo "scale=2 ; $difference/3600" | bc

Output:

$ ./test.bash
3.50
Haxiel
  • 8,361
  • I know it's old but: Shouldn't it be bc -l in the last line instead of only bc? – martinw Oct 19 '21 at 13:02
  • @martinw According to the manpage, -l changes the default scale from zero to 20 and also loads a set of math library functions. I'm explicitly setting the scale here, and I'm not using any of the math library functions. So in this scenario, -l makes no difference in the output. – Haxiel Oct 19 '21 at 16:37
  • Yes, sorry, you are right. I remebered badly the effect of @-l@. – martinw Oct 20 '21 at 07:48
6

Using only bash, with no external programs, you could do so something like this:

#!/bin/bash

# first time is the first argument, or 23:30     
var1=${1:-23:30}
# second time is the second argument, or 20:00
var2=${2:-20:00}

# Split variables on `:` and insert pieces into arrays
IFS=':' read -r -a t1 <<< "$var1"
IFS=':' read -r -a t2 <<< "$var2"

# strip leading zeros (so it's not interpreted as octal
t1=("${t1[@]##0}")
t2=("${t2[@]##0}")

# check if the first time is before the second one
if (( t1[0] > t2[0] || ( t1[0] == t2[0] && t1[1] > t2[1]) ))
then
  # if the minutes on the first time are less than the ones on the second time
  if (( t1[1] < t2[1] ))
  then
    # add 60 minutes to time 1
    (( t1[1] += 60 ))
    # and subtract an hour
    (( t1[0] -- ))
  fi
  # now subtract the hours and the minutes
  echo $((t1[0] -t2[0] )):$((t1[1] - t2[1]))
  # to get a decimal result, multiply the minutes by 100 and divide by 60
  echo $((t1[0] -t2[0] )).$(((t1[1] - t2[1])*100/60))
else
  echo "Time 1 should be after time 2" 2>&1
fi

Test:

$ ./script.sh 
3:30
3.50

$ ./script.sh 12:10 11:30
0:40
0.66

$ ./script.sh 12:00 11:30
0:30
0.50

If you want more complex time differences, that could span different days etc, then it's probably best to use GNU date.

user000001
  • 3,635
  • Wow, thanks. Would you be willing to add a few quick comments above some of the lines of code that explain what's happening? – user328302 Dec 24 '18 at 12:55
  • @user328302: I added some comments, let me know if anything is still unclear. I also fixed a bug that wouldn't allow times like 09:08 (it was treated as octal) – user000001 Dec 24 '18 at 13:17
4

With Awk you can set the separator to be a space or a colon to effectuate the calculations needed:

#!/bin/bash
var1="23:30"
var2="20:00"
echo "$var1" "$var2" |  awk -F":| " '{print (60*($1-$3)+($2-$4))/60 }'
0

To do all the math on the epoch times in bc:

$ echo "(`date -d'23:30' '+%s'`-`date -d'20:00' '+%s'`)/60^2" |bc -l
3.50000000000000000000
0

Should be easy enough to do the calculation by hand:

$ printf '%02d:%02d\n' "$(((d=(${var1/:/*60+})-(${var2/:/*60+})),d/60))" "$((d%60))"
03:30

Here assuming $var1 is after $var2 or you'd get things like -02:-21 as a result.

Or for a floating point number in zsh / ksh93 / yash:

$ echo "$((((${var1/:/*60+})-${var2/:/*60+})/60.))"
3.5

If you have to use bash (which doesn't support floating point arithmetic):

$ awk 'BEGIN{print ARGV[1]/60}' "$(((${var1/:/*60+})-${var2/:/*60+}))"
3.5

To do the floating point part of the calculation by hand, though you might as well do the whole calculation in awk.