I want to use UTC dates in my Node.js backend app, however, I need to be able to set time (hours and minutes) in a local/user-specified timezone.
I am looking for a solution in either pure JS or using dayjs
. I am not looking for a solution in moment
.
It seemed like using dayjs
I could solve this problem quite easily, however, I could not find a way to accomplish this.
I can use UTC timezone by using dayjs.utc()
or using dayjs.tz(someDate, 'Etc/UTC')
.
When using dayjs.utc()
, I cannot use/specify other timezones for anything, therefore I could not find a way to tell dayjs
I want to set hours/minutes in a particular (non-UTC) timezone.
When using dayjs.tz()
, I still cannot define a timezone of time I want to set to a particular date.
Example in plain JS
My locale timezone is Europe/Slovakia
(CEST = UTC 02 with DST, CET = UTC 1 without DST), however, I want this to work with any timezone.
// Expected outcome
// old: 2022-10-29T10:00:00.000Z
// new time: 10h 15m CEST
// new: 2022-10-29T08:15:00.000Z
// Plain JS
const now = new Date('2022-10-29T10:00:00.000Z')
const hours = 10
const minutes = 15
now.setHours(10)
now.setMinutes(15)
// As my default timezone is `Europe/Bratislava`, it seems to work as expected
console.log(now)
// Output: 2022-10-29T08:15:00.000Z
// However, it won't work with timezones other than my local timezone
(Nearly) a solution
I have just created the following function. I think there might be an issue in the returned date (year, month and day of month values) when it differs from the locale date (see the code comment before return
below). Can anyone fix this please?
And do you see any other issues/bugs in this function? :)
Updated: See my answer with updated code.
/**
* Set timezoned time to a date object
*
* @param {Date} arg1.date Date
* @param {Object} arg1.time Time
* @param {string} arg1.timezone Timezone of the time
* @return {Date} Date updated with timezoned time
*/
function setLocalTime({date, time, timezone} = {
date: new Date(),
time: {hour: 0, minute: 0, second: 0, millisecond: 0},
timezone: 'Europe/Bratislava'
}) {
const defaultTime = {hour: 0, minute: 0, second: 0, millisecond: 0}
const defaultTimeKeys = Object.keys(defaultTime)
// src: https://stackoverflow.com/a/44118363/3408342
if (!Intl || !Intl.DateTimeFormat().resolvedOptions().timeZone) {
throw new Error('`Intl` API is not available or it does not contain a list of timezone identifiers in this environment')
}
if (!(date instanceof Date)) {
throw Error('`date` must be a `Date` object.')
}
try {
Intl.DateTimeFormat(undefined, {timeZone: timezone})
} catch (e) {
throw Error('`timezone` must be a valid IANA timezone.')
}
if (
typeof time !== 'undefined'
&& typeof time !== 'object'
&& time instanceof Object
&& Object.keys(time).every(v => defaultTimeKeys.indexOf(v) !== -1)
) {
throw Error('`time` must be an object of `{hour: number, minute: number, second: number, millisecond: number}` format, where numbers should be valid time values.')
}
time = Object.assign({}, defaultTime, time)
const userTimezoneOffsetHours = new Intl
.DateTimeFormat('en', {timeZone: timezone, timeZoneName: 'longOffset'})
.format(date)
.match(/[\d: -] $/)?.[0]
// TODO: This might cause an issue when the `date` date in UTC is different from `date` date in user-defined timezone, e.g. `2022-10-28T22:00:00.000Z` → [UTC] `2022-10-28`, [UTC 02] `2022-10-29`.
return new Date(`${date.getUTCFullYear()}-${(date.getUTCMonth() 1).toString().padStart(2, '0')}-${date.getUTCDate().toString().padStart(2, '0')}T${time.hour.toString().padStart(2, '0')}:${time.minute.toString().padStart(2, '0')}:${time.second.toString().padStart(2, '0')}.${time.millisecond.toString().padStart(3, '0')}${userTimezoneOffsetHours}`)
}
setLocalTime()
// Output
// 2022-10-28T22:00:00.000Z
setLocalTime({date: new Date(2022, 0, 1)})
// Output
// 2021-12-30T22:00:00.000Z
setLocalTime({date: new Date(2022, 0, 1), timezone: 'America/Toronto', time: {hour: 4, minute: 45}})
// Output
// 2021-12-31T02:45:00.000Z
CodePudding user response:
In JavaScript the Date
objects usually expect time definitions in the local (browser) time and store it in UTC time. You can explicitly set a
time in the UTC timezone with the .setUTC<time part>()
methods. You can set the time hours
for the timezone GMT 2 by using the .setUTCHours()
function with an argument of hours-2
.
const d=new Date(),hours=10,minutes=40;
d.setUTCHours(hours-2);d.setUTCMinutes(minutes);
console.log("GMT/UTC:",d); // UTC time ("Z")
console.log("New York:",d.toLocaleString("en-US",{timeZone:"America/New_York"})); // local time US
console.log("Berlin:",d.toLocaleString("de-DE",{timeZone:"Europe/Berlin"})); // local time Germany (GMT 2)
The method .setUTCMinutes()
and setMinutes()
will in most cases achieve the same result. One notable exception is the time zone "Asia/Kolkata" which will apply a 30 minutes offset.
CodePudding user response:
I created the following function. It works with my tests, however, if you find an issue, I want to hear about it. ;)
/**
* Set timezoned time to a date object
*
* @param {Date} arg1.date Date
* @param {Object} arg1.time Time
* @param {string} arg1.timezone Timezone of the time
* @return {Date} Date updated with timezoned time
*/
function setLocalTime(dto = {
date: new Date(),
time: {hour: 0, millisecond: 0, minute: 0, second: 0},
timezone: 'Europe/Bratislava'
}) {
const defaultTime = {hour: 0, millisecond: 0, minute: 0, second: 0}
const defaultTimeKeys = Object.keys(defaultTime)
// src: https://stackoverflow.com/a/44118363/3408342
if (!Intl || !Intl.DateTimeFormat().resolvedOptions().timeZone) {
throw new Error('`Intl` API is not available or it does not contain a list of timezone identifiers in this environment')
}
if (!(dto.date instanceof Date)) {
throw Error('`date` must be a `Date` object.')
}
try {
Intl.DateTimeFormat(undefined, {timeZone: dto.timezone})
} catch (e) {
throw Error('`timezone` must be a valid IANA timezone.')
}
if (
typeof dto.time !== 'undefined'
&& typeof dto.time !== 'object'
&& dto.time instanceof Object
&& Object.keys(dto.time).every(v => defaultTimeKeys.indexOf(v) !== -1)
) {
throw Error('`time` must be an object of `{hour: number, minute: number, second: number, millisecond: number}` format, where numbers should be valid time values.')
}
dto.time = Object.assign({}, defaultTime, dto.time)
// Note: We need to specify a date in order to also consider DST settings.
const localisedDate = new Intl
.DateTimeFormat('en-GB', {timeZone: dto.timezone, timeZoneName: 'longOffset'})
.format(dto.date)
const userTimezoneOffsetHours = localisedDate.match(/[\d :-] $/)?.[0]
// Note: `dateParts` array contains date parts in the following order: day of month, month (1-indexed), year
const dateParts = localisedDate.split(/[,/]/).slice(0, 3)
return new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}T${dto.time.hour.toString().padStart(2, '0')}:${dto.time.minute.toString().padStart(2, '0')}:${dto.time.second.toString().padStart(2, '0')}.${dto.time.millisecond.toString().padStart(3, '0')}${userTimezoneOffsetHours}`)
}
console.log('no parameters:\n', setLocalTime())
// Output
// 2022-10-28T22:00:00.000Z
console.log('{date: new Date(2022, 0, 1)}:\n', setLocalTime({date: new Date(2022, 0, 1)}))
// Output
// 2021-12-30T22:00:00.000Z
console.log("{date: new Date(2022, 0, 1), timezone: 'America/Toronto', time: {hour: 4, minute: 45}}:\n", setLocalTime({date: new Date(2022, 0, 1), timezone: 'America/Toronto', time: {hour: 4, minute: 45}}))
// Output
// 2021-12-31T02:45:00.000Z