A JavaScript calendar date class, that can represent any calendar date from
January 1, 1 CE to December 31, 9999. Dates are internally represented as a
single Number
for very high performance and memory efficiency.
This is a tiny single-file library with no dependencies and is just 1.35 kB when minified and gzipped. It's also by far the most performant implementation I know of (see benchmarks).
For dates prior to October 1582, it assumes a back-projected ("proleptic") Gregorian calendar, as if that were always the calendar in use at the time.
A calendar date like "August 5, 2023" is a very different thing from a timestamp (an exact moment in time). But many APIs conflate the two.
Python very correctly distinguishes between them with its date
and datetime
classes. But many JavaScript APIs (including the core JavaScript language spec)
conflate these two concepts into a single Date
class, which can create a lot
of potential for bugs to creep into your application.
In my opinion, you should avoid using JavaScript's built-in Date()
type for
representing calendar dates, because it only works if you are extremely careful
to always use UTC, both when constructing the Date
and when displaying it.
e.g. on my system, new Date("2021-03-01").toString()
gives Feb 28 2021...
-
the date "March 1" has been accidentally changed to "Feb 28" through code that
looks perfectly reasonable. In this case, the Date
is correctly constructed
using UTC, but is formatted using local time. Likewise, in some timezones you
can see new Date(2021, 2, 1).toISOString().substring(0,10)
(March 1) will be
printed as "2021-02-28"
(Feb 28) through the opposite problem - the date is
constructed using local time but printed using UTC.
🌟 There is a new
Temporal.PlainDate
API in JavaScript which solves all of these problems, but it is not yet fully
implemented in all major runtimes/browsers. What's more, it's slower than this
CalendarDate
class. So if performance, memory use, or code size are a concern,
I'd recommend this CalendarDate
implementation; otherwise, use a
Temporal.PlainDate
polyfill to be more future-compatible.
The benefits of using CalendarDate
are:
- It allows you to be explicit in your API design (in your TypeScript types) about whether you're using a calendar date or a timestamp.
- It lets you avoid all kinds of subtle bugs related to timezones and daylight savings time (this is the voice of experience talking!).
- This implementation is very optimized and is typically 6-9x faster than using
the native
Date
orTemporal.PlainDate
classes. It represents each date as a singleNumber
so it is also very memory efficient. - Unlike
Date
, it is immutable, which makes your code more predictable.
Instantiation:
import { CalendarDate, D } from "@bradenmacdonald/calendar-date";
// You can construct CalendarDate instances using the included D literal helper:
const someDate = D`2023-08-15`;
// Or using CalendarDate.create()
const otherDate = CalendarDate.create(2023, 9, 27); // Sept. 27, 2023
// Or using CalendarDate.fromString()
const thirdDate = CalendarDate.fromString("2023-10-02"); // ISO 8601 format
// Or from a JavaScript Date
const convertedDate = CalendarDate.fromDate(new Date("2023-11-12"));
// Or get the current date
const today = CalendarDate.today();
Printing/conversion:
// You can print dates using ISO 8601 format:
someDate.toString(); // "2023-08-15"
// Or get various properties:
[someDate.year, someDate.month, someDate.day]; // [ 2023, 8, 15 ]
// Or convert back to a JavaScript date:
someDate.toDate(); // Date [2023-08-15T00:00:00.000Z]
// Or format using any locale you want (see FAQ):
someDate.format(myLocaleFormat); // "Aug 15, 2023", "15 авг. 2023 г.", etc.
Manipulation:
// You can add days (this returns a new instance; CalendarDates are immutable)
const nextDay = someDate.addDays(1);
// Or add months
const nextMonth = someDate.addMonths(1);
// Or add years
const nextYear = someDate.addYears(1);
For more usage details and examples, just check out the code or the test cases. It's very readable.
See the included CalendarDate.bench.ts
file for
details. You can run these benchmarks using
deno bench --unstable-temporal
.
This test uses fromString()
to construct a calendar date, then addDays()
to
construct a second date, then uses toString()
to print both dates in ISO 8601
format.
On this test, CalendarDate
is:
- 6x faster than the native
Date
object - 9x faster than the native
Temporal.PlainDate
object. - 8x faster than
calendar-date
on NPM - 18x faster than Day.js
- 25x faster than the
pollyfilled
Temporal.PlainDate
object.
This test parses 16 ISO 8601 date strings, as you might do when consuming a JSON API response.
On this test, CalendarDate
is:
- 1.25x faster than the native
Date
object - 4x faster than the native
Temporal.PlainDate
object. - 6x faster than
calendar-date
on NPM - 6x faster than Day.js
- 13x faster than the
pollyfilled
Temporal.PlainDate
object.
This test starts with a January 1 date then iterates through every date in the year, converting each date to an ISO 8601 string.
On this test, CalendarDate
is:
- 9x faster than the native
Date
object - 18x faster than the native
Temporal.PlainDate
object. - 20x faster than
calendar-date
on NPM - 35x faster than Day.js
- 52x faster than the
pollyfilled
Temporal.PlainDate
object.
A: Using fullYearsSince()
.
const today = CalendarDate.today();
today.fullYearsSince(birthDate); // This will print the person's age in years
A: First, declare a formatter that specifies the user's locale and the "style"
of date that you want to use (e.g. medium
). Then use CalendarDate
's
.format()
method.
const format = new Intl.DateTimeFormat("en", {
timeZone: "UTC", // Using UTC is required
dateStyle: "medium", // "full" | "long" | "medium" | "short"
});
const dateValue = D`2023-08-15`; // a CalendarDate
dateValue.format(format);
// "Aug 15, 2023"
To help you avoid bugs, the .format()
method will throw an error if your
DateTimeFormat
is not using UTC timezone.
In a React application, you can achieve the same effect using
react-intl
's <FormattedDate>
.
A: You'll have to use the formatter yourself to format a range:
const formatter = new Intl.DateTimeFormat("en", {
timeZone: "UTC", // Using UTC is required
dateStyle: "long", // "full" | "long" | "medium" | "short"
});
const start = D`2023-07-13`; // a CalendarDate
const end = D`2023-07-27`; // a CalendarDate
formatter.formatRange(start.toDate(), end.toDate());
// "July 13 – 27, 2023"
In a React application, you can achieve the same effect using
react-intl
's <FormattedDateTimeRange>
.
This is based on my own PDate and later VDate code. If you happen to need Neo4j compatibility, check out VDate instead.
MIT