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
5
  • Thanks for pointing that you. You're probably right. I've updated the question.
    – user328302
    Dec 24, 2018 at 12:37
  • 2
    What are your constraints? Shell builtins only, or external programs like bc or awk ? Dec 24, 2018 at 12:39
  • Related: unix.stackexchange.com/q/24626/315749
    – fra-san
    Dec 24, 2018 at 12:41
  • @fra-san, the dates are static, in variables, so it's kinda confusing to use date to solve this problem. @MarkPlotnick, no constraints, which ever method requires the least code. I'm ready to accept your Answer
    – user328302
    Dec 24, 2018 at 12:47
  • See also my answer here: stackoverflow.com/questions/8385627/… Apr 25, 2019 at 19:27

5 Answers 5

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
3
  • I know it's old but: Shouldn't it be bc -l in the last line instead of only bc?
    – martinw
    Oct 19, 2021 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, 2021 at 16:37
  • Yes, sorry, you are right. I remebered badly the effect of @-l@.
    – martinw
    Oct 20, 2021 at 7: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.

2
  • 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, 2018 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, 2018 at 13:17
3

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 }'
1
  • Or just do echo "$var1:$var2" | awk -F: ... Jan 21, 2022 at 7:02
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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .