Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timezone support #118

Closed
dgouldin opened this issue Jun 11, 2016 · 85 comments
Closed

Timezone support #118

dgouldin opened this issue Jun 11, 2016 · 85 comments

Comments

@dgouldin
Copy link

It would be nice if there was a way to display a calendar in a timezone other than the browser's local one. moment-timezone supports timezone conversion using a named zone:

http://momentjs.com/timezone/docs/#/using-timezones/converting-to-zone/

I attempted to make this work by setting moment's default timezone, but it looks like all of the date "math" to determine the range of days and weeks is done by date-arithmetic, a library that AFAICT has no awareness of timezones.

@jquense
Copy link
Owner

jquense commented Jun 11, 2016

times ones should just be a matter of what dates you provide, the date math shouldn't need to know what tz you want to work... what bits aren't working?

@dgouldin
Copy link
Author

Date always assumes the browser's timezone. So when you take an ISO 8601 datetime string like '2016-01-01T00:00:00Z' and convert it to a Date using moment, the object you get back is no longer in the timezone originally specified. (In my case, the example string becomes Thu Dec 31 2015 16:00:00 GMT-0800 (PST).)

So now react-big-calendar is taking those UTC datetimes which javascript converted to PST and doing math like "start of day", which gives the correct answer in PST but the wrong answer in UTC.

As I said earlier, I can get halfway there by telling moment to always convert to a given timezone. That causes the times events to show up in the correct timezone, but because the time column start and end dates are computed using date-arithmetic, the column doesn't render as midnight to midnight. (In the case where my events are in UTC and my local computer is PST, it renders 4pm to 4pm.)

I think you're assuming that you can just entirely ignore the timezone and treat all datetimes as timezone-naive, but in reality, there is no such thing as a timezone-naive datetime in javascript. I'd much rather be able to explicitly state the timezone I'd like the calendar to render in than try to manually convert to local time as if the event was already in the local timezone.

@cgc
Copy link

cgc commented Jun 13, 2016

Given that fullcalendar actually explicitly deals in terms of ambiguous time, there seems to be precedent for the difficulty of integrating it explicitly into the core.

I can imagine ways to integrate it as a plugin, or at least with minimal footprint in the calendar core by doing something like wrapping props.events and wrapping the various callback functions to do conversion to and from zoned/zoneless.

a sketch of what the wrapper might look like:

const ZonedCalendar = React.createClass({
  onSelectSlot(slotInfo) {
    return this.props.onSelectSlot({
      start: forceZone(slotInfo.start, this.props.timezone),
      end: forceZone(slotInfo.end, this.props.timezone),
    });
  },
  onSelectEvent(event) {
    return this.props.onSelectEvent(event._originalEvent);
  },
  render() {
    const events = this.props.events.map(event =>
      toAmbiguousEvent(event, this.props.timezone));
    return <BigCalendar
      { ...this.props }
      onSelectSlot={ this.onSelectSlot }
      onSelectEvent={ this.onSelectEvent }
      events={ events }
    />;
  },
});


function toAmbiguousEvent(event, tz) {
  return {
    ...event,
    start: toAmbiguousDate(event.start, tz),
    end: toAmbiguousDate(event.end, tz),
    // Thinking a scheme like this is best: it means that the event object you
    // put in the event object you get out, making equality comparison later
    // on easier.
    _originalEvent: event,
  };
}

function toAmbiguousDate(dt, tz) {
  // just wrote this one, so it's a bit of a guess

  // the sign of getTimezoneOffset is opposite of moment
  const browserOffset = -1 * new Date().getTimezoneOffset();
  const result = moment(dt).tz(tz);
  // adding moment's offsets takes you back to UTC, so we add the utcOffset
  // of the input and subtract the offset of the browser to render the time
  // for the browser.
  result.add(result.utcOffset() - browserOffset, 'minutes');
  return result.toDate();
}

function forceZone(dt, tz) {
  // pulled this from our fullcalendar integration
  const result = moment(dt).tz(tz);
  result.add(result.utcOffset(), 'minutes');
  return result;
}

@s-taylor
Copy link

s-taylor commented Jun 22, 2016

I haven't looked at what methods BigCalendar uses internally from the localiser, but I was thinking it might work if you could bind moment with the timezone. Something like...

const _ = require('lodash');
const moment = require('moment-timezone');

const momentBound = _.partial(moment.tz, _, <timezone-here>)

Then pass momentBound in as the big calendar moment localizer. I'm also trying to solve this issue at the moment and going to see if this might work...

UPDATE: this doesn't work #sadface

@jquense
Copy link
Owner

jquense commented Jun 22, 2016

we are explicitly deferring to date libraries like moment to handle this sort of thing via localizers. I haven't delta with TZ issues myself but I'd be interested to know where we aren't being ambiguous about it. I'm guessing that's mostly a matter of date math creating date objects

@s-taylor
Copy link

s-taylor commented Jun 22, 2016

Let me articulate the problem a bit better. When Big Calendar renders dates it is displaying these on the calendar using your browser's timezone (which comes from their device configuration whatever that may be). If you change your timezone setting the dates in Big Calendar will move to reflect that change.

This is what I don't want to happen, I want to render dates in the calendar in a specific timezone irrespective of the users browser's timezone. The reason for this, is the particular project I'm working on, the users will have a timezone associated with their profile. When they see dates in the calendar they want it to be based on this defined timezone. This is relevant because if they take a trip overseas and their device timezone changes they don't want the dates in the calendar to change as they haven't changed their timezone preference. Does that make sense?

I've managed to hack around this by applying an offset to the start / end date of each event that is the delta between their timezone preference and the browser's timezone (though detecting this is possibly inaccurate according to moment-timezone).

@dgouldin
Copy link
Author

I've implemented a similar workaround which I'd prefer not to have to do. In my component that wraps Big Calendar, I have a couple of methods:

toLocalTimezone(dateStr) {
  // Construct a local timezone date string ignoring the source timezone
  const dateStrNoTz = moment(dateStr).tz(this.state.timezone).format('YYYY-MM-DDTHH:mm:ss');
  return moment(dateStrNoTz).format();
},

toTargetTimezone(dateStr) {
  // Construct a target timezone date string ignoring the source timezone
  const dateStrNoTz = moment(dateStr).format('YYYY-MM-DDTHH:mm:ss');
  return moment.tz(dateStrNoTz, this.state.timezone).format();
},

Dates that come out of BigCalendar (like the one passed to onNavigate) go through toTargetTimezone before being sent to the back end, and dates that come from the back end go through toLocalTimezone before being passed to Big Calendar.

@jquense
Copy link
Owner

jquense commented Jun 22, 2016

I understand the issue y'all, I just don't know why and how RBC would be to blame. it defers all rendering of dates to the localizer. What I'd like to happen is that you just configure Moment or Globalize to handle timezones and it should work, and RBC just renders whatever you give it.

the only place I think this might break is where RBC creates dates but I think those places are fairly limited and in most cases it could use the offset of the dates it already has. if someone wants to dig more into it I'm sure I'd accept a PR

@dgouldin
Copy link
Author

Ah ok, yes my main concern is the dependency on date-arithmetic. It's the only bit that can't be made timezone aware without modifying the dependency itself.

@jquense
Copy link
Owner

jquense commented Jun 22, 2016

@dgouldin I don't think it needs to be timezone aware though. all the dates should be the same TZ and it should maintain them during any Math yeah? It may need to be adjusted to do that though, thats fine as well (I own that dep anyway)

@dgouldin
Copy link
Author

The problem is as soon as you start working with native Date objects, everything is in the browser's timezone, so in order to accomplish "all dates should be in the same TZ", you'll need to actually have support for dates that aren't in the browser's timezone.

@jquense
Copy link
Owner

jquense commented Jun 22, 2016

That is only true if you don't specify an offset, we can have any date math maintain the input date's offset fairly straight forwardly I think

@s-taylor
Copy link

That is only true if you don't specify an offset, we can have any date math maintain the input date's offset fairly straight forwardly I think

But offsets are not a constant. Daylight savings affects the offset depending on the date.

@wendykwok
Copy link

wendykwok commented Oct 21, 2016

wanna circle back to this issue, I am trying to set default timezone in moment but it doesn't seem like BigCalendar honoring the moment.defaultZone value

Fwiw, when I set the system timezone to Asia/Tokyo which works like a charm but below code snippets simply doesn't work. any thoughts?

moment.tz.setDefault("Asia/Tokyo");
BigCalendar.setLocalizer(
  BigCalendar.momentLocalizer(moment)
);

While react-datepicker seems working as expected with

<DatePicker selected={moment()} />

@fxfactorial
Copy link
Contributor

@wendykwok I'm using it this way:

import moment_timezone from 'moment-timezone';
moment_timezone.tz.setDefault('Asia/Yerevan');

BigCalendar.momentLocalizer(moment_timezone);

And that works correctly for me.

@pencilcheck
Copy link

Tried both solutions from @wendykwok and @fxfactorial however none of them worked. BigCalendar still displays the event in user local timezone instead of the timezone specified in moment-timezone using setDefault

@pencilcheck
Copy link

I have a workaround by setting the date components manually into the Javascript Date()

startdate is the moment date object of the event in the target timezone.
event.startdate is the date object passed into Big Calendar

event.startdate = new Date(startdate.year(), startdate.month(), startdate.date(), startdate.hour(), startdate.minute(), 0);
event.enddate = new Date(enddate.year(), enddate.month(), enddate.date(), enddate.hour(), enddate.minute(), 0);

@jasonogasian
Copy link

I am using the moment-timezone setDefault method posted by @fxfactorial and it is working for setting the time bar but not the correct day in week or month view. Creating a new moment() object does create with the correct date, not sure why the calendar isn't using it.

@rafinskipg
Copy link

@pencilcheck I would love to see a more detailed explanation about that because I don't understand how the calendar picks startdate instead of start/end on the events.

@jtomaszewski
Copy link
Contributor

jtomaszewski commented Mar 27, 2017

@pencilcheck workaround works for me. Just not sure if it works correctly with the DST changes and so on, because it's a "fake move": we are changing the date's timezone. While 2:30am might exist in startdate's timezone, it may not exist in event.startdate's timezone (browser's timezone). Not sure what happens then

@jtomaszewski
Copy link
Contributor

jtomaszewski commented Mar 27, 2017

However... there's a small bug in week/day views when during the shown week/day there has been a DST change.

Example: My local browser's timezone is Europe/Warsaw . We have DST change on 26th march at 2am (it goes forward to 3am). I entered 4 events:

  • 25.03 2am
  • 26.03 1am
  • 26.03 2am
  • 26.03 3am

When passing events prop to BigCalendar, I'm passing native new Date() objects, which return those values:

  • Sat Mar 25 2017 02:00:00 GMT+0100 (CET)
  • Sun Mar 26 2017 01:00:00 GMT+0100 (CET)
  • Sun Mar 26 2017 03:00:00 GMT+0200 (CEST)
  • Sun Mar 26 2017 04:00:00 GMT+0200 (CEST)

(so times are given in a correct timezone)

However, this is how ReactBigCalendar renders it:

image

So, any events after 2am on 26th march are shown in a wrong row (3am event is shown in 2am row). However when I switch to a next week, everything is back to normal:

image

Day view problem (buggy placement is only on 26th after 2am):

image

Month view is okay:

image

Any ideas how to tackle it? ;)

--- edit ---

Just seen it now it's already mentioned in #334 , #313 , and #221 .

@jtomaszewski
Copy link
Contributor

jtomaszewski commented Mar 29, 2017

Another problem appeared for me, when I started to use moment.tz.setDefault()... which switches the default zone created by moment() calls. In my project I can have actually 3 different timezones at use:

  • BigCalendar's timezone - this I want to set with a timeZoneName prop
  • current user's timezone - this is currently set with moment.tz.setDefault
  • current user browser's (local) timezone - this we cannot change

Of course I could just not use moment.tz.setDefault() to avoid the problems... but seems like I like to complicate things ;>

Anyway, I made it work correctly. Clicking, dropping the events, showing the datetimes - works well, without any changes in BigCalendar's code.

Here's the code:

  1. Big Calendar setup
// remember the browser's local timezone, as it might by used later on
BigCalendar.tz = moment.tz.guess()
// format all dates in BigCalendar as they would be rendered in browser's local timezone (even if later on they won't)
const m = (...args) => moment.tz(...args, BigCalendar.tz)
m.localeData = moment.localeData

BigCalendar.setLocalizer(
  BigCalendar.momentLocalizer(m)
)

BigCalendar component wrapper:

const convertDateTimeToDate = (datetime, timeZoneName) => {
  const m = moment.tz(datetime, timeZoneName)
  return new Date(m.year(), m.month(), m.date(), m.hour(), m.minute(), 0)
}

const convertDateToDateTime = (date, timeZoneName) => {
  const dateM = moment.tz(date, BigCalendar.tz)
  const m = moment.tz({
    year: dateM.year(),
    month: dateM.month(),
    date: dateM.date(),
    hour: dateM.hour(),
    minute: dateM.minute(),
  }, BigCalendar.tz)
  return m
}

const TimeZoneAgnosticBigCalendar = ({ events, onSelectSlot, onEventDrop, timeZoneName, ...props }) => {
  const bigCalendarProps = {
      ...props,
      events: events.map(event => ({
        ...event,
        start: convertDateTimeToDate(event.start, timeZoneName),
        end: convertDateTimeToDate(event.end, timeZoneName),
      })),
      onSelectSlot: onSelectSlot && (({ start, end, slots }) => {
        onSelectSlot({
          start: convertDateToDateTime(start, timeZoneName),
          end: convertDateToDateTime(end, timeZoneName),
          slots: slots.map(date => convertDateToDateTime(date, timeZoneName)),
        })
      }),
      onEventDrop: onEventDrop && (({ event, start, end }) => {
        onEventDrop({
          event,
          start: convertDateToDateTime(start, timeZoneName),
          end: convertDateToDateTime(end, timeZoneName),
        })
      }),
  }
  return <BigCalendar {...bigCalendarProps} />
}

@IanLondon
Copy link
Contributor

This may be too obvious to be helpful, but if you have times as datetime strings with a UTC offset, eg "2017-04-26T11:55:54-04:00", they will render in the local time (eg "11:55:54 am") when formatted with Moment.js.

@peterox
Copy link
Contributor

peterox commented May 17, 2017

@jtomaszewski you are a legend. Your piece of magic worked for me.

I made a few modifications to make use of the startAccessor and endAccessor instead of mapping all events

import React, {Component} from 'react'
import PropTypes from 'prop-types'
import BigCalendar from 'react-big-calendar'
import {accessor} from 'react-big-calendar/lib/utils/accessors'
import moment from 'moment'
import 'moment-timezone'

import 'react-big-calendar/lib/css/react-big-calendar.css';

// remember the browser's local timezone, as it might by used later on
BigCalendar.tz = moment.tz.guess();
// format all dates in BigCalendar as they would be rendered in browser's local timezone (even if later on they won't)
const m = (...args) => moment.tz(...args, BigCalendar.tz);
m.localeData = moment.localeData;

BigCalendar.setLocalizer(
  BigCalendar.momentLocalizer(m)
);



export const convertDateTimeToDate = (datetime, timeZoneName) => {
  const m = moment.tz(datetime, timeZoneName);
  return new Date(m.year(), m.month(), m.date(), m.hour(), m.minute(), 0)
};

export const convertDateToDateTime = (date, timeZoneName) => {
  const dateM = moment.tz(date, BigCalendar.tz);
  return moment.tz({
    year: dateM.year(),
    month: dateM.month(),
    date: dateM.date(),
    hour: dateM.hour(),
    minute: dateM.minute(),
  }, BigCalendar.tz);
};


class TimeZoneAgnosticBigCalendar extends Component {
  static propTypes = {
    events: PropTypes.array,
    onSelectSlot: PropTypes.func,
    onEventDrop: PropTypes.func,
    timeZoneName: PropTypes.string,
    startAccessor: PropTypes.func,
    endAccessor: PropTypes.func,
  };

  static defaultProps = {
    startAccessor: 'start',
    endAccessor:'end'
  };

  startAccessor = (event) => {
    const start = accessor(event, this.props.startAccessor);
    return convertDateTimeToDate(start, this.props.timeZoneName);
  };

  endAccessor = (event) => {
    const end = accessor(event, this.props.endAccessor);
    return convertDateTimeToDate(end, this.props.timeZoneName);
  };

  render() {
    const { onSelectSlot, onEventDrop, timeZoneName, ...props } = this.props;
    const bigCalendarProps = {
      ...props,
      startAccessor: this.startAccessor,
      endAccessor: this.endAccessor,
      onSelectSlot: onSelectSlot && (({start, end, slots}) => {
        onSelectSlot({
          start: convertDateToDateTime(start, timeZoneName),
          end: convertDateToDateTime(end, timeZoneName),
          slots: slots.map(date => convertDateToDateTime(date, timeZoneName)),
        })
      }),
      onEventDrop: onEventDrop && (({event, start, end}) => {
        onEventDrop({
          event,
          start: convertDateToDateTime(start, timeZoneName),
          end: convertDateToDateTime(end, timeZoneName),
        })
      }),
    };
    return <BigCalendar {...bigCalendarProps} />
  }
}
export default TimeZoneAgnosticBigCalendar;

@dedan
Copy link

dedan commented Jul 22, 2017

@peterox @jtomaszewski Thank you two very much for your code, that saved me a few hours. However I'm a bit confused about convertDateToDateTime where you pass in timeZoneName but then use the browser's time that you'd stored in BigCalendar.tz.

Am I missing something here or is that a bug?

@jtomaszewski
Copy link
Contributor

Oh, true. AFAIK everything works okay with this code. So, that timeZoneName argument in convertDateToDateTime is just needless.

@ramkandasamy
Copy link

ramkandasamy commented Aug 1, 2017

So I'm using react-big-calendar version 0.14.4, moment version 2.18.1, moment-timezone version 0.5.13, and for me, the simple solution worked, i.e.:

import moment from 'moment'; import moment_timezone from 'moment-timezone'; moment.tz.setDefault("UTC"); BigCalendar.momentLocalizer(moment);

Just throwing that out there for any new users of this library.

EDIT: Never mind, it works in the sense that the events are in UTC time, but the 'Day' view starts at 7 AM and ends at 7 AM of the next day, which could be confusing to the user, i.e. an event at 4:00 AM GMT Friday August 4th is shown on Thursday August 3rd.

@ndevvy
Copy link

ndevvy commented Oct 31, 2017

I am trying to address the issue raised in @ramkandasamy edit - when I pass a localizer with a different time zone to BigCalendar, the events display correctly, but the Day and Week views still show the hours of the day (and organize events into day columns) based on my browser local time. Has anyone figured out a good fix for this?

@fbatista
Copy link

fbatista commented Nov 3, 2017

using the tweaks suggested by @peterox and @jtomaszewski with a user-defined timezone different than the browser's almost works, but the current time indicator on the day and week views shows in the "wrong" place.

Imho, the only way to actually solve this issue without hacking is to use moment for calculations instead of date-arithmetic, as the current .startOf(date,'day') and endOf(date,'day') functions used by the date-arithmetic don't take into account a desired timezone, resulting in 0:00 of browser timezone, that as others have mentioned will result in the times being rendered very weirdly.

@nileshtrivedi
Copy link

What's the plan for solving this issue? We may be able to help as long as the devs recommend a plan so that our PR actually gets merged.

@dlerman2
Copy link

dlerman2 commented Jan 4, 2019

@ehahn9 Thanks for that gist - very helpful! Are the referenced files available somewhere -- /client/utils/timezones, /client/utils/date?

@jquense
Copy link
Owner

jquense commented Jan 4, 2019

@nileshtrivedi There isn't a specific plan, I've outlined above a few steps I think are probably required to support TZ's, and others have contributed as well, nothing is probably sufficient tho, just first steps. If you wanna jump in we are happy to support, tho admittedly it's not an issue i've a lot of familiarity in doing correctly.

In terms of constraints tho:

  • don't require timezone data in core to support (in localizers is fine)
  • don't use one specific localization library, it needs to work generally with moment, luxon, etc.
  • it's fine if TZ support only works for localization libraries that can handle it, e.g. maybe luxon can't but moment can
  • We should try and keep an solution minimal in terms of complexity in core and byte size

@elixirdada
Copy link

@jquense
I am going to convert UTC date to CDT timezone date. How can I integrate it?

@diegoarcega
Copy link

I'm having the same problem @khoaanh2212 plus:

Errors

  • Day starts at 9:00 PM instead of 12:00 AM
  • Today is 20th and it shows 19th

** I'm using moment-timezone

image

@wootwoot1234
Copy link

@ehahn9 Thanks for that gist - very helpful! Are the referenced files available somewhere -- /client/utils/timezones, /client/utils/date?

@user1m
Copy link

user1m commented Jul 27, 2019

@jtomaszewski helped solve our problem. Use his wrapper.
#118 (comment)

@stale
Copy link

stale bot commented Sep 25, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@davidroman0O
Copy link

First, thank you @dgouldin

Then, don't forget to add :

 slotPropGetter: slotPropGetter && ((date) => {
                return slotPropGetter(
                   convertDateToDateTime(date, timeZoneName)
                )
            }),

On my case, I want to manipulate the dates more easily in full ISO.

Thx for the lib and this extended timezone support ! 😄

@stale stale bot removed the wontfix label Oct 7, 2019
@stale
Copy link

stale bot commented Dec 6, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Dec 6, 2019
@stale stale bot closed this as completed Dec 20, 2019
@hamez0r
Copy link

hamez0r commented Jan 8, 2020

@jtomaszewski helped solve our problem. Use his wrapper.
#118 (comment)

What's the version this patch works on?

@krvajal
Copy link

krvajal commented Mar 6, 2020

@diegoarcega I am having the same issue as you. The problem is that the call to
https://github.com/jquense/react-big-calendar/blob/master/src/Day.js#L22
returns the start of day in the current timezone instead of the timezone the calendar is in.

https://github.com/jquense/date-math

@itsalb3rt
Copy link

@wendykwok I'm using it this way:

import moment_timezone from 'moment-timezone';
moment_timezone.tz.setDefault('Asia/Yerevan');

BigCalendar.momentLocalizer(moment_timezone);

And that works correctly for me.

This still working! It is a very nice approach to set the timezone!

@mjieyoub
Copy link

mjieyoub commented Jul 11, 2023

For those using date-fns, this is a solution that I'm using to offset by the difference between browser time zone and desired displayed time zone.

import { differenceInHours, addHours } from "date-fns"
import { zonedTimeToUtc } from "date-fns-tz"
import { Event } from "react-big-calendar"

const mapEvents = <
    T extends { start: Date | number; end: Date | number; timeZone: string }
>(
    events: T[]
): Event[] =>
    events.map(event => {
        const [start, end] = getDatesWithOffset(
            event.start,
            event.end,
            event.timeZone
        )

        return {
            ...event,
            start,
            end,
        }
    })

const getDatesWithOffset = (
    startTime: Date | number,
    endTime: Date | number,
    timeZone: string
): [Date, Date] => {
    // Get the time zoned check in
    const zonedStart = zonedTimeToUtc(startTime, timeZone)

    // Calculate the offset (moving in opposite direction of the delta)
    const offset = differenceInHours(zonedStart, startTime) * -1

    // Adjust check in and check out by the offset
    const startWithOffset = addHours(startTime, offset)
    const endWithOffset = addHours(endTime, offset)

    return [startWithOffset, endWithOffset]
}

and then in your calendar component:

<BigCalendar
     {...}
     events={mapEvents(events)}
/>

@XpTo2k
Copy link

XpTo2k commented Oct 27, 2023

I had this issue and solved it with the following code (start and end):

new Date(someUtcDate.replace("Z", ""))

@cutterbl
Copy link
Collaborator

When needing timezone specific implementations, I would use either the moment or luxon localizers. We have examples for this in our documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests