Home > front end >  Is there a way to set hours/minutes in local timezone, while using UTC dates only?
Is there a way to set hours/minutes in local timezone, while using UTC dates only?

Time:10-30

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

  • Related