I won't lie to you dealing with date and time is one of the most tricky areas that humans have to deal with, in programming, this is not different. If your app is working with events that belong to users in different points of the world you need to add timezones and maybe recurrence to save events that might happen more than one time, in this article we are going to cover some approaches to deal with that kind of applications:
- How to store dates in DB.
- How to handle recurrence.
- Where to convert times to users' local time.
- Libraries that can help with those tasks.
let's get into it.
How to store dates in DB
The most common approach to save dates in the database when you have users in different locations is saving time in UTC (Coordinated Universal Time) which is the principal time standard for clocks and time regulation, but this is not always the best solution you have to check your specific use case; for instance:
- From where users are saving dates?
- All users need to save dates or just one admin?
- Where the events are happening?
For example, recently I had to do a TV schedule plugin for a church in my country, given that the events are happening just in one place saving date-time in UTC would be an overengineer because is not really needed, so I saved it in church local timezone.
But on the other hand, in my work I had a case where users can save and edit events across the world, in this case, was more convenient to save in UTC
How to manage date recurrence
When I first face a problem in web development I always look for apps that I use because they give me the user experience, the interface, and sometimes the API if the app is ready to integrate with third-party apps. so I immediately opened my browser and looked for Google Calendar.
They have a pretty straightforward interface to save recurrence and they mentioned RRule in their API Documentation. RRule is a standard to deal with recurrence and there are several implementations in most programming languages, in javascript rrule.js is the answer.
Here is an example code to have an event every week until September 30, 2021
// To create the rrule
const rule = new RRule({
freq: RRule.WEEKLY,
dtstart: new Date(Date.UTC(2021, 8, 18, 8, 17, 0)),
until: new Date(Date.UTC(2021, 8, 30, 8, 17, 0)),
count: 30,
interval: 1
});
// to get the RRule in string
rule.toString();
// DTSTART:20210918T081700Z
// RRULE:FREQ=WEEKLY;UNTIL=20210930T081700Z;COUNT=30;INTERVAL=1;WKST=MO
// to get the ocurrence
rule.all();
you can save the RRule string in a field in the database. but I think is better to save every property of the RRule as an individual field (frequency
, interval
, etc) to query the events from the database better.
Where to convert times to users' local time?
Time conversion is a visual aspect and even if you are serving an API to mobile and web apps is better to free your backend code from those conversions and let the frontend handle them. You can detect the user's local timezone directly from the web browser using the Intl
API.
Intl.DateTimeFormat().resolvedOptions().timeZone
It has a very acceptable browser support and you can read more about it in MDN.
Another option would be asking the user to specify their time zone with the current timezone pre-selected.
To convert from UTC or the timezone you saved in the database to the user's timezone once we get it, we have some good options in javascript: luxon, date-fns it is also recommended to wrap the functionality from those libraries in a central place in case you need to change for whatever reason it would be easier to test and to move around if you face a similar situation in another application.
To illustrate here is an example of the wrapper I did to manage the timezone conversions to give you an idea:
import { DateTime } from "luxon";
export const ISO_TIME_FORMAT = "HH:mm";
export function useTime(zone, serverTimezone = "UTC") {
const timeZone = zone;
...
/**
* Transform a JS Date in users' timezone to ISO date in UTC
* @param {Date} date
* @returns {Object}
*/
const getIsoUtcDateTime = (date) => { ... };
/**
* Transform DB date and time in ISO to a JS Date in users' timezone
* @param {String} isoDate
* @param {String} isoTime
* @returns {Object}
*/
const getLocalDateTimeFromISO = (isoDate, isoTime) => { ... };
return {
...
getIsoUtcDateTime,
getLocalDateTimeFromISO
}
I'll omit implementation details because I just want to show you a general aspect of the benefit a wrapper would bring. Here a main function useTime
is defined to that take user and database timezone just once and the functions it returns will use those timezones to do the conversions.
To use our wrapper, assuming date and time are saved as ISO strings "yyyy-MM-dd"
and "HH:mm"
format we can proceed as follows:
import { useTime } from "./useTime";
import constants from "./constants";
const { getLocalDateTimeFromISO } = useTime(user.timezone, constants.SERVER_TIMEZONE);
// ... code to fetch events would go here
// transform iso dates to users' timezone
const eventsInLocal = events.map((event) => {
const { date, time } = getLocalDateTimeFromISO(event.date, event.time);
event.date = date;
event.time = time;
return event;
}
Testing
To test the behavior in development if we are taking the timezone from the browser you can simulate a different timezone in the browser in the inspector by clicking the three dots at the end of the top bar in the inspector > more tools > sensors.
This will open a section at the bottom of the browser inspector with an option to override the current location and by extension the timezone:
Now we have our browser timezone in Asia/Tokio
and new Date()
will behave as we were in Tokyo (Arigato)
Conclusion
Every time we overcome a hard challenge and we overcome we level up our skillset if I could give a number to the points dealing with dates sum to your skills I do not imagine one, but it would be a high number ๐. Thankfully we have people that paved the way to give us standards like UTC and RRule
Thanks for reading, I hope the article can save you some time if you have any questions the comments are open, or if you like Twitter as well as my Github where I do some experiments and projects.
Have a good day.