JavaScript's Date
object can behave in non-intuitive ways when handling daylight saving time (DST) transitions.
For example, see the following scenario in the Central Standard Time (CST) zone of the USA.
const date = new Date('2021-03-14T03:00:00.000')
date.setMilliseconds(-1) // results 2021-03-14 03:59:59.999 CST
On March 14, 2021, daylight saving time begins. The time jumps directly from 2021-03-14 01:59:59 CST
to 2021-03-14 03:00:00 CST
.
In this example, subtracting 1 millisecond from 2021-03-14 03:00:00.000 CST
results in 2021-03-14 03:59:59.999 CST
. This appears to be a simple subtraction of 1 millisecond, but it actually advances the time by 1 hour.
This behavior is not a bug but a result of strictly following the ECMAScript specification (https://262.ecma-international.org/11.0/#sec-local-time-zone-adjustment).
Local time to UTC time
A Date
object created from a duplicated time during daylight saving time transition always refers to the time before DST ends. In other words, there is no simple way to obtain a Date
object that refers to the UTC time after the end of DST from a duplicated time.
Let's consider a method to obtain the correct UTC time by specifying a local time string that represents a duplicated time during the daylight saving time transition and indicating whether it is before or after the end of DST.
function utc(localtime, after) {
const result = new Date(localtime)
if (after) {
const clock = date => date.getUTCHours() * 60 + date.getUTCMinutes()
const nextday = new Date(localtime)
nextday.setDate(localtime.getDate() + 1)
const adjust = clock(nextday) - clock(localtime)
if (adjust > 0) {
const advanced = new Date(localtime).setMinutes(localtime.getMinutes() + adjust)
const advancedUTC = new Date(localtime).setUTCMinutes(localtime.getUTCMinutes() + adjust)
if (advanced !== advancedUTC) {
result.setUTCMinutes(localtime.getUTCMinutes() + adjust)
}
}
}
return result
}
By taking the difference in UTC hours and minutes between the same time on the next day in local time and the original time, you can find the time adjusted for daylight saving time within 24 hours (the overlapping period, usually 1 hour).
If you advance the original time by the overlapping period in both local time and UTC, and there is a difference, you can determine that the original time is the overlapping time when daylight saving time ends.
Handling daylight saving time in web systems
When handling time in a globally accessible web application, daylight saving time must be considered.
- The server does not know the time zone of the user or the client environment (OS).
- The user's time zone is generally the same as the client environment (OS) time zone.
This is the typical situation.
For example, if a user usually resides in Japan but temporarily uses the application in the United States, they may still want the application to display time in Japanese time. However, if they adjust their OS time zone to match the local time, the client environment’s time zone will differ from the user’s intended time zone, making the above functions ineffective.
Additionally, there is another crucial hidden assumption:
- The time zone database of the client environment (OS) is properly maintained.
The implementation period of daylight saving time in Brazil changes every year, and time zones themselves are determined by laws that are frequently revised. This means the time zone database must be regularly updated.
If the application is used in a closed or unmanaged environment where such updates cannot be performed due to special circumstances, the approach mentioned above will be insufficient.
Libraries
These issues may also be lurking in date-time libraries such as moment.js, date-fns, Day.js, and luxon.
Localization can be done with the ECMAScript® Internationalization API.
Moment.js
A widely used library that was the de-facto standard. It went into maintenance mode in 2020.
It has a fundamental problem with mutable objects, making it prone to bugs. The later date-time libraries introduced below are all designed to be immutable.
Luxon
An immutable and rich library created by the maintainers of Moment.js. Sophisticated and feature-rich. Good codebase to explore.
By default, it handles time in local time and cannot strictly handle ambiguous times.
It differs from other libraries in that the documentation clearly shows how it behaves with ambiguous time.
Day.js
A Moment.js compatible library with a minimum size of 2KB, which has many GitHub stars and is becoming the de-facto standard. The code readability is not high.
The codebase is large due to time zone and locale support (178 source files as of 2021-11-02), but the effective size can be reduced if tree-shaking is available.
Requires importing plugins each time because most functions are provided as plugins.
Planning a major version upgrade while solving many issues.
date-fns
Provides over 200 pure functions for manipulating JavaScript Date
objects, implemented in TypeScript and tree-shaking enabled.
Since the JavaScript Date
object takes the lead, problems such as mutability and month starting at 0 are inherited.
The ECMA TC39 Temporal Proposal
An ECMAScript® API proposal that may become a future standard. The specification is rigorous, spectacular, and inspired by java.time.
Qrono
Qrono provides type-safe, immutable and chainable functions necessary for most cases. This library addresses the aforementioned issue.
qrono('2021-08-31 12:34').plus({ month: 1 }).isSame(qrono('2021-09-30 12:34'))
qrono('2021-08-31 12:34') < qrono('2021-09-30 12:34')
qrono({ localtime: true }, '2021-08-31 12:34').toString() === '2021-08-31T12:34.000-04:00'
qrono('2000-01-01 01:00:00.000') - qrono('2000-01-01') // => 3,600,000 milliseconds = 1 hour
qrono('2000-01-01 01:00:00.000') < qrono('2000-01-01') // => false
qrono('2000-01-01').plus(7200000).minus(3600000) // => 2000-01-01T01:00:00.000Z
// In operations using Object, `year`, `month`, and `day` are calculated literally.
// For example, adding one month at the end of a month results in the end of the following month.
// `hour`, `minute`, `second`, and `millisecond` are treated as a duration in calculations.
qrono('2000-01-01').minus({ hour: 1, minute: 30 }) // => 1999-12-31T22:30:00.000Z
qrono('2020-02-29').plus({ year: 1 }) // => 2021-02-28T00:00:00.000Z
qrono('2021-12-31').minus({ month: 1 }) // => 2021-11-30T00:00:00.000Z
const today = qrono()
const yesterday = today.minus({ day: 1 })
const tomorrow = today.plus({ day: 1 })
today.isBetween(yesterday, tomorrow) // => true
Conclusion
Handling time and daylight saving time (DST) in JavaScript can be complex due to the quirks of the Date
object and differences in time zone transitions. The issues outlined in this article highlight the importance of understanding how JavaScript interprets and manipulates time, particularly in edge cases such as DST changes.
To mitigate these challenges:
- Be aware that the
Date
object strictly follows the ECMAScript specification, which can lead to unexpected results when working with DST transitions. - When dealing with local and UTC conversions, ensure that you correctly handle duplicated or skipped times during DST changes.
- Consider using well-maintained date-time libraries like Luxon, date-fns, or Qrono, depending on your needs.
- Regularly update the time zone database in managed environments to reflect the latest changes in global time zone policies.
- In unmanaged or offline environments where updates are not possible, carefully evaluate how time zones are handled to avoid inconsistencies.
Among these libraries, Qrono stands out as a simple yet powerful solution that resolves many of the challenges associated with JavaScript date-time handling. Unlike traditional libraries that attempt to manage multiple time zones and require careful handling of DST transitions, Qrono takes a different approach by focusing on local time operations in a type-safe and immutable manner. This eliminates many common pitfalls while providing a more predictable and reliable way to work with dates.
Ultimately, the best approach depends on the specific requirements of your application. Whether you use built-in JavaScript Date
functions or external libraries, a clear understanding of how time zones and DST affect date-time operations is essential for building reliable web applications.
Top comments (0)