Handling TimeZones in ServiceNow (TimeZoneUtil)

Dealing with Time Zones in ServiceNow can be a real nightmare. The only documented way to set the time-zone of a GlideDateTime object, is the setTZ() method. Unfortunately, this method requires a "TimeZone" object be passed in, in order to set the time-zone.

What is a TimeZone object? It's an object that doesn't exist in JavaScript, and the only way to get hold of one using the ServiceNow API is to use gs.getSession().getTimeZone(), which just gets the current user's time-zone. If you want to know what time it is in a different time zone though, you're out of luck, chump! 

At least, you were. Sufficiently annoyed by this problem, I finally decided to write a Script Include to handle this for me, and now that I've been using (and testing) it for a while now, I'm comfortable publishing it for general consumption. Hopefully you'll find each method of the Script Include well documented, so I'll skip the usual explanation about what's going on in each function. If you want to know how it works, just have a look at the comments. 

Note: If you'd prefer that, even for well-documented scripts, I still provide a detailed explanation, leave a comment and let me know!

var TimeZoneUtils = Class.create();
TimeZoneUtils.prototype = {

    /**
     * Upon initialization, you can pass in a GlideDateTime object you've already created and set to a specific time.
     * The reference to this object will be used, and your GDT will be modified in-place. Alternatively, you may choose
     * not to specify a parameter upon initialization, and a new GlideDateTime object will be created, used, and returned
     * with the current time in the specified time-zone.
     *
     * @param {GlideDateTime} [gdt] - A reference to the (optional) GlideDateTime object to be modified IN-PLACE.
     * If not specified, a new one will be generated, and a reference returned.
     */
    initialize: function(gdt) {
        this.gdt = (typeof gdt == 'undefined') ? (new GlideDateTime()) : gdt;
    },

    /**
     * Get the GlideDateTime object (as a reference).
     * This will return a *reference* to the GlideDateTime object. Note that because of JavaScript's
     *  pass-by-reference jive, you should expect that if you set a variable using this method, then
     *  call another method which modifies the GDT object referenced in this class, you will be modifying
     *  the object to which your variable is a reference! In other words, your variable will be modified *in-place*.
     * @returns {*|GlideDateTime}
     */
    getGDT: function() {
        return this.gdt;
    },

    /**
     * Get the number representing the current GDT object's offset from UTC, in hours.
     * If the GlideDateTime object is in the Pacific time zone for example, this method will return either
     * "8" or "7" (depending on DST).
     * @returns {number}
     */
    getOffsetHours: function() {
        return ((Number(this.gdt.getTZOffset() / 1000) / 60) / 60);
    },

    /**
     * Note that you can specify time-zones in a number of formats, like "US/Pacific",
     * "US\\Eastern", or by short name (such as "mountain").
     *
     * Currently, this utility only understands a few time-zones by short name. You can print out a list of
     *  pre-defined these supported short-names by printing out the keys in the timeZones property.
     *  Example: gs.print(Object.keys(new TimeZoneUtils().timeZones));
     *
     * You can reference any time-zone using the following (case-sensitive) format:
     *  <Region>\<Zone>
     *  Example: "Pacific\Guam", or "America\Puerto_Rico"
     *
     * @param {Packages.java.util.TimeZone|string} tz - The TimeZone object to use to set the time-zone of
     *  the current GlideDateTime object.
     * @returns {*|GlideDateTime}
     */
    setTimeZone: function(tz) {

        /*
            FYI: http://twiki.org/cgi-bin/xtra/tzdatepick.html
            Click any of the locations there, and on the corresponding page, find the
            "Timezone" value.
            These are the valid values for the time-zone parameter.
        */

        //ensure we've got a string and that it's lower-case.
        tz = (typeof tz === 'string') ? tz : tz.toString();
        //Validate the TZ string, and get a TimeZone Java object from it.
        tz = this._getTimeZoneFromString(tz);

        this.gdt.setTZ(tz);
        return this.gdt;
    },

    /**
     * Gets the display value of the current GlideDateTime object.
     * If a time-zone was specified by calling .setTimeZone(), this will return the time in that time-zone.
     * If the GDT's time value was set prior to passing it into TimeZoneUtils, this will return that date/time
     * in the specified time-zone.
     * @returns {string} The current time, in the specified time-zone.
     */
    getDisplayValue: function() {
        return this.gdt.getDisplayValue();
    },

    /**
     * @returns {string} The current value, in SYSTEM time, of the GlideDateTime object.
     */
    getValue: function() {
        return this.gdt.getValue();
    },

    /**
     *
     * @param {Packages.java.util.TimeZone|string} tz - The TimeZone object to use to set the time-zone of
     * @returns {*} The TimeZone object, OR false if an invalid time-zone was passed in.
     * @private
     */
    _getTimeZoneFromString: function(tz) {
        //If it's a valid time-zone coming in, bob's our uncle.
        if (this._isValidTimeZone(tz)) {
            if (this.timeZones.hasOwnProperty(tz.toLowerCase())) {
                return this.timeZones[tz.toLowerCase()];
            } else {
                return Packages.java.util.TimeZone.getTimeZone(tz);
            }
        }
        //Otherwise, check if it matches one of our timeZone object properties.
        var shortTZ = this._getShortTimeZoneName(tz);
        if (this._isValidTimeZone(shortTZ)) {
            return this.timeZones[shortTZ.toLowerCase()];
        }

        //If nothing else has returned by now, it means the time zone isn't valid.
        gs.warn('Invalid time zone specified. Time zone: ' + tz, 'TimeZoneUtils Script Include, _getTimeZoneFromString method');
        return false;
    },

    /**
     * Checks if the passed string is a valid time zone string.
     * @param {string} tz - The TimeZone string to use to set the time-zone of
     * @returns {boolean}
     * @private
     */
    _isValidTimeZone: function(tz) {
        var tzObj = Packages.java.util.TimeZone.getTimeZone(tz);
        //If the tz string wasn't valid, then getID will return the string "GMT",
        //which - unless the user specified GMT as the time-zone, will not match the string argument.
        //However, if it does match, OR if the arg is found in the timeZones object, then we're good to go.
        return ((String(tzObj.getID()) === tz) || this.timeZones.hasOwnProperty(tz.toLowerCase()));
    },

    /**
     * Try another way of getting the proper time-zone. This is used when to look for a time-zone based only on the short-name.
     * @param {string} tz - The time-zone name we're looking at, at a string.
     * @returns {string} The time-zone, or a valid version of it if it needs validation, in lower-case.
     * @private
     */
    _getShortTimeZoneName: function(tz) {
        //Check if the string contains a forward-slash, back-slash, or underscore.
        if (tz.indexOf('\\') >= 0 || tz.indexOf('/') >= 0 || tz.indexOf(' ') >= 0) {
            /*
                If it contains a "/" or "\", grab everything after that character.
                Trim the resulting (sub-)string.
                If the remainder contains a space, replace it with an underscore.
             */
            tz = tz.slice(tz.indexOf('\\') + 1).slice(tz.indexOf('/') + 1).trim().replace(/ /g, '_');
        }
        return tz.toLowerCase();
    },

    /**
     * Just a reference to the setTimeZone method.
     * @param {Packages.java.util.TimeZone|string} tz - The TimeZone object to use to set the time-zone of the current GlideDateTime object.
     * @returns {*}
     */
    setTZ: function(tz) {
        return this.setTimeZone(tz);
    },

    /**
     * These are the pre-defined short-names for certain common time-zones.
     * Feel free to expand upon this object.

     * Currently, this utility only understands a few pre-defined time-zones by short name.
     * You can print out a list of these supported short-names by printing out the keys in the timeZones property.
     * Example: gs.print(Object.keys(new TimeZoneUtils().timeZones));
     * In a future update, this list will update itself with values from the sys_choice table, here:
     * https://YOUR_INSTANCE.service-now.com/sys_choice_list.do?sysparm_query=nameINjavascript%3AgetTableExtensions('sys_user')%5Eelement%3Dtime_zone
     */
    timeZones: {
        alaska:      Packages.java.util.TimeZone.getTimeZone('US/Alaska'),
        eastern:     Packages.java.util.TimeZone.getTimeZone('US/Eastern'),
        central:     Packages.java.util.TimeZone.getTimeZone('US/Central'),
        mountain:    Packages.java.util.TimeZone.getTimeZone('US/Mountain'),
        hawaii:      Packages.java.util.TimeZone.getTimeZone('US/Hawaii'),
        pacific:     Packages.java.util.TimeZone.getTimeZone('US/Pacific'),
        arizona:     Packages.java.util.TimeZone.getTimeZone('US/Arizona'),
        guam:        Packages.java.util.TimeZone.getTimeZone('Pacific/Guam'),
        puerto_rico: Packages.java.util.TimeZone.getTimeZone('America/Puerto_Rico'),
        india:       Packages.java.util.TimeZone.getTimeZone('Asia/Kolkata'),
        utc:         Packages.java.util.TimeZone.getTimeZone('UTC')
    },

    type: 'TimeZoneUtils'
};

Example usage

var gdt = new GlideDateTime(); //Get the current time in the Pacific time-zone
gs.print(gdt.getDisplayValue()); //Print the current time in the user's time-zone.
var tzu = new TimeZoneUtils(gdt); //Specify a GDT object is optional. If unspecified, a newly initialized GDT with the current date/time will be used.
tzu.setTimeZone('US/Eastern'); //Sets the time-zone to US/Eastern (GMT-5)
gs.print(gdt.getDisplayValue()); //Prints out the current time in the Eastern time-zone

You may simply copy and paste that into your instance, and you'll have a server-side utility for handling time-zones! And as for a client-side version, well here's a quick GlideAjax one for you: 

var ClientTimeZoneUtils = Class.create();
ClientTimeZoneUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {

    getCurrentTimeInTimeZone: function() {
        var tz = this.getParameter('sysparm_tz');
        var tzu = new TimeZoneUtils();
        var gdt = tzu.setTimeZone(tz);
        
        return gdt.getDisplayValue();
    },

    type: 'ClientTimeZoneUtils'
});

Example Usage

var gaTZ = new GlideAjax('ClientTimeZoneUtils'); //initialize GA
gaTZ.addParam('sysparm_name', 'getCurrentTimeInTimeZone'); //specify the function to run
gaTZ.addParam('sysparm_tz', 'guam'); //specify the time-zone as one of the predefined shorthand values
gaTZ.getXML(gaCallback);
function gaCallback(response) {
    var answer = response.responseXML.documentElement.getAttribute('answer');
    console.log('The current time in the time-zone you specified, is ' + answer);
}

It's probably a good idea to add a "get specified time in specified time-zone" sort of function into the client-callable Script Include, but I didn't get around to it. Perhaps I'll update this article later once I get around to writing one. ^_^