Patrick Mao
Patrick Mao

Reputation: 1193

date-fns | How do I format to UTC

Problem

It looks like when I use the format() function, it automatically convert the original UTC time into my timezone (UTC+8). I have been digging through their docs for hours and couldn't seem to find a way to default it to UTC time.

import { parseISO, format } from "date-fns";

const time = "2019-10-25T08:10:00Z";

const parsedTime = parseISO(time);
console.log(parsedTime); // 2019-10-25T08:10:00.000Z

const formattedTime = format(parsedTime, "yyyy-MM-dd kk:mm:ss");
console.log(formattedTime); // 2019-10-25 16:10:00 <-- 8 HOURS OFF!!

I have tried to use the package data-fns-tz and use something like

format(parsedTime, "yyyy-MM-dd kk:mm:ss", {timeZone: "UTC"});

still no luck.

Please help!

Expected Output

2019-10-25 08:10:00

Actual Output

2019-10-25 16:10:00

Upvotes: 112

Views: 182854

Answers (12)

Beni Cherniavsky-Paskin
Beni Cherniavsky-Paskin

Reputation: 10039

UPDATE: date-fns v4 finally has builtin TZ support 🎉

I haven't tried it.

UPDATE: For date-fns v3, use date-fns-tz's formatInTimeZone()

A formatInTimeZone() helper has been added several years ago, but some bugs were fixed recently, best use at least v3.1.2.

<script type="importmap">{ "imports": { "date-fns": "https://esm.run/[email protected]", "date-fns/": "https://esm.run/[email protected]/", "date-fns-tz": "https://esm.run/[email protected]" } }</script>
<script type="module">

import { parseISO } from 'date-fns';
import { enGB } from 'date-fns/locale/en-GB';
import { fr   } from 'date-fns/locale/fr';
import { formatInTimeZone } from 'date-fns-tz';

const parsedTime = parseISO("2019-10-25T07:10:00Z");
console.log('parsedTime:', parsedTime);


const formattedTime = formatInTimeZone(
  parsedTime, "UTC", "yyyy-MM-dd kk:mm:ss zzzz", 
  // IF using 'zzzz', can optionally pass locale for better TZ names
  { locale: fr });
console.log('formattedTime:', formattedTime); // 2019-10-25 07:10:00 temps universel coordonné
</script>

The implementation is quite similar to the following (but now it also compensates for shifted time falling on wrong side of DST — not an issue for UTC but can be for many TZs):

Previous "official hack"

You were almost there, but format() only uses timeZone to show TZ name; you also need to shift the time correspondingly. Here is the combo date-fns-tz used to recommend:

<script type="importmap">{ "imports": { "date-fns": "https://esm.run/[email protected]", "date-fns/": "https://esm.run/[email protected]/", "date-fns-tz": "https://esm.run/[email protected]" } }</script>
<script type="module">

import { parseISO } from 'date-fns';
import { enGB } from 'date-fns/locale/en-GB';
import { fr   } from 'date-fns/locale/fr';
import { format, toZonedTime } from 'date-fns-tz';

const time = "2019-10-25T08:10:00+01:00"; // = 07:10Z

const parsedTime = parseISO("2019-10-25T07:10:00Z");
console.log('parsedTime:', parsedTime);


const formatInTimeZone = (date, timeZone, fmt, locale) =>
  // called `toZonedTime` since 3.0.0, was `utcToZonedTime` before.
  format(toZonedTime(date, timeZone), 
         fmt, 
         // timeZone required! locale is optional, helps 'zzzz' names.
         { timeZone, locale });

const formattedTime = formatInTimeZone(parsedTime, "UTC", "yyyy-MM-dd kk:mm:ss zzzz", fr);
console.log('formattedTime:', formattedTime); // 2019-10-25 07:10:00 temps universel coordonné
</script>

Behind the scenes

The date-fns[-tz] libraries stick to the built-in Date data type that carries no TZ info.
Some functions treat it as a moment-in-time, but some like format treat it more like a struct of calendaric components — year 2019, ..., day 25, hour 08, ....

Now the trouble is a Date is internally only a moment in time. Its methods provide a mapping to/from calendaric components in local time zone.

So to represent a different time zone, date-fns-tz/utcToZonedTime temporarily produces Date instances which represent the wrong moment in time — just to get its calendaric components in local time to be what we want!

And the date-fns-tz/format function's timeZone input affects only the template chars that print the time zone (XX..X, xx..x, zz..z, OO..O).

See https://github.com/marnusw/date-fns-tz/issues/36 for some discussion of this "shifting" technique (and of real use cases that motivated them)...
It's a bit low-level & risky, but the specific way I composed them above — formatInTimeZone() — is I believe a safe recipe? UPDATE: except TZ name could be wrong around DST boundary 🐞!

Upvotes: 67

savkevip
savkevip

Reputation: 53

Here is my solution for it: (It is similar to the first one just timeZone is different and I posted it here just to be more visible than in the comment/thread)

import { format, utcToZonedTime } from 'date-fns-tz';

const FORMAT_DATE_AND_TIME = 'dd/MM/yyyy HH:mm';

const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; // I had an issue here I set 'UTC' but I needed to get the time zone from the device/browser

export const formatDateAndTime = (date: string) => {
  return format(utcToZonedTime(date, timeZone), FORMAT_DATE_AND_TIME, {
    timeZone,
  });
};

Upvotes: 0

tilo
tilo

Reputation: 14169

If you only care about UTC, you can use @date-fns/utc:

import { UTCDateMini } from "@date-fns/utc";
import { format } from 'date-fns';

const given = "2019-10-25T08:10:00Z";
const format = "yyyy-MM-dd kk:mm:ss";

const utcDate = new UTCDateMini(given);
const formatted = format(utcDate, format); // 2019-10-25 08:10:00

Upvotes: 3

Navid Mitchell
Navid Mitchell

Reputation: 1445

This works using only the date-fns provided functionality.

import { format } from 'date-fns';
import { utcToZonedTime } from 'date-fns-tz';

const date = new Date(); // This could be any date

// Convert the local date to UTC
const utcDate = utcToZonedTime(date, 'Etc/UTC');

// Format the UTC date
const formattedDate = format(utcDate, 'yyyy-MM-dd HH:mm:ss');

console.log(formattedDate);

Upvotes: 2

solution for timestamp

format(utcToZonedTime(timestamp, 'UTC'), 'MM/dd/yyyy hh:mm a', { timeZone: 'UTC' })

Upvotes: 1

Christoph H&#228;ckel
Christoph H&#228;ckel

Reputation: 476

I would suggest using the built-in Date util:

const date = new Date("2019-10-25T08:10:00Z");
const isoDate = date.toISOString();

console.log(`${isoDate.substring(0, 10)} ${isoDate.substring(11, 19)}`);

Outputs:

2019-10-25 08:10:00

Not a general solution for any format, but no external libraries required.

Upvotes: 42

Jaeho Lee
Jaeho Lee

Reputation: 523

I guess

To construct the date as UTC before parsing would be helpful.

import { parseISO, format } from "date-fns";

const time = "2019-10-25T08:10:00Z";

const parsedTime = parseISO(new Date(Date.UTC(time)));
const formattedTime = format(parsedTime, "yyyy-MM-dd kk:mm:ss");

like this.

Upvotes: -3

2JN
2JN

Reputation: 653

Here is how I did it

const now = new Date()
  const date = format(
    new Date(now.toISOString().slice(0, -1)),
    'yyyy-MM-dd HH:mm:ss'
  )

I just removed the Z from the ISO string. I'm not sure if it solves this issue though

Upvotes: 3

Lokii
Lokii

Reputation: 482

I did something like this using date/fns and native date methods

import format from 'date-fns/format';
import parseISO from 'date-fns/parseISO';

export const adjustForUTCOffset = date => {
  return new Date(
    date.getUTCFullYear(),
    date.getUTCMonth(),
    date.getUTCDate(),
    date.getUTCHours(),
    date.getUTCMinutes(),
    date.getUTCSeconds(),
  );
};

const formatDate = (dateString) = > {
    const date = parseISO(dateString);
    const dateWithOffset = adjustForUTCOffset(date)
    return format(dateWithOffset, 'LLL dd, yyyy HH:mm')
}

Upvotes: 10

rodrigoum
rodrigoum

Reputation: 185

try

const formatDate = new Date().toISOString().substr(0, 19).replace('T', ' ');

Upvotes: -3

Sherwin F
Sherwin F

Reputation: 763

Note
The following solution will not work for all time zones, so if timezone accuracy is critical for your application you might want to try something like the answer from Beni. See this link for more info

I had the exact same question today and did some research to see if anyone has come up with anything better since this question was posed. I came across this solution which fit my needs and stylistic preference:

import { format, addMinutes } from 'date-fns';

function formatDate(date) {
  return format(addMinutes(date, date.getTimezoneOffset()), 'yyyy-MM-dd HH:mm:ss');
}

Explanation

getTimezoneOffset returns the number of minutes needed to convert that date to UTC. In PST (-0800 hours) it would return 480 whereas for somebody on CST (+0800 hours) it would return -480.

Upvotes: 35

Darkisa
Darkisa

Reputation: 2037

I had the same problem. What I do is remove the timezone from the ISO string and then use that time with date-fns:

let time = "2019-10-25T08:10:00Z".slice(0, -1)

The above is a time with no time zone, and because there is no timezone date-fns assumes the local timezone, so when you do:

format(parseISO(time), 'h:mm a')

you get: 8:10 AM, or whatever format you prefer. You just have to be careful with the string that you are slicing. If its always the same format then it should work.

Upvotes: 13

Related Questions