3

I have a Date. It is in the local timezone. I want a new Date that is at the beginning of the dayin a different timezone. Here are some things I do not want:

  • A Date in UTC equivalent to the first date converted to UTC
  • A string

Specifically, UTC does not work because getting the start of a day in UTC is not the same as getting the start of the day in a timezone.

So If I have a date in Calcutta and want to get the start of that day in San Francisco, the date in Calcutta and the date in Greenwich might not be the same date. It could be June 15th in Calcutta, June 15th in Greenwich, but June 2nd in San Francisco. So calling setMinutes(0) etc on a date that is set to UTC will not work.

I am also using date-fns (not moment) if that's helpful, but it doesn't seem to be because all dates (including those in date-fns-tz) are returned in either local or UTC time.)

Is this possible in Javascript or am I insane?

Note:

This is not the same as Convert date to another timezone in JavaScript

That is about converting to strings. I do not want strings.

Chris
  • 11,819
  • 19
  • 91
  • 145
  • You could try to convert the date in UTC to a timestamp, subtract or add the timezone difference in milliseconds and then create a new date out of the corrected timestamp. – Emiel Zuurbier Jun 03 '20 at 20:24
  • Does this answer your question? [Convert date to another timezone in JavaScript](https://stackoverflow.com/questions/10087819/convert-date-to-another-timezone-in-javascript) – Emiel Zuurbier Jun 03 '20 at 20:26
  • @EmielZuurbier that does not. It is about converting to strings and that is not what I want. – Chris Jun 03 '20 at 20:37
  • See this method, [getTimezoneOffset](https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Obiekty/Date/getTimezoneOffset) may help – Grzegorz T. Jun 03 '20 at 20:50

1 Answers1

1

One way is to:

  1. Get the current timezone offset at the required location
  2. Create a date for the required UTC date
  3. Apply the offset from #1

e.g. using the answer at Get Offset of the other Location in Javascript:

function getTimezoneOffset(date, loc) {
  let offset;
  ['en','fr'].some(lang => {
    let parts = new Intl.DateTimeFormat(lang, {
      minute: 'numeric',
      timeZone: loc,
      timeZoneName:'short'
    }).formatToParts(date);
    let tzName = parts.filter(part => part.type == 'timeZoneName' && part.value);
    if (/^(GMT|UTC)/.test(tzName[0].value)) {
      offset = tzName[0].value.replace(/GMT|UTC/,'') || '+0';
      return true;
    }
  });
  let sign = offset[0] == '\x2b'? '\x2b' : '\x2d';
  let [h, m] = offset.substring(1).split(':');
  return sign + h.padStart(2, '0') + ':' + (m || '00');
}

// Convert offset string in ±HH:mm to minutes
function offsetToMins(offset) {
  let sign = /^-/.test(offset)? -1 : 1;
  let [h, m] = offset.match(/\d\d/g);
  return sign * (h * 60 + Number(m));
}

// Format date as YYYY-MM-DD at loc
function formatYMD(loc, date) {
  let z = n => ('0'+n).slice(-2);
  let {year, month, day} = new Intl.DateTimeFormat('en',{timeZone: loc})
    .formatToParts(date)
    .reduce((acc, part) => {
      acc[part.type] = part.value;
      return part;
    }, Object.create(null));
  return `${year}-${z(month)}-${z(day)}`
}

// Return stat of day for date at loc
function startOfDayAtLoc(loc, date = new Date()) {
  let offset = getTimezoneOffset(date, loc);
  let offMins  = offsetToMins(offset);
  let d = new Date(+date);
  d.setUTCHours(0, -offMins, 0, 0);
  // If date is + or - original date, adjust
  let oDateTS = formatYMD(loc, date);
  let sodDateTS = formatYMD(loc, d);
  if (sodDateTS > oDateTS) {
    d.setUTCDate(d.getUTCDate() - 1);
  } else if (sodDateTS < oDateTS) {
    d.setUTCDate(d.getUTCDate() + 1);
  }
  return d;
}

// QnD formatter
let f = (loc, d) => d.toLocaleString('en-gb', {
  year: 'numeric',
  month: 'short',
  day: 'numeric',
  hour12:false,
  hour: '2-digit',
  minute: '2-digit',
  second: '2-digit',
  timeZone: loc,
  timeZoneName: 'long'
});

// Examples
// 1 June 2020 00:00:00 Z
let d = new Date(Date.UTC(2020, 5, 1));
['America/New_York',
 'Asia/Tokyo',
 'Pacific/Tongatapu',
 'Pacific/Rarotonga'
].forEach(loc => {
  let locD = startOfDayAtLoc(loc, d);
  console.log(loc + ' ' + getTimezoneOffset(d, loc) + 
  '\nZulu : ' + locD.toISOString() +
  '\nLocal: ' + f(loc, locD));
});

// Dates on different date to UTC date
let laDate = new Date('2022-04-30T18:00:00-07:00');
let la = 'America/Los_Angeles';
console.log(`${la} - ${f(la, laDate)}` +
`\nStart of day: ${f(la, startOfDayAtLoc(la, laDate))}`
);
let chaDate = new Date('2022-05-01T03:00:00+10:00');
let cha = 'Pacific/Chatham';
console.log(`${cha} - ${f(cha, chaDate)}` +
`\nStart of day: ${f(cha, startOfDayAtLoc(cha, chaDate))}`
);

However, I'd suggest you use a library with timezone support as there are many quirks with the Date object and there is a new Temporal object in the works.

RobG
  • 142,382
  • 31
  • 172
  • 209
  • This doesn't work. Running `startOfDayAtLoc('America/Los_Angeles', new Date('Apr 24 2022 18:00:00 pdt'))` returns `Mon Apr 25 2022 00:00:00 GMT-0700 (Pacific Daylight Time)`. And that's wrong, because the start of the day on April 24th 6pm PDT is not April 25th. – Michael Matthew Toomim Apr 24 '22 at 20:52
  • 1
    @MichaelMatthewToomim— 'Apr 24 2022 18:00:00 pdt' is not a format supported by ECMAScript so parsing is implementation dependent and your starting point is not reliable. If parsed correctly, the function returns 2022-04-25T07:00:00.000Z (i.e. the start of the day on 25 April in Los Angeles) as it's using the UTC date not the local date. – RobG Apr 25 '22 at 03:39
  • It should not return the start of the day on 25 April. It should return the start of the 24 of April. Parsing is not the problem. It does the same thing no matter how you express the time. For instance, this fails too: `startOfDayAtLoc('America/Los_Angeles', new Date('Sun Apr 24 2022 18:00:00 GMT-0700 (Pacific Daylight Time)'))` As does this: `startOfDayAtLoc('America/Los_Angeles', new Date(1650848400000))`. They all return the 25th, not the 24th. – Michael Matthew Toomim Apr 26 '22 at 04:38
  • 1
    @MichaelMatthewToomim—I've explained why it does that. It's now fixed. – RobG Apr 26 '22 at 13:06
  • I confirm the fix. Thanks @RobG! – Michael Matthew Toomim Apr 27 '22 at 16:20
  • 1
    The exact format of `.toLocaleDateString('en-CA')` is not portable and **will break in newer browsers**! It recently changed from `yyyy-MM-dd` to `M/d/yyyy` in browsers with ICU 72 (Chrome 110 and Firefox 110 beta). Do not make assumptions about specific the specific formatting of locales. – Anders Kaseorg Feb 12 '23 at 08:16
  • @AndersKaseorg—good point. I've actually argued before that the output is implementation dependent. It's supposed to follow the rules at the CLDR project, however they may change and implementations may differ on the rules they follow. Thanks for giving me an example. :-) – RobG Feb 12 '23 at 22:30
  • @AndersKaseorg—use of *toLocale\** methods replaced with *Intl.DateTimeFormat*. BTW, it seems fr-CA formats the date as en-CA did, however the time is quite different. The CLDR project has changed the format of en-CA to M/d/y. – RobG Feb 17 '23 at 00:48