Issue 71 * May 21 2009

Around the World in 80+ Clocks
Learning to tell the time with on-rev technologies

by Mark Waddingham

Recently, we at RunRev have started to run the occasional Webinar to help demonstrate some of the cool new stuff we've been working on. It goes without saying that for any live broadcast to be useful everyone has to know what time it is at where you are – and this is not as straightforward as it sounds...

As a potential help to this problem, in this article we will be using the pre-release version of our new server-side scripting technology (irev) to produce a simple world-clock script – given a time anywhere in the world, it will produce a list of times for many other locations.

The prerelease irev technology is currently only available as part of our on-rev hosting service*, but if you haven't yet switched to on-rev, you can still try out the world clock we build in this article here: http://samples.on-rev.com/world-clock/world_clock.irev.

A brief history of time irev

Before going any further, it would probably be an idea to say a few words about the irev technology. This technology is one part of a initiative here at RunRev to bring our unique Revolution language to the web platform at large.

The irev version of Revolution is a new variant of our core engine that runs in a similar vein to PHP. Indeed we are working to ensure that it is the ideal language for the generation of dynamic HTML content. We hope that anybody who has flirted with PHP before will feel at home with the environment provided by this new variant technology.

An irev script is simply a text file, consisting of alternating blocks of HTML and Revolution script. For example, here is the classic Hello World! coded using it:

<html>
  <head>
    <title>Hello World, irev style</title>
  </head>
  
  <body>
  <?rev
    put "Hello World!"
  ?>
  </body>
</html>

The meat of this example is contained in the <?rev ... ?> block – anything contained inside such a block is considered to be Revolution script and is parsed and executed as such when it is encountered. Anything outside such blocks is output verbatim.

Here you can see the use of a destination-less put as a means to output text to the page we are generating. This is identical to echo in PHP-parlance.

There are a few other customizations to the language up irev's sleeves which we will introduce as we proceed.

You can have any time you like, as long as its now

One of the simplest examples of an irev script is one which outputs the current time. Indeed, this is as easy as:

<html>
  <body>
  <?rev
    put the time
  ?>
  </body>
</html>

However, if you try this script, then chances are it won't be displaying the time you think it should. A moment's thought will reveal why – the irev technology runs on the server, and as such will use the server's notion of time.

One might hope that one could use the built in convert command to some avail but then we run up against the problem with prelease things – we still have some stuff to work on and extend!

As it stands, convert only works in local time – which, for now, means server local time. What we really need is a convert command that can convert between any two timezones...

One-by-one the penguins are stealing my sanity

One advantage of things running on a central server is that the environment is much less variable compared to the numerous environments a typical desktop application will run in. Indeed, the irev technology runs in a pretty standard Linux environment, and as such you have access to all the command-line tools that any standard Linux distribution has.

It is a fundamental philosophy of Unix-like environments that functionality should be provided through lots of small programs that do specific things that can be built up together; so it should come as no surprise that there is a command-line program that can do date manipulation. Indeed, that command is called date!

Fortunately, one feature we have already implemented in irev is the shell() function making it a simple matter to improve upon our time script (well improve on it for people in Britain, anyway!):

<html>
  <body>
  <?rev
    put "Europe/London" into $TZ
    put shell("date")
  ?>
  </body>
</html>

Here you'll see I'm putting a location into the special $TZ variable. Just like desktop Revolution, variables preceded by $ are treated as environment variables – they are passed through to any commands invoked using shell.

As a general rule, command-line programs that manipulate date and time will do so relative to the region described by the TZ environment variable – in the case of date it will print the current server time as seen from that region.

If you have an on-rev account, then try changing it to a few different ones such as: Europe/Prague, America/New_York or Africa/Kinshasha.

We'll see how to discover the list of locations you can choose for $TZ a bit later on.

Function over form

Okay so the above is all very well, we have found a way to tell the current time in different regions, but what we really need is a way to convert a given time between two different regions.

Ideally we want a neat little function along the lines of:

  function convertTime pTime, pFromRegion, pToRegion

However, things (as always) are a bit more complicated than that. As it turns out, when you want to deal with time conversions between regions you can't just convert an isolated time - you also need a date. There are two culprits here, one is the fact that a time conversion can cross midnight before or after the initial time so the day may be different after conversion. The other is daylight savings time...

For example, here in Edinburgh, 16:00 on the 19th May is 15:00 GMT, whereas 16:00 on the 19th December is 16:00 GMT.

Thus, what we actually need is a function of the form:

  function convertDateAndTime pDateAndTime, pFromRegion, pToRegion

This will take a string of the form "YYYY-MM-DD HH:MM:SS" and convert it between the specified region's time-zones.

Fortunately, the Linux date command provides a command line argument to do this, and we get a function along the lines of the one shown in the following example:

<?rev
function convertDateAndTime pDateAndTime, pFromRegion, pToRegion
  local tOldTZ, tInputTZ, tInputDTZ

  -- Make sure our function doesn't leave permanent changes to global
  -- state by storing the current value of $TZ
  put $TZ into tOldTZ

  -- Work out the actual timezone the source region is in at the given date and time
  put pFromRegion into $TZ
  put shell("date --date=" & quote & pDateAndTime & quote && "+%Z") into tInputTZ

  -- Construct the input date/time as DATE TIME TIMEZONE
  put pDateAndTime && tInputTZ into tInputDTZ
  
  -- Invoke the 'date' shell command making sure it uses a matching output
  -- format.
  put pToRegion into $TZ
  get shell("date --date=" & quote & tInputDTZ & quote && \
                      "+" & quote & "%Y-%m-%d %T" & quote)
  
  -- Restore the previous value of $TZ
  put tOldTZ into $TZ
  
  -- Return the converted date/time
  return it
end convertDateAndTime
?>
<html>
  <body>
  <p>
    2009-05-19 17:00 in London is
    <?rev put convertDateAndTime("2009-05-19 17:00", "Europe/London", "Europe/Prague") ?>
    in Prague
  </p>
  <p>
    2009-05-19 17:00 in London is
    <?rev put convertDateAndTime("2009-05-19 17:00", "Europe/London", "Africa/Kinshasha") ?>
    in Kinshasha
  </p>
  </body>
</html>

Notice here that handlers in irev work as you would expect – you can define them and then use them elsewhere in the script. However, note that because irev scripts are not completely parsed before execution begins, handlers must appear before their first use.

Seeing the wood for the trees

At this point we have our neat not-so-little function for doing the conversion we require. Now, if we were coding on the desktop, then this is the kind of thing that would ideally be placed in a utility library stack or back script rather than have it clutter up all the places it is referenced from. Such re-usability is currently provided for in irev by the use of a new piece of syntax: the include command.

This command's behavior is taken from the world of PHP and the good old C macro preprocessor. On execution, it will attempt to open the given file and then read and process its contents at that point in time. The key point being, there is no difference between a file you might include, and one that gets executed directly by the irev engine as a result of a web page request.

With this in mind we can clean up our previous example a bit by separating into two separate files. In the first file datetime.irev we want:

<?rev
function convertDateAndTime pDateAndTime, pFromRegion, pToRegion
  local tOldTZ, tInputTZ, tInputDTZ

  -- Make sure our function doesn't leave permanent changes to global
  -- state by storing the current value of $TZ
  put $TZ into tOldTZ

  -- Work out the actual timezone the source region is in at the given date and time
  put pFromRegion into $TZ
  put shell("date --date=" & quote & pDateAndTime & quote && "+%Z") into tInputTZ

  -- Construct the input date/time as DATE TIME TIMEZONE
  put pDateAndTime && tInputTZ into tInputDTZ
  
  -- Invoke the 'date' shell command making sure it uses a matching output
  -- format.
  put pToRegion into $TZ
  get shell("date --date=" & quote & tInputDTZ & quote && \
                      "+" & quote & "%Y-%m-%d %T" & quote)
  
  -- Restore the previous value of $TZ
  put tOldTZ into $TZ
  
  -- Return the converted date/time
  return it
end convertDateAndTime

Our main file then becomes something along the lines of:

<?rev
  include "datetime.irev"
?>
<html>
  <body>
  <p>
    2009-05-19 17:00 in London is
    <?rev put convertDateAndTime("2009-05-19 17:00", "Europe/London", "Europe/Prague") ?>
    in Prague
  </p>
  <p>
    2009-05-19 17:00 in London is
    <?rev put convertDateAndTime("2009-05-19 17:00", "Europe/London", "Africa/Kinshasha") ?>
    in Kinshasha
  </p>
  </body>
</html>

Notice how much neater and easier to read this is.

The observant among you may have noticed that the datetime.irev file is missing a terminating ?>. However, this is not an error but intentional. As a general rule library-style include files such as these (ones that are for pre-defining functions and variables) should not be terminated in a ?> as this stops any line-breaks or whitespace being output that have managed to sneak in at the end of such a file (apart from a new line immediately following a ?> token, any and all content outside of <?rev ... ?> blocks is written to the page).

A time for all regions

As mentioned earlier on, the set of regions understood by the date shell command (and indeed, the timezone system on Linux) is somewhat restricted. This, of course, raises the immediate question of what it is...

Now, it turns out that the set of regions understood by date is precisely those present in the currently installed zoneinfo database. Far from being some hard to access, proprietary thing, this is actually nothing more than a structured collection of files and folders present in a globally accessible location!

Indeed, the database starts at the root folder /usr/share/zoneinfo. Files under this folder are the region specification files, the names of which are the names of the regions. Folders under this root folder are the group names – usually pertaining to countries or continents. For example, the data needed to process times in the region Africa/Kinshasa is found in the file /usr/share/zoneinfo/Africa/Kinshasa.

With this in mind, we can easily construct a full list of available regions by just listing the files/folders in an appropriate way:

function theRegions
  local tRegions

  -- Save the previous setting of the folder so we don't affect the caller
  local tOldFolder
  put the folder into tOldFolder

  -- Switch to the zoneinfo folder
  set the folder to "/usr/share/zoneinfo"

  -- Fetch the list of groups (i.e. child folders of zoneinfo).
  local tGroups
  put the folders into tGroups

  -- Make sure they are in ascending alphabetic order
  sort tGroups

  -- Iterate through each group, fetching the list of regions beneath it
  repeat for each line tGroup in tGroups
    -- Skip any groups which are just for compatibility/misc
    if tGroup is among the items of ".,..,posix,right,Etc" then
      next repeat
    end if

    -- Switch to the group's folder
    set the folder to tGroup

    -- Fetch the list of regions (cities) we have time-zone information for in the
    -- group.
    local tCities
    put the files into tCities
    sort tCities

    -- Iterate through each city and form a region reference GROUP / CITY
    repeat for each line tCity in tCities
      put tGroup & slash & tCity & return after tRegions
    end repeat

    -- Switch back to the zoneinfo folder
    set the folder to ".."
  end repeat

  -- Clean up our list by removing the last 'return'
  delete the last char of tRegions

  -- Reset the folder to what it was before
  set the folder to tOldFolder

  -- Return our list
  return tRegions
end theRegions

If we add this to our datetime.irev library script, we can concoct a simple world-clock script really easily:

<?rev include "datetime.irev" ?>
<html>
<body>
    <?rev
      local tInDateTime, tInRegion
      put "2009-05-19 16:00" into tInDateTime
      put "Europe/London" into tInRegion
    ?>
    <table>
    <?rev
      repeat for each line tOutRegion in theRegions()
    ?>
      <tr>
        <td><b><?rev put tOutRegion ?></b></td>
        <td><?rev put convertDateTime(tInDateTime, tInRegion, tOutRegion) ?></td>
      </tr>
    <?rev
      end repeat
    ?>
    </table>
  </body>
</html>

Now, this script is all well and good, but its a bit of a pain to have to modify and re-upload the script whenever you want a new time! What we now need to add is a bit of interactivity...

Adding a little red tape

The simplest method of adding interactivity to a web-page is through the use of an HTML form. An HTML form is a collection of UI elements which are configured to send data through to the server when requested through the user submitting the form.

The data sent by such a form is made available to an irev script through the special $_GET (or $_POST) variable. These variables are actually arrays, the keys of which are the names of the form elements, the values being whatever was in the elements when the form was submitted.

The key part about forms is that submitting one implicitly causes a new page request from the client, and it is this that allows us to change what the user sees.

In our case, we have three pieces of information we wish to fetch from the user: a date, a time and a region. When the user submits these and we are happy they are valid, we then wish to display the given date/time in all regions around the world.

The first thing to do, therefore is to generate the form:

<?rev
include "datetime.irev"

-- Fetch the list of available regions as we use this more than once.
local sRegions
put theRegions() into sRegions
?>
<html>
  <body>
    <form method="get" action="world_clock.irev">
      <label for="date">Date:</label>
      <input type="text" name="date" value="<?rev put $_GET["date"] ?>"><br>
      <label for="time">Time:</label>
      <input type="text" name="time" value="<?rev put $_GET["time"] ?>"><br>
      <label for="region">Region:</label>
      <select name="region">
      <?rev
        repeat for each line tRegion in sRegions
          if $_GET["region"] is tRegion then
            put "<option selected>" & tRegion & "</option>" & return
          else
            put "<option>" & tRegion & "</option>" & return
          end if
        end repeat
      ?>
      </select>
      <input type="submit" value="Go">
    </form>
    ...

Here you can see we have used several different HTML form elements: the input element gives us a text-box in which the user can enter a string; the select element gives us a drop-down menu and in this we include one option for each region; the input element gives us a button which will actually cause the form to be submitted.

Notice that the value attributes are set to the existing contents of the appropriate value of the $_GET array. Doing this ensures that when we re-generate the form after submission, the form elements start off with the values the user previously submitted – this is generally good practice when a form can be resubmitted.

The next stage is to validate the input data, in particular, if no data has been submitted yet we do nothing:

    ...
    <?rev
      -- If an error occurs during validation, we place it in tError.
      local tError
      put empty into tError

      -- First validate the contents of the time element - it should be of the form:
      --   dd:dd or dd:dd:dd
      --
      if $_GET["time"] is not empty and \
         not matchText($_GET["time"], "^\d\d?:\d\d?(:\d\d?)?$") then
        put "Time must be in the form HH:MM or HH:MM:SS" into tError
      end if

      -- Next validate the contents of the date element - it should be of the form:
      --   dddd-dd-dd
      --
      if $_GET["date"] is not empty and \
         not matchText($_GET["date"], "^\d\d\d\d-\d\d?-\d\d?$") then
        put "Date must be in the form YYYY-MM-DD" into tError
      end if

      -- Compute the input values (if any) to our world-clock
      local tInDateTime, tInRegion
      if tError is empty and \
        $_GET["time"] is not empty and $_GET["date"] is not empty then
        put $_GET["date"] && $_GET["time"] into tInDateTime
        put $_GET["region"] into tInRegion
      end if
      
      ...

There's nothing particularly notable about this section, apart from appropriate use of the $_GET array again to fetch the values that have been submitted.

The final part is to tie this in with the world-clock generation script we wrote above – all we need do is check there is something to generate and do so:

      ...
      -- Output the world-clock, if we have a date/time
      if tInDateTime is not empty then
        put "<table>" & return
        repeat for each line tOutRegion in theRegions()
          put "<tr>"
          put "<td><b>" & tOutRegion & "</b></td>"
          put "<td>" & convertDateTime(tInDateTime, tInRegion, tOutRegion) & "</td>"
          put "<tr>" & return
        end repeat
        put "</table>"
      end if

      -- Output an error message if the input is malformed
      if tError is not empty then
        put "<font color='red'><b>" & tError & "</b></font>" & return
      end if
    ?>
  </body>
</html>

You'll notice here I've used put rather than content outside of <?rev ... ?> tags to output the HTML for the world-clock table. The two are interchangeable although for large blocks of static HTML, put can become unwieldy. In this case I felt that the Revolution code was clearer by using put and not breaking it up.

The End

Well that completes my little whistle-stop tour around some of the things you can do with the up-coming irev technology. There is still a fair amount of work to do to bring it to feature-completion, but I'm sure you'll agree it will turn out to be a powerful tool in the Revolution programmer's arsenal.

*If you haven't yet purchased On-Rev, and you would like to join the Founders, I'm told there may be a handful of places left. You can contact support to find out if you can still get the package.

About the Author

Mark Waddingham is CTO for Runtime Revolution Ltd.

 

Main Menu

What's New

RunRevLive Edinburgh