Detect the ID of the current user timezone using moment.js - javascript

What I'm looking for is a way to detect the browser's timezone ID (as defined in the Olson tables) but I don't care for the exact ID, I just need the ID of a timezone that works the same as the user's one (for example "Europe/Rome" is fine if the user is in Paris).
I'm not interested in the current offset, I really need the timezone so that I can send it to the server to do computations for other dates (the server has the Olson tables too).
Theoretically, as I already use Moment.js timezone library and have included the Olson tables, I don't need anything else, but I don't find any API to do the detection. I don't know if it's hidden somewhere or if somebody has it already written. One of the problems is that the current timezone plugin seems to keep its data private.
I dont' want a solution based on the integration of yet another copy or extract of the Olson tables (which I would have to maintain), I know there are a few libraries duplicating them, I want to use the ones I already have in Moment.js.

I made a small script to do that detection. It starts by registering the ids of the available timezones, then, on a call to the matches function tests all timezone ids for the current time and the times 4 and 8 months later (to filter out the timezones with different daylight rules) and five years before.
Here it is :
<script src="moment-with-langs.min.js"></script>
<script src="moment-timezone.min.js"></script>
<script src="moment-timezone-data.js"></script>
<script>
var tzdetect = {
names: moment.tz.names(),
matches: function(base){
var results = [], now = Date.now(), makekey = function(id){
return [0, 4, 8, -5*12, 4-5*12, 8-5*12, 4-2*12, 8-2*12].map(function(months){
var m = moment(now + months*30*24*60*60*1000);
if (id) m.tz(id);
return m.format("DDHHmm");
}).join(' ');
}, lockey = makekey(base);
tzdetect.names.forEach(function(id){
if (makekey(id)===lockey) results.push(id);
});
return results;
}
};
</script>
If you just want one timezone id, simply use
var tzid = tzdetect.matches()[0];
Demonstration
GitHub Repository : https://github.com/Canop/tzdetect.js
Update : The code here shown isn't compatible with the most recent versions of moment.js. Please refer to the GitHub repository for a maintained (free to use) code.
2017 Update: There's now an API in moment.js to guess the timezone. That's probably the best solution right now.

If you want to use the standard JavaScript API, you can use Intl.DateTimeFormat.resolvedOptions where there is browser support:
Intl.DateTimeFormat().resolvedOptions().timeZone; // "America/Los_Angeles"
resolvedOptions is currently (Jan 2016) available in all browsers except Safari on iOS and desktop: http://caniuse.com/#feat=internationalization
However, the timeZone property is currently only available on Chrome.

moment now has the guess() API as described here

There's a javascript tool that does just that :
https://github.com/entraigas/jstz
It seems to deal with timezones ambiguity also.
Combined with momentJS timezone, you can get the timezone and show formatted date :
var tzObj = jstz.determine();
var timezoneCode = tzObj.name();
console.log(moment.tz(timeZoneCode).format());

Related

How to check Daylight Saving Time with Temporal in Javascript?

In case I have a Date and I want to check if the time is DST I can use a method, such as the following:
function isDST(d) {
let jan = new Date(d.getFullYear(), 0, 1).getTimezoneOffset();
let jul = new Date(d.getFullYear(), 6, 1).getTimezoneOffset();
return Math.max(jan, jul) != d.getTimezoneOffset();
}
(source here)
In case I use MomentJS library I reach the same in this way:
moment().isDST();
Anyone knows how to do the same with the upcoming Temporal?
I'd recommend not doing this, at least not in this form.
Aside from the Ireland example mentioned in the comment, there are other time zone jurisdictions where there are one-time or non-semiannual changes in the offset from UTC that occurred for other reasons than DST, and any possible isDST() implementation will, by definition, malfunction in these cases. Another example is that Morocco observes year-round DST except during the month of Ramadan. For most of the world's population, "DST" has no meaning at all.
To solve this, I'd start by asking what you are going to use the information for?
If it's, for example, to specify "Daylight" or "Standard" time in the name of a time zone, you could instead use Intl.DateTimeFormat with the { timeZoneName: 'long' } option, which will give you the name of the time zone with this information included.
If you need it as a drop-in replacement for Moment's isDST() method so that you can port an existing system from Moment to Temporal, I'd recommend reimplementing the Moment function exactly, and plan to move away from the concept of "is DST" in the future. (Note, that the Moment documentation also describes this function as a hack that sometimes doesn't provide correct information.)
The body of the Moment function can be found here and the equivalent for Temporal would be:
function isDST(zdt) {
return (
zdt.offsetNanoseconds > zdt.with({ month: 1 }).offsetNanoseconds ||
zdt.offsetNanoseconds > zst.with({ month: 6 }).offsetNanoseconds
);
}
Another thing that you might need this information for is to interface with other systems that include an "is DST" bit in their data model (which is an incorrect concept, but you might have no choice.) In this case I'd recommend restricting the "is DST" function to a list of allowed time zones that are known to employ the concept of "DST" and returning false in other cases, which should at least filter out some of the false positives.
if (!listOfTimeZoneIDsWithDST.includes(zdt.timeZone.id))
return false;
the temporal api has a offsetNanoseconds read-only property
zdt = Temporal.ZonedDateTime.from('2020-11-01T01:30-07:00[America/Los_Angeles]');
zdt.offsetNanoseconds;
// => -25200000000000
also there's the with method which returns a new object with specified field being overwritten.
i have to admit i haven't tested it but something like this should basically be the equivalent to your function. (month index starts at 1)
function isDST(d) {
let jan = d.with({month: 1}).offsetNanoseconds ;
let jul = d.with({month: 7}).offsetNanoseconds ;
return Math.min(jan, jul) != d.offsetNanoseconds ;
}
zoned DateTime
refine dev
web dev simplified

moment.js mock local() so unit test runs consistently

I want to test the following piece of code. I am wondering if there is a way to mock moment.js or force it to think my current location is America/New_York so that my unit test doesn't fail in gitlab.ci runner which may be in various geographical locations?
const centralTimeStartOfDay = moment.tz('America/Chicago').startOf('day');
const startHour = centralTimeStartOfDay
.hour(7)
.local()
.hour();
Basically I want to hard code my timezone to be America/New_York and want this function to behave consistently.
Edit:
I tried:
Date.now = () => new Date("2020-06-21T12:21:27-04:00")
moment.tz.setDefault('America/New_York')
And still, I get the same result. I want to mock the current time so startHour returns a consistent value.
The problem
So there is no one line answer to this question. The problem is a fundamental one to javascript, where you can see dates in one of two ways:
UTC (getUTCHours(), getUTCMinutes() etc.)
local (i.e. system, getHours(), getMinutes() etc.)
And there is no specified way to set the effective system timezone, or even the UTC offset for that matter.
(Scan through the mdn Date reference or checkout the spec to get a feeling for just how unhelpful this all is.)
"But wait!" we cry, "isn't that why moment-timezone exists??"
Not exactly. moment and moment-timezone give much better / easier control over managing times in javascript, but even they have no way to know what the local timezone Date is using, and use other mechanisms to learn that. And this is a problem as follows.
Once you've got your head round the code you'll see that the moment .local() method (prototype declaration and implementation of setOffsetToLocal) of moment effectively does the following:
sets the UTC offset of the moment to 0
disables "UTC mode" by setting _isUTC to false.
The effect of disabling "UTC mode" is to mean that the majority of accessor methods are forwarded to the underlying Date object. E.g. .hours() eventually calls moment/get-set.js get() which looks like this:
export function get(mom, unit) {
return mom.isValid()
? mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]()
: NaN;
}
_d is the Date object that the moment (mom) is wrapping. So effectively for a non-UTC mode moment, moment.hours() is a passthrough to Date.prototype.getHours(). It doesn't matter what you've set with moment.tz.setDefault(), or if you've overridden Date.now(). Neither of those things are used.
Another thing...
You said:
Basically I want to hard code my time to be America/New_York and want this function behaves consistently
But actually, that is not generally possible. You are using Chicago, which I imagine has offset shifts in sync with New York, but e.g. the UK shifts at a different time from the US, so there are going to be weeks in the year where your test would fail if you were converting from a US timezone to a UK timezone.
The solutions.
But this is still frustrating, because I don't want my devs in Poland and the west coast of America to have breaking local tests because my CI server is running in UTC. So what can we do about it?
The first solution is a not-a-solution: find a different way of doing the thing you're doing! Generally the use cases for using .local() are quite limited, and are to display to a user the time in their current offset. It's not even their timezone because the local Date methods will only look at the current offset. So most of the time you'd only want to use it for the current time, or if you don't mind if it's wrong for half of the Date objects you use it for (for timezones using daylight savings). It could well be better to learn the timezone the user wants through other means, and not use .local() at all.
The second solution is also a not-a-solution: don't worry about your tests so much! The main thing with displaying a local time is that it works, you don't really care what it is exactly. Verify manually that it's displaying the correct time, and in your tests just verify that it returns a reasonable looking thing, without checking the specific time.
If you still want to proceed, this last solution at least makes your case work and a few others, and it's obvious what you need to do if you find you need to extend it. However, it's a complicated area and I make no guarantees that this will not have some unintended side-effects!
In your test setup file:
[
'Date',
'Day',
'FullYear',
'Hours',
'Minutes',
'Month',
'Seconds',
].forEach(
(prop) => {
Date.prototype[`get${prop}`] = function () {
return new Date(
this.getTime()
+ moment(this.getTime()).utcOffset() * 60000
)[`getUTC${prop}`]();
};
}
);
You should now be able to use moment.tz.setDefault() and using .local() should allow you to access the properties of the datetime as though it thought the local timezone was as configured in moment-timezone.
I thought about trying to patch moment instead, but it is a much more complicated beast than Date, and patching Date should be robust since it is the primitive.
try
// package.json
{
"scripts": {
"test": "TZ=EST jest"
}
}
Brilliant daphtdazz - thank you! To clarify for those who follow, this is the full solution I used to control the current date, timezone, and with daphtdazz's help - the local() behavior in moment:
import MockDate from 'mockdate';
const date = new Date('2000-01-01T02:00:00.000+02:00');
MockDate.set(date);
[
'Date',
'Day',
'FullYear',
'Hours',
'Minutes',
'Month',
'Seconds',
].forEach(
(prop) => {
Date.prototype[`get${prop}`] = function () {
return new Date(
this.getTime()
+ moment(this.getTime()).utcOffset() * 60000
)[`getUTC${prop}`]();
};
}
);
const moment = require.requireActual('moment-timezone');
jest.doMock('moment', () => {
moment.tz.setDefault('Africa/Maputo');
return moment;
});

How can i get the timezone value from a moment object?

I have a moment object like below
from which i want to extract the time zone value "PDT".
How can i get this value using moment?
i have tried
moment.tz(this.startDate.toString()).zoneAbbr();
but that returns a value "UTC"
Any suggestions?
I am Afraid you will get this only..
Since this is how the getZoneAbbr function works.
function getZoneAbbr () {
return this._isUTC ? 'UTC' : '';
}
Now, This is a problem. UTC is what one can use with timeOffest to determine which TimeZone you are lying in.
and only way to do it to make a list of Time zone names with their offset values, then compare which offset value you get.
then you can pick your time zone.
Even the normal JavaScript Date function do not have this support as in yet. you will have to create that offset and time zone name map yourself.
You might wan't to add the specific timezone you're in for the date that you parse like following:
moment.tz(moment.now(), 'America/New_York').zoneAbbr();
Moment had the z / zz format parameter to get abbreviated time zone name, but as the doc says:
Note: as of 1.6.0, the z/zz format tokens have been deprecated from plain moment objects. Read more about it here. However, they do work if you are using a specific time zone with the moment-timezone addon.
In the linked github issue, moment developer suggest to use moment.tz.guess() when creating moment object to have good chances of getting enviroment's timezone.
Here a working code example that you can use to get abbreviated time zone name:
var startDate = new Date();
var momObj = moment.tz(startDate, moment.tz.guess());
var zoneAbbr1 = momObj.zoneAbbr();
console.log(zoneAbbr1);
var zoneAbbr2 = momObj.format('z')
console.log(zoneAbbr2);
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.13.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.5/moment-timezone-with-data-2010-2020.min.js"></script>
Make sure you have installed moment-timezone with
npm install moment-timezone --save
After that
var moment1 = require('moment-timezone');
var myTime = moment1.tz('America/Denver').format('YYYY-MM-DD HH:mm:ss');
console.log(myTime);

What is the scope of moment.tz.setDefault() when used in Meteor?

TL;DR What is the scope of moment.tz.setDefault()?
I'm sure my problem here stems from my inexperience with both JavaScript and Meteor but I've been struggling with the problem for several straight days now.
I'm working on an app that must take into account the client's timezone but I'm having significant difficulty in forcing the server code to use the client's timezone. Somewhere along the way--that being from the moment the client presses "Submit" to the moment Meteor inserts--my timezone setting is getting lost and local time (of the server) is being used.
The app flow is like this:
(client) user submits form
(client) validation of data is performed
(server) Meteor method is called
(server) validation of data is performed (same code as earlier)
(server) business logic is applied
(server) insert into DB
I capture the timezone at step 1 and try to pass it along through all the steps but I must be missing something because between 4 and 5 the timezone is (seemingly) lost. The fast is, I'm not seeing why. I've checked this 100 times and tried all manner of different permutations but can't figure out where the gap is (I've used soooo many console.log()s it's crazy.)
So instead of trying to set the timezone at every point Moment() is used (because it defaults to calculating in local time) I discovered moment.tz.setDefault() and tried using that at least once on each .js file in my app. But it didn't work.
Reading this it might sound like I'm not doing enough testing but that is not the case. I have spent 10s of hours on this and I'm just not getting it. I'd love to share the code but I think it's just too long and complicated to properly share so I've done my best to explain the problem.
Good news! You're overcomplicating it :-)
Open up a browser console & type time = new Date(). Notice how it's in the correct timezone? That's because the timezone conversion is happening on the client.
Now, type time.valueOf(). As you probably know, you've got the number of milliseconds since 1-1-1970...but in what timezone?? You guessed it, UTC!
So if all you're doing is saving a number, and the client is fully capable of converting that number into the local timezone, why not save the time in UTC on the server? You'll get an ISODate() in your database (which is a fancy int64). Then, when you retrieve it on the client, you can put it in their local time (they might be traveling!) or any other timezone you chose. If it's a meetup in a certain city, simply grab the timezone of that city & apply it to the field. Hint: THIS is the appropriate time to use moment.js, not before!
Edit for time patterns:
Based on the new info, I imagine you have something that accepts an arrivalTime & then makes sure the time is between an earlyArrival and lateArrival say, 7:00 - 8:30AM. So, save the times as dates
timeToDate = function(time) {
return new Date('1970 1 1 ' + time);
};
earlyArrival = timeToDate('7:30 AM');
arrivalTime = timeToDate('8:00 AM');
lateArrival = timeToDate('8:30 AM');
Then, validate via simple math: earlyArrival < arrivalTime.
OR, if you use simple schema (which you should), a validation pattern might look like this:
departureTime: {
type: Date,
min: timeToDate('5:00 PM'),
max: timeToDate('6:30 PM'),
autoValue: function() {
return timeToDate(this.value);
},
custom: function () {
if (this.value < this.field('arrivalTime').value) {
return "lateAfterEarly";
}
}

In JSF - Getting the Client's Locale (To Display time in browser's timezone)

I'm writing a webapp in JSF 2.0 that displays Timestamps as part of the information to the user.
I would like the user to see the timestamps localized to their location (browser's locale).
So far, whatever I did, the timestamps would always appear localized to the timezone of the server.
I tried getting the locale with these methods:
Locale browserLocale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
or
Locale browserLocale = FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
Both returned the server's locale.
I then use the locale object with SimpleDateFormat to print timestamps.
Am I using the correct API?
I've read somewhere that you have to use client side code (Javascript) to get the browser's timezone. Is that correct? How would you do that?
Thank's 'n' Advance.
UPDATE found this (jsTimezoneDetect) JavaScript code. But I'm still not sure how to translate the timezone to a Java Locale Object
You probably should use built-in formatting tag instead of SimpleDateFormat. Your question implies that you want to show date and time to International user, and in this case you should really use user's local format (they tend to differ, you know).
In case of time zone, it has nothing to do with Internationalization and Localization, i.e. there are several different time zones in USA. There are two approaches you can use here:
Store time zone information in the user profile (if you have one). This is the easiest way and allow you to use built-in <f:convertDateTime> tag.
Get time zone information from web browser. You can get it via Ajax request just like in Ben's example. Technically you can also use <f:convertDateTime> tag here.
You can send the timestamps in UTC in some common, locale-independent (or invariant if you prefer) format, parse it on the client side to create JavaScript's date object and format for locale with Globalize.
Some examples will follow but let me explain something first.
Locale browserLocale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
This will give you web browser's locale (but not time zone, since this is not locale related). It will actually read the contents of HTTP Accept-Language header and choose the best possible locale. If it is not working for you, please make sure that you have correctly set supported locales in your faces-config.xml. By best possible Locale, I understand that it will try to use user's most preferred Locale (if that's supported by your application), then second best and so on. If none of the Locales is supported, it will fall-back to your application's default Locales (again, faces-config.xml has to have this setting) or to server's default Locale if this setting is missing (or at least I think so, it kind of makes sense).
Locale browserLocale = FacesContext.getCurrentInstance().getExternalContext().getRequestLocale();
This one will give you the top Locale from Accept-Language header. Please check your web browser's settings - there is almost no way for it to give you the server Locale, unless they are exactly the same as your web browser's. It can give you server's defaults if and only if, none of the web browser's Locale is supported by JVM (which seems a bit unlikely).
BTW. FacesContext.getCurrentInstance().getExternalContext().getRequestLocales() will give you the Iterator so you can manually iterate through the list of Locales in Accept-Language header. It is just to let you know, you probably should not use it (UIViewRoot is really enough).
Now, suppose you have some bean with the user profile and the method which will give you the time zone. This method is better than Ajax call, in the sense that it might happen that two different time zones have the same UTC offset but switch Daylight Saving Time on different date (in other words some timestamps would be printed incorrectly). Anyway, in case like this, you can format your time-stamp like this (date also come from some bean):
<h:outputText value="#{someBean.timestamp}">
<f:convertDateTime type="both" dateStyle="default" timeStyle="default" timeZone="#{userProfile.timeZone}" />
</h:outputtext>
Now let me explain the attributes:
type - what to show, both means date and time
dateStyle - style of date (out of short, medium, long, full, default). You should really use default here as this will use the most proper format for each Locale
timeStyle - similar to date style but for time part
timeZone - takes either an UTC offset (so you don't need to convert anything) or time zone name (i.e. America/Los_Angeles).
The tag will use current view Locale by default, so you do not have to worry about this part, especially if you set up Locale support correctly.
Combining it with Ajax (see Ben's answer) would be easy enough (I think).
I also mentioned that you can write out invariant dates, parse them on the client-side and then format them with Globalize. Assuming that you have parsed date already (it depends on the representation you want to use, so I will skip this part), it could be done like that:
// you also have to assign the culture based on UIViewRoot locale and send it out with JavaScript
Globalize.culture(theLocale);
var formattedDateTime = Globalize.format(parsedDateTime, "f"); // this will use short date time format
Unlike Java, Globalize have only short ("f") and long ("F") date and time formats. So I decided on using short one.
Please also keep in mind, that Globalize cultures are separated via hyphen, not underscore, so you need "fr-CA", not "fr_CA" for example.
Please let me know if you want to use that method and need more concrete example.
Succeeded.
Here is what I did:
Added to JSF a hidden input field so I can send JavaScript values to the server:
<h:form prependId="false">
<h:inputText id="timezone_holder" value="#{bean.timezone}" styleClass="hide">
<f:ajax listener="#{bean.timezoneChangedListener}"></f:ajax>
</h:inputText>
</h:form>
Using the plugin above, I ran JavaScript code that retrieved the offset of the browser.
$(document).ready(function() {
var timezone = jstz.determine_timezone();
$("#timezone_holder").val(timezone.offset());
$("#timezone_holder").change();
});
When the timezone input is changed (initiated from the javascript code above) I run this code in the eventListener:
String strFromJavaScript = getTimezone();
HttpServletRequest request = (HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext()
.getRequest();
Locale browserLocale = request.getLocale();
TimeZone tz = TimeZone.getTimeZone("GMT" + strFromJavaScript);
// set time zone
SimpleDateFormat formatter = new SimpleDateFormat("MMM d, yyyy, HH:mm", browserLocale);
formatter.setTimeZone(tz);
Then, whenever I need to format a date I use the Date Formatter that was created above.
Another option would be to create a cookie on a Javascript that executes when the home page is ready. After that, the cookie will exist on each subsequent request and would be available
Your Javascript could use jQuery and jsTimezoneDetect
$(document).ready(function() {
setTimezoneCookie();
});
function setTimezoneCookie() {
var timezone = jstz.determine().name();
if (null == getCookie("timezoneCookie")) {
document.cookie = "timezoneCookie=" + timezone;
}
}
function getCookie(cookieName) {
var cookieValue = document.cookie;
var cookieStart = cookieValue.indexOf(" " + cookieName + "=");
if (cookieStart == -1) {
cookieStart = cookieValue.indexOf(cookieName + "=");
}
if (cookieStart == -1) {
cookieValue = null;
} else {
cookieStart = cookieValue.indexOf("=", cookieStart) + 1;
var cookieEnd = cookieValue.indexOf(";", cookieStart);
if (cookieEnd == -1) {
cookieEnd = cookieValue.length;
}
cookieValue = unescape(cookieValue.substring(cookieStart, cookieEnd));
}
return cookieValue;
}
Your Facelet would then use the cookie's value if it exists:
<h:outputText value="#{bean.time}">
<f:convertDateTime
dateStyle="full"
timeStyle="full"
type="both"
timeZone="#{cookie.timezoneCookie.value}">
</f:convertDateTime>
</h:outputText>
You may want to try jsTimezoneDetect to detect timezone on the client side and send to the server.
UPDATE: to get a user's Locale, you can try the following:
HttpServletRequest request = (HttpServletRequest)FacesContext.getCurrentInstance().getExternalContext().getRequest();
Locale locale = request.getLocale();
Returns an Enumeration of Locale objects indicating, in decreasing
order starting with the preferred locale, the locales that are
acceptable to the client based on the Accept-Language header. If the
client request doesn't provide an Accept-Language header, this method
returns an Enumeration containing one Locale, the default locale for
the server.
If all you need is to display time stamps with the users local time you dont need the Locale object (you need to add the users hours offset to GMT + 0 time), Y
You need to send the value of timezone.offset() (from the example in the link) to the server (you can do it by using servlet post with parameter)
and then add this offset to the date object created on your server (set your locale on the server to be GMT + 00)
that way you will create time stamps with the correct time of the user that is logged into your web app
(that's what I did myself...)
You can also use it like this direct in JSF code:
<script type="text/javascript">
$(document).ready(function () {
var timezone = jstz.determine_timezone();
$("#timezone_holder").val(timezone.name()); //use TimeZone name instead of offset
$("#timezone_holder").change();
});
</script>
Then you can reuse the timezonename in a JSF converter:
<f:convertDateTime pattern="dd.MM.yyyy HH:mm" timeZone="#{bean.timezone}" />

Categories

Resources