Bash, Ruby & Time Calculations

As a follow up to Clock In, Clock Out, I thought I would discuss the methods I used for calculating the amount of hours worked in my clock script, since working with time mathematically is always a bit tricky.

First off, Bash shell scripts are really great for text and file manipulation (among other powerful aspects). However, once you start getting into more complex functionality like math and anything object-oriented, you really want to switch over to another language or environment. That’s why I called upon Ruby to do my “heavy lifting” as I put it.

So let’s jump into the first timestamp the script records. Pretty basic.

TIME_IN=`date "+%Y/%m/%d %H:%M:%S"`

The backticks (`) allow me to store the result of the date command in the variable TIME_IN. (Actually, the backticks are sorta deprecated. The new convention is to use $(<command>) instead. Bad habit I suppose.) I store it in a variable so that I can use it later on to print the timestamp to Terminal (below).

echo -e "\033[1;32mStatus - Clocked in\033[0m"
echo -e "\033[0;37m$TIME_IN\033[0m"

Those funky bits of code (i.e. \033[0;37m$TIME_IN\033[0m ) are used to adjust the text color when printed in Terminal. In this case, the first line is green and the second line gray.

Now when you clock out, clock grabs the last line in the timecard, stores the new timestamp and feeds both into the timediff.rb script as follows:

TIME_IN=`tail -n 1 $TIMECARD`
TIME_OUT="Out - `date "+%Y/%m/%d %H:%M:%S"`"
...
HOURS="     Session Length - `timediff.rb "$TIME_IN" "$TIME_OUT"`"

You might have noticed that the TIME_OUT variable not only includes the timestamp from date but also some extra text, specifically “Out – “. Later on, clock uses the TIME_OUT variable to print to the console. timediff.rb is robust enough that this extra text is parseable and disregarded in the time difference calculation. Let’s take a look at what happens in timediff.rb

First I implement the required gems.

require 'rubygems'
require 'time'
require 'time_diff'

Next, timediff.rb uses the parse method to create a Time object with the appropriate date and time.

t1 = Time.parse(ARGV[0])
t2 = Time.parse(ARGV[1])

The ARGV parameter is an inherent array in Ruby that grabs arguments that are passed into the script when it is run, in this case $TIME_IN and $TIME_OUT from clock. Lastly, the script uses the diff method added to Time from the time_diff gem to find the difference between the two timestamps, which are subsequently sent back to clock with puts.

puts Time.diff(t1, t2, '%h:%m:%s')[:diff]

Since the output of diff() is actually a hash (see hashes in Ruby), the [:diff] call at the end of the line tells Ruby to only pass back the :diff portion of the hash with the timestamp formatted as ‘%h:%m:%s’.

Now let’s talk about timeadd.rb. In essence, clock grabs the hours for each recorded session and pumps each one into timeadd.rbas an argument.

TOTAL=`grep "Session Length -" $TIMECARD | cut -c23-30`
...
echo "Total Hours - `timeadd.rb $TOTAL | cut -d . -f 1`" >> $TIMECARD

First, timeadd.rb creates a base timestamp of the current day at 00:00:00 and a totalTimeInSecondsvariable. The rest of that portion is explained in the comments of the code below.

baseTime = Time.parse('00:00:00')
totalTimeInSeconds = 0.0

ARGV.each do|arg|

   # Parses the hours into a Time for today
   time = Time.parse(arg)

   # Calculates the time difference in seconds using the base time 00:00:00
   timeInSeconds = time - baseTime

   # Sums up the time in seconds
   totalTimeInSeconds += timeInSeconds

end

To calculate the total time worked for that period, I implemented a nice little gem called ChronicDuration. You can read more about it in a blog post by Everyday Rails. In short, ChronicDuration takes a time in seconds and converts it to a given format, which in this case is day:hours:minutes:seconds as indicated by :chrono. After that I pass it back to clock.

totalTime = ChronicDuration::output(totalTimeInSeconds, :format => :chrono)
puts "#{totalTime}"

Finally, clock does some final formatting on the timecard as explained in the previous post (download the files there too).

While Bash can handle basic math by converting time into seconds, I chose to take the Ruby route to learn some Ruby and keep the code in clock a bit more simple. Arguably, abstracting to external Ruby scripts is just as complex. However, it did simplify much of the work and I didn’t have to do a lot of tedious math and formatting in Bash.