[Bash Challenge 10] Can You Solve This Bash Script Puzzle?

Welcome to our last Bash Challenge by Yes I Know IT & It’s FOSS. In this weekly challenge, we will show you a terminal screen, and we will count on you to help us obtain the result we wanted. There can be many solutions, and being creative is the most amusing part of the challenge.

If you haven’t done it already, do take a look at previous challenges:

You can also buy these challenges (with unpublished challenges) in book form and support us:

Suggested read
Bash It Out! Bash Script Puzzle Book by It's FOSS is Available Now!

Ready to play? So here is this week’s challenge:

The Back in Time Function

Bash scripting puzzle 10

This week, I want a shell function to display the date & time it was two hours ago. The function output must follow the YYYY-MM-DD hh:mm format.

I came to a solution using simple shell arithmetics:

minus-two-hours() {
date -d "$1" +"%F %H:%M" | \
{
IFS=": " read -a COMP
echo "${COMP[0]} $((10#${COMP[1]}-2)):${COMP[2]}"
}
}

As you noticed, the function takes a date as an argument, parse it, and write back that date minus two hours. Unfortunately, the result is far from being satisfactory as the expected format is not always satisfied & I even have negative hours sometimes:

yesik:~$ minus-two-hours now
2016-11-22 20:55
yesik:~$ minus-two-hours "2016/11/21 05:27:18"
2016-11-21 3:27
yesik:~$ minus-two-hours "2016/11/21 01:10:42"
2016-11-21 -1:10

Could you help me finding a solution to obtain the desired result? As always, I’m counting on your creativity and I’m looking forward reading your solutions in the comment section below!

Few details

To create this challenge, I used:

  • GNU Bash, version 4.4.5 (x86_64-pc-linux-gnu)
  • Debian 4.8.7-1 (amd64)
  • All commands are those shipped with a standard Debian distribution
  • No commands were aliased

The solution

How to reproduce

Here is the raw code we used to produce this challenge. If you run that in a terminal, you will be able to reproduce exactly the same result as displayed in the challenge illustration (assuming you are using the same software version as me):

rm -rf ItsFOSS
mkdir -p ItsFOSS
clear
minus-two-hours() {
date -d "$1" +"%F %H:%M" | \
{
IFS=": " read -a COMP
echo "${COMP[0]} $((10#${COMP[1]}-2)):${COMP[2]}"
}
}
minus-two-hours now
minus-two-hours "2016/11/21 05:27:18"
minus-two-hours "2016/11/21 01:10:42" 

What was the problem?

Date arithmetic is much more complicated that one might expect. I strongly discourage you from taking the path I used in my initial attempt: never do date and time calculations by yourself. If you really need an argument to convince you, think for example about issues with daylight saving time.

That being said, which options do we still have? Any decent programming language should have some facilities to deal with time specific issues. Here we are using the Bash, and we have to rely on the date tool for our purpose.

Converting date to quantities

When faced with similar problems, the typical solution will be to convert the (human readable) date and time to some numeric quantity.

Usually, we convert dates to a number of seconds (or milliseconds) since some reference time. Having that numeric quantity, we can now use classic arithmetics to add or remove homogeneous quantities (say remove 7200s — that is 2×60×60s — to obtain the date it was two hours ago). Finally, using the same facilities as in the initial step, we can convert back the result to a date time format.

In practice, in Unix-like systems, the reference date is usually 00:00:00 UTC on 1 January 1970 — sometimes known as Unix Epoch (hence the name of the traitor in Matrix — you remember him?). And the date utility does provide:

  • the %s specifier to convert a date to the number of seconds since the Epoch]
  • and the “@” symbol to specify an input date is expressed as a number of seconds since the Epoch (BSD will use the -r option for that purpose)

So here is a possible solution to my issue:

minus-two-hours() {
# 1. Convert to nbr of seconds since Unix Epoch
SRC=$(date -d "$1" +"%s")
# 2. Remove two hours (expressed as a number of seconds)
DST=$((SRC-2*60*60))
# 3. Display the result using the required format
date -d "@$DST" +"%F %H:%M"
}

Using the mighty powers of GNU date utils

The solution above is highly portable — even beyond the limits of shell programming.

But when using GNU date as we do on Linux for example, we have access to a whole world of subtleties to express the date. In particular, you can simply write that:

minus-two-hours() {
date -d "$1 2 hours ago" +"%F %H:%M"
}

Yes: “2 hours ago” is part of the date specification and is understood by GNU date as a way to say “remove two hours to the previous date”.

As you can see, when portability is not a concern, it worth taking the time to explore a little bit your specific tools documentation as they may contain hidden gems!

A last word

And this ends our last Bash Challenge.

I hope you enjoyed this series — and that it was the occasion for many of you to discover new things, either through the challenge itself, its solution, or the comments.

Speaking of that, don’t hesitate to use the comment section below to say what you thought about that series!

Comments

  1. Make use of timezones, but also embed another date command to allow use of ‘now’:
    minus-two-hours () {
    date -d “$(date -d ‘$1′ +’%F %H:%M’) +0200″ +”%F %H:%M”
    }

    • Since I am not able to put comments, I am replying to this message:
      inus-two-hours() {
      echo `date -d “$1 2 hours ago” +”%Y-%m-%d %H:%M”`
      }

      • Ah! I was focusing on ‘now’, I tried “now +0200” but didn’t get that to work. I thought I tried “now 2 hours ago” but I guess I didn’t!

  2. minus-two-hours() {
    date -d “$1” + “%F %H:%M” | \
    {
    IFS=”: ” read -a COMP
    echo “${COMP[0]} $(( (${COMP[1]}+10)%12 )):${COMP[2]}”
    }
    }

  3. minus-two-hours() {
    date -d “$1″ +”%F %H:%M” | \
    {
    IFS=”: ” read -a COMP
    echo “${COMP[0]} $(($((10#${COMP[1]})) > 2 ? $((10#${COMP[1]}-2)) : $((10#${COMP[1]}+22)))):${COMP[2]}”
    }
    }

Leave a Reply

Your email address will not be published. Required fields are marked *

[i]
[i]