v2 API design: Stick to existing standards

Nov 21, 2018
3 mins read
Sasha πŸ‘πŸ’¨ Koss

One of the most important changes in v2 is the new API design. We carefully refined every function to make date-fns consistent, predictable and reliable. This post is the third post on API design where I tell about our decision to stick to existing standards.

You can read the opening post here: https://blog.date-fns.org/post/date-fns-v2-goals-and-values-3q4uj0mon4lhp

date-fns v2 introduces many new features and breaking changes, and this post is a part of a series #date_fns_v2 where we describe in detail most notable ones.

v2 started with the change of the filenames naming scheme. I come from the Ruby world, so I thought it's a good idea to have file names in underscore format. JavaScript renaissance just started, so there wasn't a common standard. Initially, it was irritating to see so many JavaScript'ers using the camel case format for files. But eventually, I accepted the difference and decided to prioritize common practices over personal taste and adopted camel case as well.

// v1
const addDays = require('date-fns/add_days')

// v2
const addDays = require('date-fns/addDays')

This breaking change was a turning point that allowed us to abstract from our points of views and habits and embrace existing standards and well-established practices.

One of the most significant changes was adopting Unicode Technical Standard #35 for format and parse. It caused a lot of confusion, but I believe it's worth it. You can read about that in a dedicated post:

Another standard that caused us to revisit naming schema was ISO 8601. Starting with isWithinRange function we used the word "range" for time spans:

const isWithinRange = require('date-fns/is_within_range')
isWithinRange(
  new Date(2014, 0, 3), // the date to check
  new Date(2014, 0, 1), // start
  new Date(2014, 0, 7) // end
)
//=> true

It turned out that ISO 8601:2004 defines term "interval":

time interval: part of the time axis limited by two instants

We adopted this terminology and made interval a separate entity:

import { isWithinInterval } from 'date-fns'
isWithinInterval(new Date(2014, 0, 3), {
  start: new Date(2014, 0, 1),
  end: new Date(2014, 0, 7)
})

It also made the code easier to read!

When a string is passed to new Date(), the JavaScript engine tries to do its best parsing it. In v1 we relied on the mechanism but then quickly learned that different browsers have different parsers and it leads to bugs that hard to find.

Starting with v2, whenever a string represents a date it must be a valid ISO 8601 string overwise you'll get Invalid Date.

・・・
In the next posts, I'll continue overview of the v2 API design decisions and changes

One of the most important changes in v2 is the new API design. We carefully refined every function to make date-fns consistent, predictable and reliable. This post is the second post on API design where I tell about our decision to stick to JavaScript behavior and related changes.

You can read the previous post here: https://blog.date-fns.org/post/date-fns-v2-goals-and-values-3q4uj0mon4lhp

date-fns v2 introduces many new features and breaking changes and this post is a part of a series #date_fns_v2 where we describe in detail most notable ones.

The initial idea of the library was to create a set of helpers that will work with the native Date object. We avoided introducing functionality that already existed in the standard library and named functions as they are part of it. In v2 we decided to go further and made date-fns behave like JavaScript in every aspect.

JavaScript and its behavior is often a subject of just critique. Because of the need for backward compatibility over the years, it accumulated nuances that nowadays considered as a bad language design. Yes, I'm talking about its coercion rules, NaN, null and so on.

However, behind every strange looking behavior stands logic consistent across the language. It might be covering weak parts of JavaScript, but it's consistent and if you learn it once it starts making sense.

In v2 we made date-fns throw TypeError and RangeError in cases when standard JavaScript functions do it.

Whenever an argument is required, JavaScript throws TypeError:

window.fetch()
//=> TypeError: 1 argument required, but only 0 present.

From now all functions check if the passed number of arguments is less than the number of required arguments and throw TypeError exception if so:

import { addDays } from 'date-fns'
addDays()
//=> TypeError: 2 arguments required, but only 0 present

Whenever an argument value is not in the set or range of allowed values, JavaScript throws RangeError:

(42).toFixed(-1)
//=> RangeError: toFixed() digits argument must be between 0 and 100

From now on functions throw RangeError if optional values passed to options are not undefined or have expected values:

import { formatDistanceStrict } from 'date-fns'
formatDistanceStrict(new Date(2014, 6, 2), new Date(2015, 0, 1), {
  roundingMethod: 'qwe'
})
//=> RangeError: roundingMethod must be 'floor', 'ceil' or 'round'

Coercion.

Just like JavaScript date-fns coerce passed arguments to the expected type.

import { addDays } from 'date-fns'
addDays(new Date(1987, 1, 11), '42')
//=> Wed Mar 25 1987 00:00:00 GMT+0530 (+0530)

Despite being the most hated aspect of the language, coercion is quite straightforward and consistent, although it leads to unexpected results in combination with arithmetic operators.

Here are the rules that we use to coerce the arguments:

Here, the columns are what type we expect the argument to be and the rows what we actually supply as an argument β€” for example, in addDays the first argument will be transformed by the rules from the β€œdate” column, and the second argument by the rules from the β€œnumber” column, so addDays(1, '1') is equivalent to addDays(new Date(1), 1).

Date internally represented by a number so just like with Number, incorrect operations on dates results in Invalid Date (an invalid date is a date which time value is `NaN`):

const date = new Date()

date.setHours('nope')
//=> NaN

date
//=> Invalid Date

date-fns reflects this behavior and will return Invalid Date when you pass unconvertable values:

import { addDays } from 'date-fns/addDays'
addDays(new Date(), 'nope')
//=> Invalid Date

This was one of the trade-offs that were particularly hard to make:

On the one hand, we would expect to have an exception when an argument has a wrong value. On the other hand, the exception would force us to wrap every function call into try-catch blocks that is bad developer experience. The standard JavaScript's approach to the problem is to expect the developer to be responsible for validating the user input. In the worst case scenario, the application will print Invalid Date and keep working that wouldn't happen if we'd throw exceptions.

While writing the post, I found an inconsistency that we didn't consider. While toString called on an invalid date returns Invalid Date, toISOString as well as Intl API throws RangeError:

date.toString()
//=> 'Invalid Date'

date.toISOString()
//=> RangeError: Invalid time value

new Intl.DateTimeFormat('en-US').format(date)
//=> RangeError: Invalid time value

We incorrectly applied toString behavior to format:

import { format } from 'date-fns'

const date = new Date()
date.setHours('nope')

format(date, 'yyyy-MM-dd')
//=> 'Invalid Date'

I've opened an issue that we plan to fix before the first beta release:

・・・
In the next posts, I'll continue overview of the v2 API design decisions and changes

date-fns v2 goals and values

Nov 15, 2018
3 mins read
Sasha πŸ‘πŸ’¨ Koss

One of the most important changes in v2 is the new API design. We carefully refined every function to make date-fns consistent, predictable and reliable. In this post, I tell about goals and values that helped us to design simple API that is pleasant to use.

date-fns v2 introduces many new features and breaking changes and this post is a part of a series #date_fns_v2 where we describe in detail most notable ones.

When we just started working on date-fns, our only goal was to build a library for working with dates in functional style. For years we were adding more and more functions, often copying and adapting Moment.js API without second look on it.

After a while, we realized that our API and behavior is not as consistent as we want them to be. Arguments in functions doing a similar job were not always in the same order and had a different naming scheme. Sometimes functions were throwing exceptions where they shouldn't and weren't throwing when they should. I'm not talking about coercion logic; it was behind even our understanding.

The need for changes was obvious, but first, we had to define goals and values, that would help us make decisions in ambiguous cases.

Stick to JavaScript behavior whenever possible. We want date-fns to be an extension of the language and its standard library but not a substitute. We believe that it will ensure a long life of the library in the rapidly changing ecosystem.

Stick to existing standards. Instead of reinventing the wheel and relying on our subjective opinion we decided to look for current standards and well-established practices. That will provide the best compatibility with other languages and save us from mistakes that others made years if not decades ago.

Consistency. We want date-fns to be as predictable and easy to understand as possible. Function names, the order of arguments and the behavior must be consistent across the whole library.

Explicitness. date-fns should prefer explicitness over implicitness. Sometimes the latter helps to make code look cleaner. You know that feeling when a library does what you wanted it to do without saying a word. But more often it causes bugs that are hard to debug or even worse to notice.

Convenience. We want date-fns to be pleasant to use. We should help developers to avoid mistakes.

・・・
In the next posts, I'll elaborate every point and tell more about the related changes in the API.
Get date-fns posts to your Telegram
Subscribe

FP in date-fns v2

Oct 11, 2018
3 mins read
Sasha πŸ‘πŸ’¨ Koss

In this post, I tell about one of the most exciting features in v2: functional programming submodule. It provides alternative to code chains and enables currying. FP fans will love this!

date-fns v2 introduces many new features and breaking changes and this post is a part of a series #date_fns_v2 where we describe in detail most notable ones.

FP submodule introduces copies of regular functions which accept arguments in inverse order and curryied by default. They could be imported from date-fns/fp and used along with regular functions.

const { differenceInDays: regularDifferenceInDays } = require('date-fns')
const { differenceInDays: fpDifferenceInDays } = require('date-fns/fp')

regularDifferenceInDays(Date.now(), 0)
//=> 17815

fpDifferenceInDays(0, Date.now())
//=> 17815

fpDifferenceInDays(0)(Date.now())
//=> 17815

const daysSinceUnixEpoch = fpDifferenceInDays(0)
daysSinceUnixEpoch(Date.now())
//=> 17815

We also refined the order of arguments in the regular functions so they would work well with currying.

Each FP function comes in two variations:

1. One that accepts options as the first argument. Such functions has WithOptions added to the end of its name.

2. Another that doesn't have the options argument.

const { format, formatWithOptions } = require('date-fns/fp')
const { eo, ru } = require('date-fns/locale')

const dateFormat = 'LLLL yyyy'

const english = format(dateFormat)
const russian = formatWithOptions({ locale: ru }, dateFormat)
const esperanto = formatWithOptions({ locale: eo }, dateFormat)

english(Date.now())
//=> 'October 2018'

russian(Date.now())
//=> 'ΠΎΠΊΡ‚ΡΠ±Ρ€ΡŒ 2018'

esperanto(Date.now())
//=> 'oktobro 2018'

FP submodule was inspired by Lodash and fully compatible with it. You can use its helpers to compose date-fns functions:

import { flow } from 'lodash/fp'
import { setDate, addDays, addMonths, format } from 'date-fns/fp'

const { flow } = require('lodash/fp')
const { setDate, addDays, addMonths, format } = require('date-fns/fp')

const idealDate = format('yyyy-MM-dd')
const firstDayNextMonth = flow(
  addMonths(1),
  setDate(1)
)
const nearestSunday = date => date.getDay() && addDays(7 - date.getDay(), date)
const nextMonthSunday = flow(
  firstDayNextMonth,
  nearestSunday,
  idealDate
)

nextMonthSunday(Date.now())
//=>  '2018-11-04'

While we were working on the FP submodule we realized that functions which create an internal state (e.g. `isToday`) couldn't be safely curried and might cause bugs as in the given example:

import { distanceInWordsToNowWithOptions } from 'date-fns/fp'
const distanceInWordsToNow = distanceInWordsToNowWithOptions({ addSuffix: true })

// Days later you'll get an expected behavior:
const result = distanceInWordsToNow(Date.now())

So we've removed all functions that involve creating a date, and it made date-fns 100% pure library.

・・・
Happy coding time! Please support us at Open Collective: opencollective.com/date-fns
Join date-fns newsletter πŸ‘‰
Subscribe to the date-fns newsletter to receive community updates, news and tips about the library and JavaScript dates in general.

Unicode tokens in date-fns v2

Oct 10, 2018
2 mins read
Sasha πŸ‘πŸ’¨ Koss

In this post, I tell about how and why we switched from Moment.js formatting tokens to Unicode Technical Standard #35: https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table

date-fns v2 introduces many new features and breaking changes and this post is a part of a series #date_fns_v2 where we describe in detail most notable ones.

When we worked on the format function, we mimicked Moment.js behavior that we thought would simplify the transition to date-fns. It also didn't make sense to reinvent the wheel, so we decided to stick to its selection of tokens.

In turned out that YYYY and DD (year and day) tokens are misused in Moment.js because many other languages (Java, C#, Objective-C, etc.) use yyyy and dd for this purpose:

In v2 we decided to improve on consistency and standardization. We worked hard to make date-fns behave like ECMAScript in edge cases and opted to use existing standards over common conventions. So it made sense to follow the proposed specifications from the Unicode Consortium.

Unfortunately, this change caused a flood of issues reporting misbehaving format and parse. Developers kept using YYYY and DD and getting confusing results. To solve this problem, we had to disable confusing tokens D, DD, YY and YYYY and throw an error:

format(Date.now(), 'YYYY-MM-DD')
//=> RangeError: `options.awareOfUnicodeTokens` must be set to `true` to use `YYYY` token; see: https://git.io/fxCyr

To enable those tokens, you should set the awareOfUnicodeTokens option:

format(Date.now(), 'YY-MM-dd', { awareOfUnicodeTokens: true })
//=> '86-04-04'

I hope in the future when and if developers get used to the standard we'll remove this option.

・・・
Happy coding time! Please support us at Open Collective: opencollective.com/date-fns
Next page