0

I'm a bash enthusiast. I've written many scripts, but this time I think this one is too big for me. I'm working on a script that would check the number of hours based on the schedule from the file and sum them up.

I have a file with weekly schedule. By using grep it returns me time schedule (for example 13:00 - 20:30 or 13:30 - 21:30). Those files will come always in the same format, only the hours will be different so my grep query will always work. The only difference is that sometimes it will return four lines (with hours), sometimes less, sometimes more, dependes what's in the given file (I can always use grep -c if the number of those lines matters).

When I subtract the finishing time from the starting time, the result is correct, although it won't work if the hours are a bit odd (for example 14:00 - 16:17). How can I achive that (2h and 17minutes)?

And then I think I should use a loop. I tried a couple of things and got super confused and decided to come here.

Here's what I've got so far (26to31August is obviously the file with the schedule).

#!/bin/bash


#COUNT=$(grep -c '[0-9][0-9]:[0-9][0-9]' 26to31August)
FINISH=$(grep '[0-9][0-9]:[0-9][0-9]' 26to31August |head -1 |awk '{ print $3 }')
START=$(grep '[0-9][0-9]:[0-9][0-9]' 26to31August |head -1 |awk '{ print $1 }')



VAR1=$(date -d "${FINISH}" +%s)
VAR2=$(date -d "${START}" +%s)

HOURS=$((VAR1-VAR2))


SECS2HOURS=$(expr $HOURS / 3600)
echo "That day it's $SECS2HOURS hours"

I want the script to echo each of those lines telling me how many hours it is per line and then add them all together.

Should I use For loop? Should I use a function in the loop?

I'll apprecieate your answers.

lane17
  • 3
  • 4
  • `expr` is an ancient tool from the 1970s. Use `$(( ... ))` for built-in arithmetic in modern (post-1991) shells. And avoid all-caps variable names -- per POSIX specification, they're used for variables meaningful to the shell and operating system utilities, whereas names with at least one lowercase character are guaranteed safe for applications to use; see https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html, fourth paragraph. – Charles Duffy Sep 27 '19 at 23:03
  • ...that said, the question-as-asked is rather broad; it's not "here's a narrow technical issue" (which is what we're best at here!), but rather "how would you advise I solve this problem?" -- innately open to many solutions. – Charles Duffy Sep 27 '19 at 23:04
  • ...that said, are you familiar with modulo operations? If you take the remainder of your division, you can then divide that into minutes; take the remainder of *that*, and you have the number of seconds. – Charles Duffy Sep 27 '19 at 23:08

2 Answers2

0

This may not be what you want to hear, but this is definitely a case where a different programming language will work infinitely better than Bash arithmetic. In Python, for example, you would simply do duration = finish - start. That will do a timezone-aware, leap day-aware, sane calculation resulting in an object from which you can get everything from years to fractional seconds.

l0b0
  • 55,365
  • 30
  • 138
  • 223
0

Yes you would use a loop. You should maintain the running total in seconds and convert to hours:minutes in the final output

First thought (warning, this code contains a bug)

total=0
grep '[0-9][0-9]:[0-9][0-9]' 26to31August | while read -r start x finish x; do
    a=$( date -d "$start" "+%s" )
    b=$( date -d "$finish" "+%s" )
    (( total += b - a ))
done

# output
hours=$(( total / 3600 ))
minutes=$(( (total % 2600) / 60 ))
printf "%d:%02d\n" $hours $minutes

bash can parse a whitespace-separated string into words with the read command.

Where's the bug? With bash's default configuration, each command in a pipeline is executed in a separate subshell. Therefore, the total variable's contents will be lost when the subshell running the while loop ends.

Here are two ways to get around that:

  1. group the output commands into the same subshell

    total=0
    grep '[0-9][0-9]:[0-9][0-9]' 26to31August | {
        while read -r start x finish x; do
            a=$( date -d "$start" "+%s" )
            b=$( date -d "$finish" "+%s" )
            (( total += b - a ))
        done
        hours=$(( total / 3600 ))
        minutes=$(( (total % 2600) / 60 ))
        printf "%d:%02d\n" $hours $minutes
    }
    
  2. don't use a pipe, redirect the output of a process substitution into the while loop

    total=0
    
    while read -r start x finish x; do
        a=$( date -d "$start" "+%s" )
        b=$( date -d "$finish" "+%s" )
        (( total += b - a ))
    done < <( grep '[0-9][0-9]:[0-9][0-9]' 26to31August )
    
    hours=$(( total / 3600 ))
    minutes=$(( (total % 2600) / 60 ))
    printf "%d:%02d\n" $hours $minutes
    

Note, don't use ALLCAPS variable names. It's too easy to accidentally overwrite crucial shell variables.

glenn jackman
  • 238,783
  • 38
  • 220
  • 352