API
@-Formulas
JavaScript
LotusScript
Reg Exp
Web Design
Notes Client
 
Is Valid Time (with Time Zones)
I am currently updating an application that used to be Notes only and is now going to be Notes and web. One feature of this application is date and time fields. The application was set up that the person creating the record would enter in a date and time in their own time zone. The record could be read by anyone in any number of time zones. So the field has the property to always show the time zone.

When making this form editable on the web, the field ends up as a regular text field (just like a date field is a regular text field on the web). The text field includes the hours, minutes, seconds, AM or PM, and the time zone. So a text value of 12:00:00 PM GMT is valid.

Time zones in Notes are far more evolved than in JavaScript. JavaScript basically knows about your time zone and GMT. By this I mean that you can't display a time in any other time zone besides those two. You can create times in other time zones, as long as the format when creating is a certain format. The format is "GMT" followed by either a plus or a minus sign, and then four digits for the number of hours and minutes ahead or behind GMT. Those formats don't line up with the Notes time zones. I needed a way to validate what the user was entering into the field before it was submitted to the server.

I updated the isValidTime JavaScript function to handle the possibility of Notes time zones. It also allows for the possibility of the JavaScript time zones as well. And it still handles the case were no time zone information is included (just like it used to be).
Please note... if you have purchased the Reusable Object Library Level 2 application, you can use the "Find New Reusable Objects" feature to quickly download this design element and it's associated documentation at no cost.
Create a JavaScript script library to hold two functions. The library is called "isValidTime.js" and starts out with the "isValidTime" function:

function isValidTime(value) {

The value parameter is the time string. The only required pieces are the hours, then a colon, then the minutes. If you want to include seconds, then you should have a colon and the seconds. If you want to include the "meridian" (AM/PM value), then you should have a space and either "AM", "PM", "A", or "P", in either upper or lower case. If you want to include the time zone, then you should have a space and the time zone format you see in Notes or the time zone format you see in JavaScript.

The first thing the function does is validate the basic format and set some variables to indicate if the meridian is present and/or if the time zone is present:

   var hasMeridian = false;
   var hasZone = false;
   var re1 = /^\d{1,2}[:]\d{2}([:]\d{2})?( [aApP][mM]?)?(( [\-A-Z0-9a-z]{3,4})|( GMT[\-\+]\d{4}))?$/;
   var re2 = /^\d{1,2}[:]\d{2}([:]\d{2})?( [aApP][mM]?){1}(( [\-A-Z0-9a-z]{3,4})|( GMT[\-\+]\d{4}))?$/;
   var re3 = /^\d{1,2}[:]\d{2}([:]\d{2})?( [aApP][mM]?)?(( [\-A-Z0-9a-z]{3,4})|( GMT[\-\+]\d{4})){1}$/;

The first regular expression is the basic check of the format. At the start of the string must be either one or two digits for the hour, then a colon, then two digits for the minute. After that is a grouping (in parentheses) that can appear either not at all or exactly one time (the question mark after the grouping indicates zero or one time). The grouping is a colon and then two digits for the seconds. After that grouping is another grouping that can appear not at all or one time. That grouping is a space, then an upper or lower case "a" or "p" and then optionally (the next grouping in brackets followed by another question mark) an upper or lower case "m". So, "Am" is fine, "A" is fine, and "aM" is also fine.

Finally there is one large grouping that can appear not at all or one time. Inside this large grouping are two smaller grouping that are "or-ed" together with the vertical bar. This means that either sub-grouping can appear.

The first sub-group is a space followed by 3 or 4 characters. The valid characters are a dash, an upper case letter, a number, or a lower case letter. These are the Notes time zones. One time zone (Z-13) has a dash and several time zones have numbers. But all the time zones are either three or four characters.

The second sub-group is a space followed by the characters "GMT" in upper case followed by either a minus sign or a plus sign followed by 4 digits. So "GMT+0100" would match. This is the JavaScript time zone format.

The regular expressions 2 and 3 are similar to the first one. The difference in the second one is that the question mark after the meridian grouping is replaced by {1}, so this is saying that the grouping has to appear 1 time (instead of the question mark saying it can appear 0 or 1 times). This will tell the code if there is the meridian in the string. The third regular expression is used to see if the time zone is present - the time zone grouping has its question mark replaced with by {1}.

Next, those regular expressions are used. The first one is used to validate the overall string. The second and third ones are used to set the variables hasMeridian and hasZone:

   if (!re1.test(value)) { return false; }
   if (re2.test(value)) { hasMeridian = true; }
   if (re3.test(value)) { hasZone = true; }

If the first test fails, then the time is not valid because it didn't match the required format. So the code returns the value false. The second and third are used to set the variables. There is no chance of exiting since the meridian and time zone are both optional.

At this point, the code needs to validate the actual values. For example, a passed-in value of "12:99:99 PM" would pass the regular expression check, but it is not a valid time. So, the hours, minutes, and seconds are checked next. Since a colon separates the values, the code doesn't have to care if there is one or two digits for the hours - a quick split is used:

   var values = value.split(":");

Next, the hours are checked. If military (24-hour) time is used, the hours must be in the range of 0 through 23. Anything 24 and higher is not valid. If the meridian is present, then the hours must be in the range of 1 through 12. So two checks are made on the hour:

   if ( (parseFloat(values[0]) < 0) || (parseFloat(values[0]) > 23) ) { return false; }
   if (hasMeridian) {
      if ( (parseFloat(values[0]) < 1) || (parseFloat(values[0]) > 12) ) { return false; }
   }

The minutes must be in the range of 0 through 59 no matter if the meridian is present or not:

   if ( (parseFloat(values[1]) < 0) || (parseFloat(values[1]) > 59) ) { return false; }

If the seconds are present, then those must also be in the range of 0 through 59. I will point out that the split statement will result in the upper bound having the meridian information and time zone information. For example, if the time string is "12:30 AM GMT", then values[0] = "12" and values[1] = "30 AM GMT". This is fine because the parseFloat function will take the numbers up to the space and convert that and just ignore the rest of the string. So parseFloat("30 AM GMT") will be 30.

   if (values.length > 2) {
      if ( (parseFloat(values[2]) < 0) || (parseFloat(values[2]) > 59) ) { return false; }
   }

The seconds check is only made if the value is there (if there were two colons in the string).

At this point, the time portion (excluding the time zone) is fine. But if there is a time zone present, then that time zone needs to be checked. This is done through another function. So the "isValidTime" function is just about done:

   if (hasZone) {
      return checkTimeZone(value);
   } else {
      return true;
   }
} // end the "isValidTime" function

The next function checks the time zone. If I was doing this in LotusScript, I would define this function as a Private function because it should only be called from the "isValidTime" function. But I didn't want to include it in the "isValidTime" function because this keeps everything more isolated - it is only called when it is needed.

function checkTimeZone(timeStr, hasMeridian) {

The first thing this function does is check to see if the meridian value (if present) is "AM" or "PM". This allows the function to set the right time in military time format. For example, if the time is "1:00 AM", then the corresponding 24-hour format is "01:00". But if the time is "1:00 PM", then the corresponding 24-hour format is "13:00". Changing to the 24-hour format makes things easier later on.

   var isPM = false;
   if (hasMeridian) {
      var re = /^\d{1,2}[:]\d{2}([:]\d{2})?( [pP][mM]?){1}(( [\-A-Z0-9a-z]{3,4})|( GMT[\-\+]\d{4})){1}$/;
      if (re.test(timeStr)) { isPM = true; }
   }

The variable hasMeridian was passed in to the function. The "isValidTime" function computed whether the meridian value is there or not - this function checks to see if the value is "PM".

Next, the function splits out the hours, minutes, and seconds just like the "isValidTime" function.

   var values = timeStr.split(":");
   var hh = parseFloat(values[0]);
   var mm = parseFloat(values[1]);
   var ss = 0;
   if (values.length > 2) { ss = parseFloat(values[2]); }

Note that no checking needs to happen here - that was all done in the "isValidTime" function. So I can assume that the hours are in range (or else this function would have never been called).

Next, the hours should be converted to military format. I only do this so I know the exact format and I don't have to deal with "AM/PM" later on.

   if (hasMeridian) {
      if ( (!isPM) && (hh == 12) ) { hh = 0; }
      if ( (isPM) && (hh < 12) ) { hh += 12; }
   }

So, if the meridian is present and it's "AM", then convert 12:00 AM to military time 0:00. If the meridian is present and it's "PM", then convert everything starting with 1:00 PM to its corresponding military time. Everything "AM" except 12:00 AM is left alone, and everything "PM" except 12:00 PM is adjusted.

To build a time string to be converted in JavaScript, the hours, minutes, and seconds should all have leading zeros if needed. The next three statements take care of the leading zeros:

   if (hh < 10) { hh = "0" + hh; }
   if (mm < 10) { mm = "0" + mm; }
   if (ss < 10) { ss = "0" + ss; }

I will point out that JavaScript doesn't worry about data type matching. Even though the variable hh is initially an integer, I can add a string "0" to the front and the variable will end up as a string. Eventually all these variables will be concatenated together in a string, so whether that conversion is down now (by adding the leading zero) or later it doesn't matter.

Now comes the fun part of checking the time zone. Since this function is only called when there is a time zone, the assumption is made that the zone is there. And because of the required format that is validated in the "isValidTime" function, I know that everything after the last space is the time zone. So I pull out the time zone and convert it to upper case for easier comparison.

   var zone = timeStr.substring(timeStr.lastIndexOf(" "), timeStr.length);
   zone = zone.toUpperCase();

Because I use lastIndexOf and don't add any value to it, the variable zone will include the space. So it will have the space at the front. That's a necessary thing if the time zone passed in was a JavaScript time zone format - I don't want to convert that since it's already in the right format. But the space at the front will be necessary later on.

Next, I convert the Notes time zones to a JavaScript time zone. Where did I get these values? I just went through all the time zones in the Notes client and looked at the time zone abbreviation that was displayed.

   if (zone == " ADT") { zone = " GMT-0400"; }

This is the first one. If the zone is " ADT" (note the leading space) then it's converted to " GMT-0400". So this is 4 hours west of GMT. The abbreviation "ADT" actually stands for "Atlantic Daylight Time". The time zones are listed in alphabetical order and I won't bother explaining all of them individually. Just trust that I did my homework and went through all the Notes time zone abbreviations.

   if (zone == " AST") { zone = " GMT-0400"; }
   if (zone == " BST") { zone = " GMT-1100"; }
   if (zone == " CDT") { zone = " GMT-0600"; }
   if (zone == " CEDT") { zone = " GMT+0100"; }
   if (zone == " CET") { zone = " GMT+0100"; }
   if (zone == " CST") { zone = " GMT-0600"; }
   if (zone == " EDT") { zone = " GMT-0500"; }
   if (zone == " EST") { zone = " GMT-0500"; }
   if (zone == " GDT") { zone = " GMT-0000"; }
   if (zone == " GMT") { zone = " GMT-0000"; }
   if (zone == " HST") { zone = " GMT-1000"; }
   if (zone == " MDT") { zone = " GMT-0700"; }
   if (zone == " MST") { zone = " GMT-0700"; }
   if (zone == " NDT") { zone = " GMT-0330"; }
   if (zone == " NST") { zone = " GMT-0330"; }
   if (zone == " PDT") { zone = " GMT-0800"; }
   if (zone == " PST") { zone = " GMT-0800"; }
   if (zone == " YDT") { zone = " GMT-0900"; }
   if (zone == " YST") { zone = " GMT-0900"; }
   if (zone == " YW1") { zone = " GMT-0100"; }
   if (zone == " YW2") { zone = " GMT-0200"; }
   if (zone == " Z-13") { zone = " GMT+1300"; }
   if (zone == " ZE10") { zone = " GMT+1000"; }
   if (zone == " ZE11") { zone = " GMT+1100"; }
   if (zone == " ZE12") { zone = " GMT+1200"; }
   if (zone == " ZE2") { zone = " GMT+0200"; }
   if (zone == " ZE3") { zone = " GMT+0300"; }
   if (zone == " ZE3B") { zone = " GMT+0330"; }
   if (zone == " ZE4") { zone = " GMT+0400"; }
   if (zone == " ZE4B") { zone = " GMT+0430"; }
   if (zone == " ZE5") { zone = " GMT+0500"; }
   if (zone == " ZE5B") { zone = " GMT+0530"; }
   if (zone == " ZE5C") { zone = " GMT+0545"; }
   if (zone == " ZE6") { zone = " GMT+0600"; }
   if (zone == " ZE6B") { zone = " GMT+0630"; }
   if (zone == " ZE7") { zone = " GMT+0700"; }
   if (zone == " ZE8") { zone = " GMT+0800"; }
   if (zone == " ZE9") { zone = " GMT+0900"; }
   if (zone == " ZE9B") { zone = " GMT+0930"; }
   if (zone == " ZW1") { zone = " GMT-0100"; }
   if (zone == " ZW12") { zone = " GMT-1200"; }
   if (zone == " ZW2") { zone = " GMT-0200"; }
   if (zone == " ZW3") { zone = " GMT-0300"; }

So, now the code has a two digit hour (in military time), a two digit minute, a two digit second, and a JavaScript time zone. In order to build a date/time string, I need some date so I hard-code a date and create a JavaScript date object:

   var jan1 = new Date("01 Jan 2000 " + hh + ":" + mm + ":" + ss + zone);

I already know (from the "isValidTime" function) that the hours, minutes, and seconds are valid. If the time zone isn't valid, then the variable jan1 will result in an invalid date. But an invalid date is different in Firefox and IE. So there's a better way to check. A JavaScript date object has a method called toLocaleString that displays the date in a "fully qualified" format - weekday, month, day, year, hours, minutes, seconds, AM/PM. If the date isn't valid, then the toLocaleString returns something that doesn't match the format. I don't care what that format is, I just know it's not the right format. So the next statements check the format of the toLocaleString method of the date. If the format is correct, then the time zone is correct. If the format isn't correct, then the time zone is not correct.

   var timeStr = jan1.toLocaleString();
   var re = /^\w+, \w+ \d{1,2}, \d{4} \d{1,2}:\d{2}:\d{2} [AP]{1}M$/;
   return re.test(timeStr);
} // end the "checkTimeZone" function

The regular expression for checking the format is one or more characters (the weekday), then a space, then one or more characters (the month), then a space, then one or two digits (the day of the month), then a space, then four digits (the year), then a space, then one or two digits (the hour), then a colon, then two digits (the minutes), then a colon, then two digits (the seconds), then a space, then either "AM" or "PM". I check for AM/PM by checking for an upper case "A" or an upper case "P" and then an "M".

So that's my function for validating a time string that may or may not have a time zone. Next, I'll have to come up with a "time-picker" for the web so the user can select a time like they can in the Notes client. But that's another application for another time.