Command-line hacking: timezone conversions

display This is another in my series of articles on doing unusual and, perhaps, interesting things with Linux command-line tools and scripts. The purpose of this week's exercise is to create a display of times around the world, corresponding to a particular time at a particular place.

Why? Well, it's a thing I freqently need to do in my work. I need to schedule video meetings with clients and colleagues in particular places, and it can be awkward to find a time that suits everybody. So when a client suggests, say, 10AM in Calcutta, I need to know whether that will work for me, in London, and a colleague in, say, New York.

Of course, there are many on-line services that can provide this information. But it's easier -- for me at least -- just to run

$ worldtime 10:00 calcutta

and get an information dump like this:

Note: using timzone Asia/Calcutta
Chicago:     23:30 CDT
India:       10:00 IST
Jerusalem:   07:30 IDT
London:      05:30 BST
Paris:       06:30 CEST
Los Angeles: 21:30 PDT
New York:    00:30 EDT
Singapore:   12:30 +08
Sydney:      14:30 AEST

Of course, this list is of places where I have colleagues and clients; it needs to be easy to customize the output to suit a different person's needs.

I haven't included the full source code in this article, because it's too long. If you're interested, the full source for worldtime is available from my GitHub repository.

Basic principle

The key utility here is the standard GNU date, which is installed by default in most Linux distributions. This utility has a neat feature for specifying the input timezone. For example, for 12:34 in Paris (France), I can do this:

$ date --date "TZ=\"Europe/Paris\" 12:34"
Thu  4 Aug 11:34:00 BST 2022

That tells me that it's 11:34 BST where I am, in London. The format of the --date argument is a little fiddly, when it comes to scripting: it contains both spaces and double-quotes. So I need an outer pair of double-quotes to group the "TZ" string and the time together into one argument; then I need double-quotes within this group to group "Europe/Paris" (even though it does not contain spaces -- that's just how date rolls).

Bash being what it is, I could use double-quotes within single-quotes to avoid the need to escape the inner double-quotes. But that will make it awkward to use variables in the argument, rather than the literals in the example above.

So there is a way to get my local time for a time in a different geographic location. But how do we get the time in some other location?

We can do this by setting the TZ environemnt variable for the invocation of date as a whole (in addition to setting it in the --date argument). So to find the time in New York when it's 12:34 in Paris, I need:

$ TZ=America/New_York date --date "TZ=\"Europe/Paris\" 12:34"

This is the kind of thing that needs to be wrapped up in a script, because the format of the command is something that I, at least, will not easily remember.

Using short location names

It's nice to be able to use short location names. While its unambiguous to say:

$ worldtime 10:00 Europe/Paris 

it's much quicker to type

$ worldtime 10:00 paris 

We can get the full location name ("Europe/Paris") from the string "paris" by running:

$ timedatectl list-timezones | grep -i -m 1 paris 

The -m 1 argument specifies that only the first match should be reported. A more elegant solution, I guess, would be to have the script stop if there are, in fact, multiple matches. In any event, the script will print the actual value of the timezone region found in the list, so the user doesn't get bizarre results without an explanation.

Building the list

There's a compromise to be struck, between including too much information in the output, and too little. I've specifically tailored the output to the regions I work with. The script could easily print pages and pages of information, for locations all over the world, but it would difficult to interpret.

If you just want to display a few cities, it's easy just to have the script invoke date repeatedly. However, it's nicer to use an array, which makes the script easily extensible.

There are various ways to handle an array, but the (simple) approach I've adopted is to put the names of the cities, and the corresponding timezone region names, into a one-dimensional array in alternating locations. It's worth asking why the city names are needed at all. The script could just display the timezone location. That's true, but displaying a city just looks nicer. Or so it seems to me.

So my array looks like this:

REGIONS=("Chicago:     " "America/Chicago"
# Note -- India has only one timezone
         "India:       " "Asia/Kolkata"
         "Jerusalem:   " "Asia/Jerusalem"
#...
         "Sydney:      " "Australia/Sydney")

Note that bash uses parentheses to group array elements, which is different from most popular programming languages. We can iterate the array like this:

for (( i = 0; i < ${#REGIONS[@]}; i+=2 )); do
  CITY="${REGIONS[$i]}" 
  TIMEZONE_REGION=${REGIONS[$i + 1]} 
  #....
done

We need to iterate the array in units of two (i+=2), because the data are paired. The first element (the city name) of the pair is ${REGIONS[$i]}, while the second (the timezone region) is ${REGIONS[$i + 1]}. The length of the array is obtained by the rather odd, easy-to-forget, formulation ${#REGIONS[@]}.

Further work

The next step -- left as an exercise for the reader -- is to format the output to make it cleared whether a particular time is likely to be within a region's typical working hours. Of course, this can only be approximate. A clever implementation would take into account the fact that different locations have different workdays in the week. However, since the script does not deal with dates at all -- only times -- that would require extensive modifications.

Happy scripting.