When React Native Date Pickers and Daylight Savings Collide
Two of the most irritating problems for mobile development, combined.
First, some quick terminology
timestamp/epoch time: I use these phrases interchangably. They both mean “The number of milliseconds that have passed since Midnight, Jan 1, 1970, UTC”. Since they’re just a count of seconds, they’re completely timezone agnostic.
clock position: This is a term I invented, which describes an hour, minute and second (or even years, months and days), but with no timezone information. They look something like this: 12:45pm. When trying to organise international meetings, you would want to align timestamps rather than clock positions. When deciding what time to eat breakfast on holiday, you use a clock position to decide ( 8:00am seems good, and represents a different timestamp depending on if it’s looked at in London or if it’s looked at in New York)
Why are react native date picker components such a hassle to use?
When rendering a react native DatePicker component, you need to supply it with a date prop, which accepts only JS Dates. But, if you only have a timestamp rather than a date, you need to turn the timestamp into a date. This is a problem if your phone is in a different timezone to the place that generated the timezone, and you want to render it in the original timezone.
For example: Say I have a react-native app that allows me to configure the times on my automatic cat feeder at home, which talks to an API, giving and receiving epoch timestamps, which it supplies to its DatePickers. The app lets you schedule special instances of feeding, so you can set a particular time and date to feed, rather than a regular interval.
I live in Brisbane, Australia (+10:00), and at one point I schedule it to feed my cat at 5:00pm on 2019-10-24. Then, I go on holiday to Israel (+03:00). If I try to edit that instance of feeding, I would expect it to show me a form with a date picker that displays a feeding time of 5:00pm, but it will actually display 10:00am.
In order to display the correct time in the native date picker component, you actually need to shift the time such that it’s 5:00pm in Israel, allow edits, and then shift it back to the original time offset. How do we do that?
The solution — oClockShiftIntoTZ
The players:
homeTZ: The timezone at home. In the following example, it is"Australia/Brisbane"which isUTC+10:00, year-round.localTZ: The timezone where the user’s phone is. In the following example, it is"Jerusalem/Israel"which isUTC+03:00, orUTC+02:00, depending on daylight savings, which cut over on2019–10–27.timestamp: A number, unrelated to timezones, specifying the exact moment we want to put into the date pickerclockPosition: The clock position of the timestamp, when combined with thehomeTZtimezone.
We need a method that takes a timestamp and homeTZ, and returns a Javascript Date, in localTZ, with the same clockPosition.
That was a lot of words just now. An example is probably better.
By the way, as far as this example is concerned, the current date is 2019–11–05. Yes, it is a bit weird we’re trying to edit a cat feeding time in the past, but examples are hard, ok?
// In Brisbane: 2019-10-24 17:00:00.000 +10:00
// In Israel: 2019-10-24 10:00:00.000 +03:00
const timestamp = 1571900400000 const homeTZ = "Australia/Brisbane" // +10:00// The current date is 2019-11-05 (after Israel changes from +03:00 to +2:00). So our local offset is +02:00 despite the timestamp being at a time when Israel was +03:00oClockShiftIntoTZ(timestamp, homeTZ)
// We want this to return a JS Date that looks like
// Thu Oct 24 2019 17:00:00 GMT+0200 (Israel Standard Time)
So oClockShiftIntoTZ looked like this:
import { DateTime } from 'luxon';function oClockShiftIntoTZ(timestamp: number, homeTZ: string) {
return DateTime.fromMillis(timestamp)
.setZone(homeTZ)
.setZone('local', { keepLocalTime: true })
.toJSDate();
}
We use Luxon, which is a variant of the moment.js time and date library. It allows us a much finer control over our time data than built-in Javascript libraries do.
Let’s walk through it. The first line makes a luxon DateTime object from the timestamp. Since our phone happens to be in Israel right now, and the current date is 2019–11–05, this DateTime looks like this when converted to a string: “2019–10–24T10:00:00.000+03:00”.
If you’re paying attention, you might ask “But the current date is well past the daylight savings cutoff point, why is the offset +03:00 instead of +02:00?” The reason is because at the time the timestamp represents, the offset was +03:00.
The next line sets the timezone to “Australia/Brisbane” without changing the moment in time the timestamp represents. In other words, the numerical value of the previous DateTime object and this one are the same. This new DateTime looks like this: “2019–10–24T17:00:00.000+10:00”. Note the hours have changed, even though it represents the same instant.
The next line sets the timezone to the local time again “Jerusalem/Israel”. This time, however, we do change the moment in time the timestamp represents, using the keepLocalTime: true option. The new DateTime will look like this: “2019–10–24T17:00:00.000+03:00”. Note the clock position has not changed, yet the timestamp has.
Finally, we convert the DateTime to a JS Date. We now have a Date in local time that won’t get all shifted by the Native DatePicker components, right?
Hang on, what about Daylight Savings?
If you were thinking that there might be a problem because the offset that the JS Date has (+03:00) is different to the current offset (+02:00), you’re like me! The good news is that the react native DatePicker will render the times correctly, because it knows which offset to use (the one at the time the JS Date represents). We don’t have to worry about this!
oClockShiftOutOfTZ
We’re not out of the woods yet. When we submit our form, we’ll be submitting a shifted DateTime (since we have keepLocalTime: true our form field doesn’t represent the same instant as it appears to represent. It appears to represent the clockPosition displayed, in the homeTZ, but in reality it represents the clockPosition displayed in the localTZ). We now need to run our form submission through a final gate, which shifts it back.
function oClockShiftOutOfTZ(timestamp: Date, homeTZ: string) {
return DateTime.fromJSDate(time).setZone(orgTimeZone, { keepLocalTime: true })
}I hope this helps you on your fight against timezones, and clears up some mysteries about how JS renders times. Good luck!
