How do I allow users to see/enter timezone into Date/Time Fields?

Mathieu8
Kilo Guru

Hi Community,

We have an international company. User's are often booking meetings, support, and requests with people across many timezones.

User's sometimes have their Laptop's timezone set to our corporate office, their tablet timezone set to their home location, and their phone timezone to where ever they are at this moment. This means on some occasions every device is telling them a different time in ServiceNow, and every device needs to enter a different time when filling out forms.

The ServiceNow UI and Portal are incredibly confusing for people to fill out without timezone being displayed.

Ideally, the inputs would display "01-01-2020 14:00:00 PST" or "01-01-2020 17:00:00 EST". I know time format is only documented to permit the letters h/H/m/s, however knowing it was based off Java's SimpleDateFormat I thought I would try adding a timezone.

I changed my personal time format to "HH:mm:ss z" and this worked great for me. I could see my timezone and even save values other people would give me in their timezone and things worked flawlessly for me.

However, somewhat expectedly, this didn't continue to work. While it worked for PDT/PST/EST/EDT/IST/JST/etc things failed when I had one of my European coworkers try with the timezone CEST which would throw an error of an invalid date.

Alternatively I attempted to try using the format "HH:mm:ss 'GMT'ZZ" for "01-01-2020 14:00:00 GMT-0700", but this format doesn't work at all.

I was guessing that the momentjs library did the validation, so I was testing formats that both looked good and were compatible with momentjs. I decided on the format "MM-DD-YYYY HH:mm:ss [GMT]ZZ z" for "01-01-2020 14:00:00 GMT-0700 PST" but while compatible with momentjs, it was not compatible with ServiceNow.

Now were getting into some wild design decisions where we're saving the value the user entered, having them select the timezone they want to save the date/times in, and then converting the value to be saved in another column, all because the Date/Time input seemingly cannot accept certain timezones as input.

What can I do to make the Date/Time input function for our users?

Thanks.

1 ACCEPTED SOLUTION

I've spent about 2 hours on this and this is what I currently have. I'm wondering how I could go about writing tests for this. From my initial testing things appear to be working except for some EXTREME edge cases that are unlikely to happen.

Implementation

1. Under UI Scripts I've added moment.js (https://momentjs.com/downloads/moment.min.js)

Set it as a global script.

find_real_file.png

2. Under UI Scripts I've added moment-timezone.js (https://momentjs.com/downloads/moment-timezone-with-data-1970-2030.min.js)

Set it as a global script.

find_real_file.png

3.Under System Properties I've changed Time format ('glide.sys.time_format') to "HH:mm:ss z (ZZ)".

https://<YOUR_INSTANCE>.service-now.com/nav_to.do?uri=%2Fsystem_properties_ui.do%3Fsysparm_category%3DSystem%26sysparm_title%3DSystem%2520Properties

find_real_file.png

 

4. Change the Validation Script

https://<YOUR_INSTANCE>.service-now.com/nav_to.do?uri=sys_script_validator.do?sys_id=829b0bb0533131008d7d154f811c08f7

function validate(value) {
    // empty fields are still valid dates 
    if (!value) 
        return true; 

    // We "should" have the global date format defined always defined. but there's always that edge case...
    if (typeof g_user_date_time_format !== 'undefined') {
		
		//Check if format is "{{DATE}} {{TIME}} {{TIMEZONE}} ({{OFFSET}})"
		var dateTimeParts = g_user_date_time_format.split(' ');
		
		if (dateTimeParts.length === 4 && dateTimeParts[2] === 'z' && dateTimeParts[3] === "(ZZ)") {
			var valueDateTimeParts = value.split(' ');
			
			//There must be 4 parts to check.
			if (valueDateTimeParts.length === 4) {
				
				//Check the first two parts exactly like they were before
				if (!isDate(valueDateTimeParts[0] + ' ' + valueDateTimeParts[1], dateTimeParts[0] + ' ' + dateTimeParts[1])) {
					return getMessage("Invalid Date");
				}
				
				//Check if Date gets a valid date with this value
				var dateTimeVal = new Date(valueDateTimeParts[0] + ' ' + valueDateTimeParts[1] + ' ' + valueDateTimeParts[3]).getTime();
				if (isNaN(dateTimeVal)) {
					return getMessage("Invalid Date");
				}
				
				//Check if the timezone and offset are valid
				if (!isValidTimezone(valueDateTimeParts[2], valueDateTimeParts[3])) {
					return getMessage("Invalid Date");
				}
				
				return true;
				
			} else {
				return getMessage("Invalid Date");
			}
			
		} else {
			return isDate(value, g_user_date_time_format) ? true : getMessage("Invalid Date"); 
		}
		
	}

    // if we don't have that defined, we can always try guessing 
    return parseDate(value) !== null ? true : getMessage("Invalid Date"); 
	
}

function isValidTimezone(abbr, offset){
	//Offset should be the format (+0000)
	if(offset.length !== 7) return false;
	
	//remove brackets get each character
	offset = offset.substr(1,5).split('');
	
	//If it's a '-' it should be a positive number, if it's a + it should be negative
	//The first 2 digits are the number of hours for the offset
	//The second 2 digits are the number of minutes for the offset
	var offsetVal = (offset[0] === '-' ? 1 : -1) * (((offset[1]+offset[2]) * 60) + parseInt(offset[3]+offset[4]));
	
	return moment.tz.names()
		//Convert to list of Timezone Objects
		.map(function(z){ return moment.tz.zone(z); }) 
		//Check if the timezone exists at all
		.filter(function(tz){
			//The abbreviation and offset must both exist and be the same index
			var abbrsInd = tz.abbrs.indexOf(abbr);
			return  abbrsInd !== -1 && abbrsInd === tz.offsets.indexOf(offsetVal); 
		}) 
		.length; //return the number of valid results
	
}

 

Problems Considered

Here were some of my considerations.

CST = Central Standard Time (-0600), China Standard Time (+0800), Cuba Standard Time (-0500).

Similar problems exist for ECT, IST, MST, PST, etc.

Clearly the timezone abbreviation alone will sadly not be sufficient for a worldwide solution.

This lead me to design requiring the abbreviation (eg. PST) and the offset (eg. -0800). This helps both in readability for the user and explicitness for the data.

This means if you give an abbreviation of PST and offset of -0700, you will get an error.

find_real_file.png

Since PST's offset is -0800 for Pacific Standard Time, or +0800 for Philippine Standard time. Either -0800 or +0800 would not result in an error since either are valid values.

 

What is Validated

If a user doesn't have " z (ZZ)" on their time format setting, things work as they did before.

If a user is using " z (ZZ)" the script will execute 3 checks.

1. Remove the timezone and offset and do the original check it would have done.

2. Add back in the offset and check if JavaScript's Date function can understand the input. This doesn't include the abbreviation since many abbreviations will cause JavaScript's Date function to fail.

3. Use moment library to ensure the abbreviation and offset are valid values.

 

What I've manually tested

I've tested a handful of timezones and things appear to enter correctly and translate correctly to my timezone. Both within the tool and through the support portal. 

I'm in the Pacific Timezone, so this means when I enter:

TestEnteredSee After Submit 
Same Timezone2020-05-19 00:00:00 PDT (-0700)2020-05-19 00:00:00 PDT (-0700) 
Standard Time during Daylight Savings2020-05-19 00:00:00 PST (-0800)2020-05-19 01:00:00 PDT (-0700) 
India to Pacific2020-05-19 00:00:00 IST (+0530)2020-05-18 11:30:00 PDT (-0700) 
Central Europe to Pacific2020-05-19 00:00:00 CEST (+0200)2020-05-18 15:00:00 PDT (-0700) 
China to Pacific2020-05-19 00:00:00 CST (+0800)2020-05-18 09:00:00 PDT (-0700) 
Easter Island to Pacfic (Easter Island's timezone does not have a name)2020-05-19 00:00:00 -05 (-0500)2020-05-19 00:00:00 PDT (-0700)Failed
Kiritimati to Pacific (Kiritimati's timezone does not have a name)2020-05-19 00:00:00 +14 (+1400) 2020-05-19 00:00:00 PDT (-0700)Failed
No Offset Positive to Pacific2020-05-19 00:00:00 WET (+0000) 2020-05-18 17:00:00 PDT (-0700) 
No Offset Negative to Pacific2020-05-19 00:00:00 WET (-0000) 2020-05-18 17:00:00 PDT (-0700) 
Chatham to Pacific (Chatham's timezone does not have a name)2020-05-19 00:00:00 +1345 (+1345) 2020-05-18 03:15:00 PDT (-0700) 
Enderbury to Pacfic (Enderbury's timezone does not have a name)2020-05-19 00:00:00 -12 (-1200) 2020-05-19 00:00:00 PDT (-0700)Failed

During my tests I realized a few things.

1. If a timezone has no name, this gets very messy. This situation is unlikely, but problematic. From what I can tell, JavaScript can handle this by ignoring the name and using only the offset, but Java's SimpleDateFormat isn't prepared for names like -05 or +14. It happens to understand +1345 because it thinks it's an offset, not a name. These timezones aren't even selectable on OSX, so I'm not sure if these issues can even happen. There is about 7.6 billion people on earth, and about 50,000 live in a timezone without a name.

2. Timezone's change frequently. Moment-timezone should be updated regularly either automatically or manually or referenced externally.

 

Concerns

1. I wish I could figure out a way to test both the UI for every timezone possible and the DB.

2. Changing Timezone Data

3. The possibility exists that a user doesn't have a timezone. This is both extremely rare and possibly the problem is already solved by their device's OS choosing a timezone. I suspect these users are used to dealing with these issues.

 

Overall I'm very happy with this. I plan to do more testing and see what other issues may exist. But I am optimistic. 

 

If anyone has concerns about my approach or test coverage. Please let me know your thoughts.

View solution in original post

12 REPLIES 12

If you do get this to work on the portal and the system UI I would like to know.  We have people complain about similar things and may consider this.  Right now we forced everyone to there local time zone and told everyone to look at it from that perspective.

 

I've spent about 2 hours on this and this is what I currently have. I'm wondering how I could go about writing tests for this. From my initial testing things appear to be working except for some EXTREME edge cases that are unlikely to happen.

Implementation

1. Under UI Scripts I've added moment.js (https://momentjs.com/downloads/moment.min.js)

Set it as a global script.

find_real_file.png

2. Under UI Scripts I've added moment-timezone.js (https://momentjs.com/downloads/moment-timezone-with-data-1970-2030.min.js)

Set it as a global script.

find_real_file.png

3.Under System Properties I've changed Time format ('glide.sys.time_format') to "HH:mm:ss z (ZZ)".

https://<YOUR_INSTANCE>.service-now.com/nav_to.do?uri=%2Fsystem_properties_ui.do%3Fsysparm_category%3DSystem%26sysparm_title%3DSystem%2520Properties

find_real_file.png

 

4. Change the Validation Script

https://<YOUR_INSTANCE>.service-now.com/nav_to.do?uri=sys_script_validator.do?sys_id=829b0bb0533131008d7d154f811c08f7

function validate(value) {
    // empty fields are still valid dates 
    if (!value) 
        return true; 

    // We "should" have the global date format defined always defined. but there's always that edge case...
    if (typeof g_user_date_time_format !== 'undefined') {
		
		//Check if format is "{{DATE}} {{TIME}} {{TIMEZONE}} ({{OFFSET}})"
		var dateTimeParts = g_user_date_time_format.split(' ');
		
		if (dateTimeParts.length === 4 && dateTimeParts[2] === 'z' && dateTimeParts[3] === "(ZZ)") {
			var valueDateTimeParts = value.split(' ');
			
			//There must be 4 parts to check.
			if (valueDateTimeParts.length === 4) {
				
				//Check the first two parts exactly like they were before
				if (!isDate(valueDateTimeParts[0] + ' ' + valueDateTimeParts[1], dateTimeParts[0] + ' ' + dateTimeParts[1])) {
					return getMessage("Invalid Date");
				}
				
				//Check if Date gets a valid date with this value
				var dateTimeVal = new Date(valueDateTimeParts[0] + ' ' + valueDateTimeParts[1] + ' ' + valueDateTimeParts[3]).getTime();
				if (isNaN(dateTimeVal)) {
					return getMessage("Invalid Date");
				}
				
				//Check if the timezone and offset are valid
				if (!isValidTimezone(valueDateTimeParts[2], valueDateTimeParts[3])) {
					return getMessage("Invalid Date");
				}
				
				return true;
				
			} else {
				return getMessage("Invalid Date");
			}
			
		} else {
			return isDate(value, g_user_date_time_format) ? true : getMessage("Invalid Date"); 
		}
		
	}

    // if we don't have that defined, we can always try guessing 
    return parseDate(value) !== null ? true : getMessage("Invalid Date"); 
	
}

function isValidTimezone(abbr, offset){
	//Offset should be the format (+0000)
	if(offset.length !== 7) return false;
	
	//remove brackets get each character
	offset = offset.substr(1,5).split('');
	
	//If it's a '-' it should be a positive number, if it's a + it should be negative
	//The first 2 digits are the number of hours for the offset
	//The second 2 digits are the number of minutes for the offset
	var offsetVal = (offset[0] === '-' ? 1 : -1) * (((offset[1]+offset[2]) * 60) + parseInt(offset[3]+offset[4]));
	
	return moment.tz.names()
		//Convert to list of Timezone Objects
		.map(function(z){ return moment.tz.zone(z); }) 
		//Check if the timezone exists at all
		.filter(function(tz){
			//The abbreviation and offset must both exist and be the same index
			var abbrsInd = tz.abbrs.indexOf(abbr);
			return  abbrsInd !== -1 && abbrsInd === tz.offsets.indexOf(offsetVal); 
		}) 
		.length; //return the number of valid results
	
}

 

Problems Considered

Here were some of my considerations.

CST = Central Standard Time (-0600), China Standard Time (+0800), Cuba Standard Time (-0500).

Similar problems exist for ECT, IST, MST, PST, etc.

Clearly the timezone abbreviation alone will sadly not be sufficient for a worldwide solution.

This lead me to design requiring the abbreviation (eg. PST) and the offset (eg. -0800). This helps both in readability for the user and explicitness for the data.

This means if you give an abbreviation of PST and offset of -0700, you will get an error.

find_real_file.png

Since PST's offset is -0800 for Pacific Standard Time, or +0800 for Philippine Standard time. Either -0800 or +0800 would not result in an error since either are valid values.

 

What is Validated

If a user doesn't have " z (ZZ)" on their time format setting, things work as they did before.

If a user is using " z (ZZ)" the script will execute 3 checks.

1. Remove the timezone and offset and do the original check it would have done.

2. Add back in the offset and check if JavaScript's Date function can understand the input. This doesn't include the abbreviation since many abbreviations will cause JavaScript's Date function to fail.

3. Use moment library to ensure the abbreviation and offset are valid values.

 

What I've manually tested

I've tested a handful of timezones and things appear to enter correctly and translate correctly to my timezone. Both within the tool and through the support portal. 

I'm in the Pacific Timezone, so this means when I enter:

TestEnteredSee After Submit 
Same Timezone2020-05-19 00:00:00 PDT (-0700)2020-05-19 00:00:00 PDT (-0700) 
Standard Time during Daylight Savings2020-05-19 00:00:00 PST (-0800)2020-05-19 01:00:00 PDT (-0700) 
India to Pacific2020-05-19 00:00:00 IST (+0530)2020-05-18 11:30:00 PDT (-0700) 
Central Europe to Pacific2020-05-19 00:00:00 CEST (+0200)2020-05-18 15:00:00 PDT (-0700) 
China to Pacific2020-05-19 00:00:00 CST (+0800)2020-05-18 09:00:00 PDT (-0700) 
Easter Island to Pacfic (Easter Island's timezone does not have a name)2020-05-19 00:00:00 -05 (-0500)2020-05-19 00:00:00 PDT (-0700)Failed
Kiritimati to Pacific (Kiritimati's timezone does not have a name)2020-05-19 00:00:00 +14 (+1400) 2020-05-19 00:00:00 PDT (-0700)Failed
No Offset Positive to Pacific2020-05-19 00:00:00 WET (+0000) 2020-05-18 17:00:00 PDT (-0700) 
No Offset Negative to Pacific2020-05-19 00:00:00 WET (-0000) 2020-05-18 17:00:00 PDT (-0700) 
Chatham to Pacific (Chatham's timezone does not have a name)2020-05-19 00:00:00 +1345 (+1345) 2020-05-18 03:15:00 PDT (-0700) 
Enderbury to Pacfic (Enderbury's timezone does not have a name)2020-05-19 00:00:00 -12 (-1200) 2020-05-19 00:00:00 PDT (-0700)Failed

During my tests I realized a few things.

1. If a timezone has no name, this gets very messy. This situation is unlikely, but problematic. From what I can tell, JavaScript can handle this by ignoring the name and using only the offset, but Java's SimpleDateFormat isn't prepared for names like -05 or +14. It happens to understand +1345 because it thinks it's an offset, not a name. These timezones aren't even selectable on OSX, so I'm not sure if these issues can even happen. There is about 7.6 billion people on earth, and about 50,000 live in a timezone without a name.

2. Timezone's change frequently. Moment-timezone should be updated regularly either automatically or manually or referenced externally.

 

Concerns

1. I wish I could figure out a way to test both the UI for every timezone possible and the DB.

2. Changing Timezone Data

3. The possibility exists that a user doesn't have a timezone. This is both extremely rare and possibly the problem is already solved by their device's OS choosing a timezone. I suspect these users are used to dealing with these issues.

 

Overall I'm very happy with this. I plan to do more testing and see what other issues may exist. But I am optimistic. 

 

If anyone has concerns about my approach or test coverage. Please let me know your thoughts.

Did you test for Daylight savings and this?  Also what are you doing for the Service Portal?

 

Yes, I did test for Daylight savings and it does work. Both for entering data and viewing.

find_real_file.png

For the Service Portal, the inputs show in the same format.

find_real_file.png

Unlike within the platform, the portal will only allow me to enter for my timezone, but at least it will show me what the timezone it is using.

The downside is if I'm entering data for information given to me, in the portal I would have to translate it to my timezone. While a downside, it's no different than it is today, just more clear with the timezone being displayed.

The upside is that validation is easier because the input will not allow you to enter an invalid date value.

An additional upside is when I save the date with the timezone to a table, I don't have to use gs.dateGenerate or GlideDateTime's set Local/UTC/Server value functions to translate the entered dates, since the timezone and offset being included will handle that for us.

edit: I didn't think about this till later, but if you allowed users the option to not display timezones, the Support Portal's forms could have problems not translating the values to the correct value to be stored in the tables.

Nice, you could keep us posted if issues come up with this and how it goes once users get ahold of it.  🙂