Command-line hacking: timezone conversions
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.