Some notes on performing date calculations in Bash scripts using the formatting abilities of the date command.

The data command performs two functions: setting the system time and printing a time value using a given format. Using the formatting ability we can perform date manipulation to do things like generate timestamps, calculate durations, detect timestamps that fall within a range, and more.

Consider a simple task manager. For each task we want to record sessions which have a start time and a duration. The start time is recorded in seconds since the Unix Epoch (1970-01-01 00:00:00 UTC) and the duration is the seconds elapsed between the start and end of the session.

We use date’s %s format to get the current time in Epoch seconds

# Get the session start time
START_SECS=$( date +%s )
# Eg., $START_SECS=1318739079

# Sleep for 5 seconds
sleep 5

# Get the session elapsed time
DURATION=$(( $(date +%s) - $START_SECS ))
# $DURATION=5

By default the date command shows the current time. An specific time can be provided using the --date=STRING option. As the date manpage says the STRING "is a mostly free format human readable date string". By combining with the format option, we can transform dates from one format to another.

# Convert a date string (in the default format) to Epoch seconds
date --date="Sun Oct 16 15:34:27 EST 2011" +%s
# 1318739667

The --date string can handle various combinations of day, month, year such as ‘16 Oct 2011’ or ‘2011-10-16 UTC’. It can also interpret terms such as ‘next’ and ‘last’, units of time (‘day’, ‘week’, ‘month’, etc.), the days of the week (‘monday’, ‘tuesday’, etc.) and the relative days ‘yesterday’ and ‘tomorrow’. You can combine multiple terms and the date command will perform the date arithmetic for you.

# The empty string returns the start of today
date --date=""
# Sun Oct 16 00:00:00 EST 2011

# Get relative times

date --date="1 day ago"
date --date="-1 day"
date --date="yesterday"
date --date="last day"
# Sat Oct 15 15:38:26 EST 201

date --date="1 day"
date --date="+1 day"
date --date="tomorrow"
date --date="next day"
# Mon Oct 17 15:39:44 EST 2011

date --date="tomorrow +1 day"
date --date="2 day"
date --date="+2 day"
# Tue Oct 18 15:41:35 EST 2011

date --date="2 weeks ago"
# Sun Oct  2 15:42:18 EST 201

date --date="last friday +9 hours"
# Fri Oct 14 09:00:00 EST 2011

One thing the --date option cannot do is interpet a bare Epoch time in seconds. To convert Epoch seconds we need to specify the date relative to the start of the Unix Epoch. This allows Epoch seconds to be converted to some readable date format.

date --date="1318739667"
# date: invalid date `1318739667'

date --date="1970-01-01 UTC +1318739667 sec" +"%a %d-%b-%Y"
# Sun 16-Oct-2011

Putting it all together, say we are storing our task sessions (start time and duration) in an sqlite database. We want to generate a week’s worth of task reports, grouped by day. For each day we want to pull the sessions whose start time lies between the start and end of the day and do some processing, such as calculate the total time spent on the task during that day.

# Week starts on Monday.
START_OF_WEEK=monday
# Generate the report for last week.
WEEK_OFFSET=1

# Get the report start time in Epoch seconds.
START=$( date --date="last $START_OF_WEEK -$WEEK_OFFSET weeks" +%s )

# Loop for 7 days
for DAY in $( seq 1 7 ); do
    # End of day is start + 1 day's worth of seconds.
    END=$(( $START + 86400 ))

    # Format the start time as a printable date heading
    echo $(date --date="1970-01-01 UTC +$START sec" +"%a %d-%b-%Y")

    # Construct query to retrieve sessions between start (inclusive) and end
    # (exclusive) of the current day.
    SQL="SELECT task,start,duration FROM sessions
         WHERE start >= $START AND start < $END"

    RESULT=$( sqlite3 data.db "$SQL" )

    # Process each line of the result (assumes each line returned by sqlite
    # contains no whitespace).
    for RECORD in $RESULT; do
        # Do something with each record.
    done

    # Advance to the next day.
    START=$END
done