How to change timezone without changing date value - javascript

My scenario is this:
I have a datepicker that accepts input from the user and it assumes the date/time picked is local time. I can't seem to change this behavior. The user should actually be picking the date/time in Europe/London time as they are booking a service in London at that time.
Lets say they are booking a flight from London at 5AM.
So I have this date the user picked (from New Zealand):
2019-08-01T05:00:00+12:00 (NZ time)
I need to change this to:
2019-08-01T05:00:00+01:00 (Europe/London time)
After that I am sending to server and storing as UTC.
I am using momentjs if that helps.
Edit:
Latest attempt:
I just need to figure out how to get the timezone offset from the exact time the user entered so I have daylight savings covered.
e.g. below I am getting the UK offset for the day before so may have DST issues
const moment = require('moment-timezone');
const nzd = '2019-08-01T05:00:00+12:00'; // This is the date my user entered via the datepicker
const ukTempString = moment(new Date(nzd.slice(0, -6))).tz('Europe/London').format('YYYY-MM-DDTHH:mm:ssZ'); // 2019-07-31T18:00:00+01:00
const ukOffset = ukTempString.substr(ukTempString.length - 6); // +01:00
const ukString = nzd.slice(0, -6) + ukOffset; // 2019-08-01T05:00:00+01:00
const ukDate = new Date(ukString); // I can then this to the backend
Edit:
Current solution:
No DST in UK example:
let nzd = moment('2019-10-27T05:00:00+13:00'); // This is the date my user entered via the datepicker
let nzString = nzd.format('YYYY-MM-DDTHH:mm:ssZ');
let ukd = moment().tz('Europe/London');
ukd.set(nzd.toObject());
console.log(ukd.format('YYYY-MM-DDTHH:mm:ssZ')); // 2019-10-27T05:00:00+00:00
let ukDate = new Date(ukd.format('YYYY-MM-DDTHH:mm:ssZ')); // I can then this to the backend
console.log(ukDate.toUTCString()); // Sun, 27 Oct 2019 05:00:00 GMT
Has DST in UK example (same code):
nzd = moment('2019-10-27T00:30:00+13:00'); // This is the date my user entered via the datepicker
nzString = nzd.format('YYYY-MM-DDTHH:mm:ssZ');
ukd = moment().tz('Europe/London');
ukd.set(nzd.toObject());
console.log(ukd.format('YYYY-MM-DDTHH:mm:ssZ')); // 2019-10-27T00:30:00+01:00
ukDate = new Date(ukd.format('YYYY-MM-DDTHH:mm:ssZ')); // I can then this to the backend
console.log(ukDate.toUTCString()); // Sat, 26 Oct 2019 23:30:00 GMT

Here is a simpler solution:
moment("2019-08-01T05:00:00+12:00")
.parseZone()
.tz("Europe/London", true)
.toISOString(true) // => "2019-08-01T05:00:00.000+01:00"
To explain that a bit:
We parse the string, which interprets the string as being in NZ, but translates it to a local time, which may or may not be +12, depending on where your computer is
use parseZone to undo that conversion, so that our date thinks of itself as being in +12, per the original string
Convert the time to London time using tz, but with the a second argument (called keepTime) of true, which means "instead of keeping the time the same and changing the local time, change what time it is to keep the local time the same".
Format the result in ISO, but pass a second argument (called keepOffset) telling it not to convert to UTC before display
So, that works, but perhaps it would be better to change the timepicker to London time so that the times they pick are already in the zone you want?
Edit, the equivalent Luxon code is a little more legible, if that helps explain it:
DateTime.fromISO("2019-08-01T05:00:00+12:00", { setZone: true })
.setZone("Europe/London", { keepLocalTime: true} )
.toISO()

If you are not doing any date time conversion (like in your example), and just changing the time zone offset in the end of the string, just do a string operation, like this:
var nzd = '2019-08-01T05:00:00+12:00'
var ukd = nzd.slice(0, -6) + '+01:00'
console.log(ukd) // outputs 2019-08-01T05:00:00+01:00
If you need to convert the full date and time, as you are using momentjs, you could use moment-timezone instead: https://momentjs.com/timezone/ . Then the following code do the trick:
const moment = require('moment-timezone')
var nz = moment('2019-08-01T05:00:00+12:00')
var uk = nz.tz('Europe/London').format()
console.log(uk) // outputs 2019-07-31T18:00:00+01:00
If you want to get the time zone offset string, also use moment-timezone:
let ukd = "2019-10-27T01:59:00".split('T')
let finalDate = moment.tz(ukd[0] + "T" + ukd[1], "YYYY-MM-DDTHH:mm:ss", true, "Europe/London").tz('Europe/London').format('Z');
console.log(finalDate) // last minute still in DST, outputs +01:00
ukd = "2019-10-27T02:01:00".split('T')
finalDate = moment.tz(ukd[0] + "T" + ukd[1], "YYYY-MM-DDTHH:mm:ss", true, "Europe/London").tz('Europe/London').format('Z');
console.log(finalDate) // first minute after DST, outputs +00:00
This seems hacky and not clean code, but it's due to a bug in timezone that parses as UTC date instead of timezone date.

Related

Using Mongoose and Luxon to try and display date of event, but I'm getting the day before

I am using Mongoose and Luxon to display a date, selected by the user from a form. But the date is console.logging one date, but displaying on the page as the day before.
This is my model:
const mongoose = require("mongoose");
const { DateTime, Settings } = require("luxon");
// Configure time zone
console.log(Settings);
const Schema = mongoose.Schema;
let AccomplishmentSchema = new Schema({
dateInput: {
type: Date,
required: true,
},
textInput: {
type: String,
required: true,
},
});
AccomplishmentSchema.virtual("dateInput_formatted").get(function () {
return DateTime.fromJSDate(this.dateInput).toLocaleString(DateTime.DATE_FULL); // format 'YYYY-MM-DD
});
module.exports = mongoose.model("Accomplishment", AccomplishmentSchema);
And this is my console log vs what is showing up on the page
dateInput: 2023-01-01T00:00:00.000Z,
textInput: 'etst',
December 31, 2022
etst
I can only assume this has to do with some kind of time conversion, but I've tried changing the time zone, and the settings, although I could've read the docs wrong. I cannot find a way to fix this issue.
When working with JS Date, the most important thing to understand is that Date doesn't store or know about a time zone. It simply stores the number of milliseconds since midnight on Jan 1, 1970 (aka UNIX epoch) in the UTC time zone. Then, when you call Date methods, the results are calculated for the current user's time zone (for methods like getMinutes()) or UTC (for methods like getUTCMinutes()). But the underlying data inside the Date is just a timezone-less number of milliseconds.
With that in mind, let's look at a simple example: new Date(0), which is equivalent to new Date('1970-01-01T00:00Z').
If your computer's time zone set to the Europe/Paris time zone, then when you call new Date(0).toLocaleDateString('en-US') then you'll get a result of '1/1/1970'. But if you change your computer's time zone to America/Los_Angeles and run the same code, you'll get a result of '12/31/1969'.
This happens because the Date's stored value corresponds to midnight on Jan 1, 1970 in the UTC time zone. In Paris that instant was 1:00AM on Jan 1, 1970, while in California it was 4:00PM on December 31, 1969. The same exact Date will print a different date depending on the time zone that's active at the time that date is printed.
Where this becomes particularly problematic is in cases where you're trying to express a date without a time. For example, assume you store a Date value of 2023-01-01T00:00:00.000Z in MongoDB.
When you display that Date in a date picker in a browser in California, the date shown will be Dec 31, 2022, because California is 8 hours ahead of UTC.
One way to solve this problem is to tell Date that the date you're using is a UTC date. Like this:
utcDateStoredInMongoDB = new Date ('2023-01-01T00:00:00.000Z');
// Get the year, month, and day in that Date, from the perspective
// of the UTC time zone.
year = utcDateStoredInMongoDB.getUTCFullYear();
month = utcDateStoredInMongoDB.getUTCMonth();
day = utcDateStoredInMongoDB.getUTCDate();
// Use that year, month, day to initialize a new Date in
// the user's time zone.
dateForDatePicker = new Date(year, month, day);
// Use that for your date picker.
dateForDatePicker.toLocaleDateString();
// => '1/1/2023'
// To go in the other direction, pull out the year/month/day
// in the current time zone, and turn that into a UTC date.
dateReturnedByDatePicker = new Date(2023, 02, 15);
year = dateReturnedByDatePicker.getFullYear();
month = dateReturnedByDatePicker.getMonth();
day = dateReturnedByDatePicker.getDate();
dateToStoreInMongoDB = new Date(Date.UTC(year, month, day));
dateToStoreInMongoDB.toISOString();
// => '2023-03-15T00:00:00.000Z'
Note that this all will get a lot easier in a year or so when the new JavaScript built-in date/time API, called Temporal starts shipping in browsers and Node.js. With Temporal, these kinds of problems are much easier because there's a dedicated API, Temporal.PlainDate for dealing with date-only values.
Luxon is working as intended and the display is correct. What is wrong is the data in your MongoDB.
Most libraries consider current time zone when you parse a date. I live in Switzerland.
const moment = require("moment");
console.log( moment('2023-01-01').toDate() )
> 2022-12-31T23:00:00.000Z
const { DateTime } = require("luxon");
console.log( DateTime.fromISO('2023-01-01').toJSDate() )
> 2022-12-31T23:00:00.000Z
const dayjs = require('dayjs')
console.log( dayjs('2023-01-01').toDate() )
> 2022-12-31T23:00:00.000Z
However, the native JavaScript new Date() constructor does not!
console.log( new Date('2023-01-01') )
> 2023-01-01T00:00:00.000Z
Maybe this behavior depends on your environment and scripting engine, I don't know.
You should also use Luxon when you parse the input data - don't forget to use .toJSDate() to convert the DateTime object back to Javascript native Date object.
Luxon uses the system time zone by default. You can change it:
const { DateTime, Settings } = require("luxon");
Settings.defaultZone = 'America/New_York';
If you don't care about the time, you can also use startOf('day')
Settings.defaultZone = 'America/New_York';
DateTime.now().startOf('day').toJSDate()
> 2023-01-13T05:00:00.000Z
DateTime.now().startOf('day').toISO()
> '2023-01-13T00:00:00.000-05:00'

JavaScript Date time different once deployed to Heroku

Locally, everything is accurate by the minute. Once deployed to Heroku, the difference between the times are off by about 6 hours. I am not looking to convert Heroku time zone (would like it to remain UTC). I've tried everything from getTimezoneOffset() conversions to different date formats and I still end up with the same result. How can I have these 2 date times match each other and not be offset by hours when deployed? Why are they different, when formatted the exact same way?
// Used to calculate current date time
const currentDate = new Date();
// ^ Production - (2021-10-12T19:12:41.081Z)
const time = `${currentDate.getHours()}:${currentDate.getMinutes()}`;
const fullDate = `${currentDate.getMonth()}/${currentDate.getDate()}/${currentDate.getFullYear()}`;
const currentDateFormatted = new Date(`${fullDate} ${time}`);
// ^ Production - (2021-10-12T19:12:00.000Z)
const currentParsedDateToUTC = Date.parse(currentDateFormatted.toUTCString());
// Used to calculate an event date time
const eventDate = new Date(`${event.date} ${event.endTime}`); // same exact format as above
// ^ Production - (2021-10-12T13:12:00.000Z)
const eventParsedDateToUTC = Date.parse(eventDate.toUTCString());
const isExpired = (currentParsedDateToUTC > eventParsedDateToUTC); // works locally, but not in production
In this example, the event date and start time is identical to the current date time. How can I prevent them from being vastly different?
That is because the Heroku server is in a different timezone than yours, you can handle it by converting the time format from your frontend, I recommend you use moment.js for example in your frontend you can convert like this:
npm install moment --save
And then you can create a function just to change the format to display:
const formatDatetime = (
datetime = "N/A",
format = 'LLL' // here is your format
) => {
return moment(datetime).isValid()
? moment(datetime).format(format)
: datetime;
};
So -- Heroku is returning the correct UTC local time, as of this writing it is 2021-10-12T19:36:00.000Z.
You're asking Heroku to interpret 2012-10-12 13:12 as a date, but you're not specifying what timezone it should use, so it defaults to its own local time of UTC.
Everything here is working as expected.
What you I think are implicitly asking is that you want it to interpret 13:12 as being in your local time. However, Heroku has no way of knowing what your local time is, so you'll need to track the timezone of events in your database.
The only reason this is working locally is because your local server happens to be in the same timezone as you -- if I were to connect to your local server from my timezone, I'd experience the same problem.
The first four lines of code seem to be an attempt to create a Date and set the seconds and milliseconds to zero. That can be done as:
let d = new Date();
d.setSeconds(0,0);
which will set the seconds and milliseconds to zero.
I don't know what you think the following does:
const currentParsedDateToUTC = Date.parse(currentDateFormatted.toUTCString());
but an identical result is given by:
d.getTime();
which is actually the value returned in the previous call to setSeconds. So the first 5 lines of code reduce to:
let currentParsedDateToUTC = new Date().setSeconds(0,0);
Then in:
const eventDate = new Date(`${event.date} ${event.endTime}`);
A timestamp in the format d/m/y H:m is parsed using the built–in parser, which is a bad idea, see Why does Date.parse give incorrect results?. You can use a library instead or just write a 2 line function to do the job.
Then again there is:
const eventParsedDateToUTC = Date.parse(eventDate.toUTCString());
which is simpley:
const eventParsedDateToUTC = eventDate.getTime();
Finally there is:
const isExpired = (currentParsedDateToUTC > eventParsedDateToUTC);
comparison operators will coerce Dates to number for you, so you can leave the values as Dates.
A function to do the job is:
// eventDate is UTC timestamp in m/d/y H:m format
function isExpired(eventDate) {
// Parse eventDate as UTC
let [M,D,Y,H,m] = eventDate.split(/\W/);
let eventD = new Date(Date.UTC(Y, M-1, D, H, m));
// return true if has passed (minute precision)
return eventD < new Date().setSeconds(0,0);
}
// Event dates (UTC)
['10/12/2021 12:00', // 12 Oct 2021 12:00
'10/13/2021 12:00', // 13 Oct 2021 12:00
'10/13/2022 12:00', // 13 Oct 2022 12:00
].forEach(d =>
console.log(d + ' has' + (isExpired(d)? '':' not') + ' Expired')
);
Where you could use the value returned by Date.UTC(Y, M-1, D, H, m) without conversion to Date so the last two lines could be:
return Date.UTC(Y, M-1, D, H, m) < new Date().setSeconds(0,0);
but it's a bit more semantic (if unnecessary) to use a Date. :-)

Express time as CST in javascript - date-fns

I am using date-fns to format dates
If I pass a date that ends in Z, it knows that it is UTC, and I can format(date, "yyyy-MM-dd") and it will be in local computer time.
If the date I want to express in local computer time is originally CST, is there something to add at the end instead of the Z, that will be understood by the format function as a CST date?
Sorry if this is a bad question
Edit: is there a way to do zonedTimeToUtc(myDate, 'UTC-6') in date-fns? (instead of using a time zone name)
If you have a string that you always want parsed as CST (US central standard time) using date-fns, you can include date-fns-tz and set the timezone when parsing (I've assumed an ISO 8601 loose format without the timezone). Note that to avoid DST, you have to pick a location that is UTC-6 all year round, e.g. Canada/Saskatchewan.
// Setup
var {parse} = require('date-fns');
var {zonedTimeToUtc, utcToZonedTime, format } = require('date-fns-tz');
// Parse using location for offset
let loc = 'Canada/Saskatchewan';
let s = '2020-08-14 13:05:52';
let fIn = 'yyyy-MM-dd HH:mm:ss';
let utcDate = zonedTimeToUtc(s, loc);
// Show local equivalent
console.log(utcDate);
This leaves you somewhat at the mercy of the administrators of Saskatchewan, who might change the offset or introduce DST. An alternative is to append the exact offset you want to the timestamp and include it in the parse tokens:
// Parse using string for offset
let tz = '-06';
let utcDate2 = parse(s + ' ' + tz, fIn + ' X', new Date());
// Show local equivalent, should be same as above
console.log(utcDate2);
The advantage of the second method is that it doesn't require date-fns-tz and you aren't beholden to historic or future changes to Saskatchewan's offset (or that of any other IANA location).
Apparently there is a UTC module in development that will allow setting specific offsets like -6 rather than using IANA locations (can't find a link to that comment atm).
At this point the string has been parsed as GMT-6, but is still just a plain Date (i.e. just a time value with no idea of the timezone that was associated with the original string).
Once you have the date you can then show it as CST for output. To use an IANA location for the offset in call to format, you have to use format from date-fns-tz, not plain date-fns, otherwise it will just use the host system offset.
Note that the value in the format call is just setting the value to use for the offset string, it doesn't do anything to the actual date and time, that adjustment has already been applied by utcToZonedTime.
// Adjust to CST
let dCST = utcToZonedTime(utcDate2, loc);
// Format strings:
let fOut1 = 'yyyy-MM-dd HH:mm:ss XXX'; // -0600
let fOut2 = 'yyyy-MM-dd HH:mm:ss z'; // CST
// Format using location
console.log(format(dCST, fOut1, {timeZone: loc}));
console.log(format(dCST, fOut2, {timeZone: loc}));
I prefer the -0600 version as it avoids questions of whether DST is observed or not (and is really what the code is doing). Also, in the "z" version you might get the offset or the timezone name (probably depending on the host default language and location, which is a quirk of date-fns-tz using Intl.DateTimeFormat I think).
You can also manually add the timezone using a format string like:
let fOut = 'yyyy-MM-dd HH:mm:ss \'-0600\'';
which will produce an output like:
"2020-08-14 13:05:52 GMT-0600"
I don't think there is any way to set a specific offset like "-0600" for both parsing and formatting without including it in the call. I think moment.js and luxon allow it.
For completeness, here's some code you can run at npm.runkit.com since there's no CDN for the current date-fns version to allow the code to run here.
var {parse} = require('date-fns');
var {zonedTimeToUtc, utcToZonedTime, format } = require('date-fns-tz');
// Parse using location for offset
let loc = 'Canada/Saskatchewan';
let s = '2020-08-14 13:05:52';
let fIn = 'yyyy-MM-dd HH:mm:ss';
let utcDate = zonedTimeToUtc(s, loc);
// Show local equivalent
console.log(utcDate);
// Parse using string for offset
let tz = '-06';
let utcDate2 = parse(s + ' ' + tz, fIn + ' X', new Date());
// Show local equivalent, should be same as above
console.log(utcDate2);
// Format using location:
let fOut1 = 'yyyy-MM-dd HH:mm:ss XXX'; // -0600
let fOut2 = 'yyyy-MM-dd HH:mm:ss z'; // CST
let dCST = utcToZonedTime(utcDate2, loc);
console.log(format(dCST, fOut1, {timeZone: loc}));
console.log(format(dCST, fOut2, {timeZone: loc}));
Try using moment libraries to solve your time problems: moment.js, and its complement moment-timezone.js
To output the current time converted to CST timezone:
moment().tz('America/Chicago').format('hh:mm:ss z')
06:43:34 CST
moment().tz('America/Chicago').format('hh:mm:ss z Z')
06:43:35 CST -06:00
moment().tz('America/Chicago').format()
2020-08-13T15:52:09-06:00
Or maybe use a function as below:
const calcTime = (cityOffset) => {
var now = new Date();
// convert to msec and add local time zone offset and get UTC time in msec
var utc = now.getTime() + (now.getTimezoneOffset() * 60000);
// create new Date object for different city using supplied offset
var newTime = new Date(utc + (3600000 * cityOffset));
return newTime.toLocaleString();
}

Date in Javascript countdown

I am trying to make countdown for website. I have simple javascript code as below. Last line in code gives output 5 where it should give output zero as I have not assigned any hour value to it. And as a result my countdown stops 5 hours late then exact time I want it to stop.
var date1 = new Date("2019-12-09");
document.write(date1.getHours());
You can use getTimezoneOffset() and then subtract it off:
var date1 = new Date("2019-12-09");
console.log(date1.getHours() + date1.getTimezoneOffset() / 60);
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset
The problem here is that when you create the Date without specifying the timezone, it will treat it as if the hours/minutes/seconds were set to zero and then the whole date was converted to your local timezone (timezone of the browser to be specific). Depending on where you are in the world, it can be many hours off the mark. Because of that, every time you need to compare two dates (either for countown or anything else), you have to either use UTC methods or make sure you specify the timezone on both dates explicitly:
const timeZero = new Date('01 Jan 1970 00:00:00 GMT') // <<- explicit GMT timezone
const isoTimeZero = new Date('1970-01-01T00:00:00.000Z') // ISO format of the same
...
const today = new Date();
const utcDateStr = `${today.getUTCFullYear()}-${today.getUTCMonth()+1}-${today.getUTCDate()} 00:00:00.000 GMT`
const utcDate = new Date(utcDateStr)
const offsetInMillisec = utcDate - timeZero // You can calculate hours/munites left to zero as needed
This example ignores hours/minutes/seconds - they are easy to add in the same fashion as the date. The key is to always use the same timezone for both days, preferably UTC
Alternatively, you may want to consider switching to moment.js and saving yourself lots of hassle :)

How do I save date/time in some other timezone

I need to save a Date: February 16th, 2017 5PM HST.
The database (Parse) only accepts JS Date. And, my system timezone is IST.
JS Date does not have the ability to save in a different timezone.
To overcome this, I save three variables. Date (Calculated, calculation explained below), Timezone Offset, Timezone
Date is converted using moment.tz(DateObject, Timezone).
But, calling the toDate() function, seems to change it back to IST.
On further examination, I found a _d key to the Moment object, which seems to have the converted datetime in IST.
But, I seem to cannot get it to work.
Any hints would be helpful.
What do you mean by "save in a different timezone"? Timezone is a presentation-layer concern. 01:00+00:00 and 02:00-01:00 are the same time, presented differently. The point in time is represented using a large integer (the timestamp), and this timestamp is the thing you should save.
When you load this timestamp and want to use it again: you can present it from the perspective of any zone you choose.
//-- parsing the user input...
// parse HST (Honolulu Standard Time) date-time
var inputTime = moment.tz("February 16th, 2017 5PM", "MMMM Do, YYYY hA", "Pacific/Honolulu");
// in case you want to double-check that it parsed correctly
var inputTimePrettyPrinted = inputTime.format(); // "2017-02-16T17:00:00-10:00"
// grab timestamp
var timestamp = +inputTime;
//-- presenting the stored timestamp in Indian Standard Time...
// install a timezone definition for Indian Standard Time
moment.tz.add("Asia/Calcutta|HMT BURT IST IST|-5R.k -6u -5u -6u|01232|-18LFR.k 1unn.k HB0 7zX0");
moment.tz.link("Asia/Calcutta|Asia/Kolkata");
var timePresentedInIndianTime = moment(timestamp).tz("Asia/Calcutta");
var indianTimePrettyPrinted = timePresentedInIndianTime.format(); // "2017-02-17T08:30:00+05:30"
Try something like this:
var UTC = new Date();
var UTC = UTC.getTime() // Get UTC Timestamp
var IST = new Date(date); // Clone UTC Timestamp
IST.setHours(IST.getHours() + 5); // set Hours to 5 hours later
IST.setMinutes(IST.getMinutes() + 30); // set Minutes to be 30 minutes later
var EST = new Date(date); // Clone date
EST.setHours(EST.getHours() - 4); // set EST to be 4 hour earlier
You can change according to your need.
You need to use moment tz to add to HST
var now = new Date();
moment.tz.add('HST|HST|a0|0|');
console.clear();
var converted = moment(now).tz("HST").format();
console.log(now);
console.log(converted);
Here is the jsfiddle link
Check console.log for the answer.
Fri Feb 17 2017 18:24:49 GMT+0530 (India Standard Time) //IST time
2017-02-17T02:54:49-10:00 // HST Time

Categories