npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

typescript-calendar-date

v1.3.4

Published

![logo](logo.png)

Downloads

562

Readme

logo

AppVeyor License npm bundle size npm

Zero-depenency, small, immutable library favouring excplicitness, for handling calendar dates correctly.

typescript-calendar-date works on objects like { year: 2021, month: 'jan', day: 1 } which you can easily construct and destructure anywhere in your app. The library provides a small set of powerful functions which are simple to understand and use. It gives you the exact level of control you feel you need when working with dates (no more guessing and feeling of uncertainty about off by one mistakes), and handles all the complexity you don't care about. No more date bugs!

Installation: npm i typescript-calendar-date

Philosophy

Everyone gets dates wrong, especially programmers. If asked to calculate how many days old he is, a programmer would convert the current date and time, say Date.now(), to the number of seconds since Unix epoch, 1970, or just epoch time, for short. Then he would convert the middle of the day, or even midnight, in some arbitrary timezone, of when he was born, say April 1, 1990, to a number representing the number of seconds he was born after epoch. Then he would subtract these two numbers, and finally, divide by some constant representing the number of seconds in a day.

This is insane. Just count the number of days! Calendars were invented by humans to be useable by humans, so lets program them directly. It's fairly easy, the most complicated part of our modern calendar, the Gregorian Calendar, is leap years: every fourth, but not every hundreth, except every four-hundreth. Luckily this library encapsulates all that complexity for you!

Things you can do with this library therefore relate to calendar dates, not absolute time. Luckily, this is usually what most business and everyday applications require. You can for instance count how many days old you are, or you can find out how many days are in the current month. You can also find out if two dates are sequential, if a certain date is "in range" of two other dates, and you can generate a list of all dates between two specific dates, for instance if you want to iterate through it.

What this library does not do is handle time of day or timezones. This is largely actually irrelevant anyway, but if this is what you want to do, I would still consider if a Calendar Date and separately storing the time and location, might be a good idea. Unsolicited advice: if what you actually want is epoch timestamps (for instance in some kind of bidding or auction application), just store the epoch timestamp as an integer, and stop worrying about dates! It'll make your life a whole lot easier. And probably your code more correct.

Another idea behind this library is to be lightweight and transparent, as well as explicit, which leads me to the inevitable conclusion that the primary data type has to be transparent; that is it is just a regular javascript value with the keys year, month and day. This also means I expect you to parse your own data, for instance ISO 8601 date strings, and construct this object yourself. I provide no constructors. The reasoning is that this gives you explicit visibilty into how it works and you can be totally confident that it does the right thing. Sadly it does mean a bit more work to use the library, but I belive this tradeoff is worth it. It's just a matter of writing a single functions you can put in a utils file or whatever. Secondly, the same goes for formatting. This part is even more important that you are in control of I believe, as no one knows how you want to display the data to your users better than you. And date formats are notoriously difficult to internationalize, and differ even between contexts. In logs it makes sense to have dates of the form 2020-03-12, but users might want to see March 12, March 12, 20200, or even 12th of March. So I give you this responsibility, but in turn I try to make the interface, that is the shape and structure, of the data as lightweight and predictable as possible.

Examples

So say you did want to perform the calculation of how many days old you are.

const birthDate: CalendarDate = { year: 1990, month: 'apr', day: 1 };

const today = {
    year: new Date().getFullYear(),
    month: monthName(new Date().getMonth() + 1),
    day: new Date().getDate(),
};

const daysOld = numberOfDaysBetween({ start: birthDate, end: today });

Here we see the first concession I had to make in terms of useability. I've decided to represent the month as a three-letter string, using the English abbreviation. It would have been a lot more convenient for sure to just use an int, but that runs into the question of zero-indexing or not. Although the right answer in this context is probably to one index, that is January is the first (1) month, but there is no way around developer confusion here. I did want to end up in a situation where you always had to second guess yourself and double check the documentation. So here I've gone with a string for explicitness, which unfortunately does mean you sometimes have to call monthName to get the string representation - but hopefully you only need to do this in parsing functions at the edge of your program. The other benefits of doing it this way is that it's super easy to debug and look at your data - no doubt that when you see the value { year: 2022, month: 'aug', day: 28 } in your console, it means August 28th, 2022. This also discourages manipulating the month directly, there is now no easy way to attempt to increment it without using the library-provided functions. You can of course still do this with the day, but please don't - it'll just lead to bugs. However you can do this with the year part as there are no edge cases to incrementing or decrementing years diretly. And lastly it makes it obvious that you need to explicitly format the data for display purposes, at the same time keeping it simple to write such a function (for instance, 'aug' becomes 'August' or 8 or whatever you want, but it forces you to make a decision).

It's always tricky to know which way around these kind of "subtraction" operations work, so I've decided to go with named parameters in numberOfDaysBetween, hopefully making it clear which goes where. If you get it backwards, you get a flipped sign.

Another gothca is you have to explcititly annotate birthDate with the type CalendarDate, otherwise TypeScript infers too wide a type for month, namely string, which won't work. An alternative design here is to use an explicit enum for the month type (or an int as discussed earlier). Please let me know if this string business gets to annoying, and you'd like another approach - I'm very open to input here.

Alright, a simpler example! You've been given two dates, and you want to know if one becomes before the other. In other words, you wish to know if they are in the correct order.

const foo = (from: CalendarDate, to: CalendarDate) => {
    if (!areInOrder(from, to)) {
        throw ...
    }
    ...
};

Here too I hope disambiguate the order of the parameters by calling the function isInOrder, in an attempt to make it obvious that two dates are in order if the first appears before the second. But to make this api even more useful, you can actually pass in more parameters! An often useful thing to know if whether some third date is between the other two (is this thing in that range?). Let me give you an example.

Say your billing department wants to do a different thing if a given date is in the fiscal year's first quarter.

const foo = (date: CalendarDate) => {
    const startOfQ1: CalendarDate = { year: 2021, month: 'jan', day: 1 };
    const endOfQ1 = lastDateInMonth({ year: 2021, month: 'mar' });

    if (areInOrder(startOfQ1, date, endOfQ1)) {
        ...
    } else {
        ...
    }
};

My opinion is this code does exactly what you intuitively think it should do. You have two values representing the start of and the end of the quarter, respectively - then you test if your date is between those dates - inclusively, of course. For convenience, areInOrder takes an arbitrary number of dates, so you can express some pretty complex relationships using just one or a few function calls.

If you're wondering about lastDateInMonth, you could just create { year: 2021, month: 'mar', day: 31 } directly in the same way you construct the first day of the year (startOfQ1), but I think it's cleaner and safer to just use lastDateInMonth always, both because it's very explicit of what you want, and you don't risk mis-remembering which months have how many days - and of course it also handles leap years correctly. I don't provide a firstDateInMonth, although you are welcome to create one for yourself. The reasoning for this is that I want to be excplicit about showing you where the complexity in this domain (calendar dates) lies - it's relatively much more tricky to express the idea of the last day in a month, than the first. There is a certain tempting symmetry of providing both firstDateInMonth and lastDateInMonth, but this would be a kind of "api lie", exactly because this pleasing symmetry is false.

By now we've covered most of the complexity, the rest should be pretty straight forward. Adding or subtracting a number of days is as simple as:

addDays(myDate, 60);

although, a lot of the time you might want to add a whole number of months, in which case you would call

addMonths(myDate, 2);

But aah, I lied. Here comes some more complexity. But it is essential complexity, I promise! What should the result be if you add a whole number of months to April 15th? Pretty obviously June 15th, but there is 61 days between these two dates. And even worse, if you have June 30th, representing the end of that month, and you add two months to it, what should the answer be? August 30th? But that isn't the last date of that month, August 31st is! And that is probably what you meant. So here I require excplicitness; therefore addMonths gives you back not a CalendarDate, but a CalendarMonth, which looks like { year, month }. This means that CalendarDate is a structural subtype of CalendarMonth, and can be used anywhere a CalendarMonth is expected. So if in your domain you have April 15th representing the middle of the month, and you want to add two months to it and get the middle of June, that is June 15th, you have to put the day part back in, like this:

const apr: CalendarDate = { year: 2021, month: `apr`, day: 15 };
const jun = { ...addMonths(apr, 2), day: 15 };

A bit more verbose and annoying maybe, but a whole of a lot simpler and more excplicit. The same goes if you want the end of the month.

const endOfJune: CalendarDate = { year: 2021, month: `jun`, day: 30 };
const endOfAugust = lastDateInMonth(addMonths(endOfJune, 2)); // This is the 31st.

I'll leave you with a final example, building on a previous example. Sometimes you need a list of all the dates in a range or period to iterate over, let's say all the dates in Q1 from earlier. Simply use the aptly named periodOfDates.

const datesInQ1 = periodOfDates(startOfQ1, endOfQ1); // : CalendarDate[]

This function has an inclusive range in both ends for convenience, as this is what most people want most of the time when writing code like this.

Docs


CalendarYear

type CalendarYear = { year: number };

This is the most basic type in this library, mostly used to build upon by CalendarMonth and CalendarDate.


CalendarMonth

type Month = 'jan' | 'feb' | 'mar' ...
type CalendarMonth = { year: number, month: Month };

Month is a union of the 12 abbreviated strings representing the 12 months. Think of this as an enum. You can convert between Month and its month number using the functions monthNumber and monthName. This is mostly done for readability when debugging and leaving no question whether it is zero or one indexed.

CalendarMonth is a subtype of CalendarYear, and can be used anywhere CalendarYear can.


CalendarDate

type CalendarDate = { year: number, month: Month, day: number };

CalendarDate is the heart of this library, and is a subtype of CalendarMonth. A CalendarDate can be used anywhere where a CalendarMonth is expected. It is expected that you construct a value of this type manually at the edges of your program, which is why it's such a simple type. It's also expected you write some kind of formatting function (or many!) to display values of these types to your users.


numberOfDaysInMonth

const numberOfDaysInMonth: ({ year, month }: CalendarMonth) => number;

Gives you the number of days in that month, 28, 29, 30, or 31. Because of leap years you need to provide an entire CalendarMonth object which includes the year, not just the month, to get a correct answer. Here is an example of where you can send in a CalendarDate and get the expected result.


addDays

const addDays: ({ year, month, day }: CalendarDate, n: number) => CalendarDate;

Let's you n number of days to a CalendarDate, which gives you a new CalendarDate n dates in the future or the past. If you pass n = 0 you get a new CalendarDate object which is identical to the one you pass in. You can add thousands of days if you wish, this functions handles all leap years and all of that.

const yesterday = addDays(today, -1);
const tomorrow = addDays(today, 1);

addMonths

const addMonths: ({ year, month }: CalenarMonth, n: number) => CalendarMonth;

Let's you add n number of months to a CalendarMonth, giving you a new CalendarMonth. This is an example where you can pass in a CalendarDate. Note that this is an example of where you'd pass in a CalendarDate, which is a subtype of CalendarMonth. This is also the only way to add a number of months to a date. It might seem annoying to get out a CalendarMonth from this, because you probably want a CalendarDate. The way to deal with this is to then convert this value to a CalendarDate buy specifying what exactly you want. For instance, you might want to the first of the month, in which case you just specify day to be 1, as shown in the example below. If you want the last of the month, set it to numberOfDaysInMonth, explained above.

const nextMonth = addMonths(today, 1);
const firstOfNextMonth = { ...nextMonth, day: 1 };
const endOfNextMonth = { ...nextMonth, day: numberOfDaysInMonth(nextMonth) };

You might also want to keep the day which the original CalendarDate has, but be careful that this next month might have fewer days than that - check with numberOfDaysInMonth. You can of course also just add 30 days using addDays if you want, but that isn't exactly the same as adding a month. It all depends on your usecase of course.


areInOrder

const areInOrder: (...dates: CalendarDate[]) => boolean;

The signature of this function seems maybe more complicated than it is. This function is meant to be used to test whether you have dates "in order". If you have two dates, a and b, you can check if one comes before the other, which is essentially a <= b. But his function is also variadic, you can pass in any number of dates, in which case it evaluates to true if they are ordered in a monotonically increasing order. This is very useful if you want to test whether a third date, say c, is "in range of" a and b, which is essentially a <= c <= b.

Dates are considered to be in order if a comes before or is the same date as b.

areInOrder(today, tomorrow); // true
areInOrder(firstOfMonth, someDate, lastOfMonth); // true if `someDate` is in this month

isMonthBefore

const isMonthBefore: (a: CalendarMonth, b: CalendarMonth) => boolean;

Basically implements a < b for months, tests whether a comes strictly before b. This function can be used readily with CalendarDates, if you only care about the months they belong to.

isMonthBefore(today, tomorrow); // true if tomorrow is the first of the next month, false otherwise.

monthsEqual

const monthsEqual: (a: CalendarMonth, b: CalendarMonth) => boolean;

Basically implements a = b for months, tests whether a represents the same month as b. This function can be used readily with CalendarDates, for instance if you want to test whether two dates appear in the same month (and year!). If you only care whether they both appear in say December, but don't care whether those are in different years, just check a.month === b.month manually.

monthsEqual(today, tomorrow); // true if tomorrow is not the first of the month

isDateBefore

const isDateBefore: (a: CalendarDate, b: CalendarDate) => boolean;

Basically implements a < b for dates, tests whether a comes strictly before b. Maybe you want areInOrder, the difference between areInOrder(a,b) and isDateBefore is that the latter is "strictly less than", while areInOrder accepts equal dates as well. For most usecases I think areInOrder is probably what you actually want, but this is available if you need it.

isDateBefore(today, tomorrow); // true

datesEqual

const datesEqual: (a: CalendarDate, b: CalendarDate) => boolean;

Basically implements a = b for dates, tests whether a represents the same date as b. Only exists because JavaScript === is reference equality, which is mostly not what you want.

datesEqual(a, b); 

numberOfMonthsBetween

const numberOfMonthsBetween: ({ start, end }) => number;

Calculates the number of months between start and end, both of type CalendarMonth. This is an example of where you can pass in CalendarDates if you only care about the month part. This essentially implements end - start; if start and end represent January and February in the same year, respectively, it evaluates to 1. If you pass in the same month, it evaluates to 0. If end comes before start, you get a negative number.

numberOfMonthsBetween({ start: startOfYear, end: endOfYear }); // 11

numberOfDaysBetween

const numberOfDaysBetween: ({ start, end }) => number;

Calculates the number of days between start and end, both of type CalendarDate. This essentially implements end - start; if start and end represent today and tomorrow, respectively, it evaluates to 1. If you pass in the same date, it evaluates to 0. If end comes before start, you get a negative number.

numberOfDaysBetween({ start: firstDayOfYear, end: lastDayOfYear }); // 364 or 365, depending on leap year

const firstDayOfNextYear =  { ...firstDayOfYear, year: firstDayOfYear.year + 1 };
numberOfDaysBetween({ start: firstDayOfYear , end: firstDayOfNextYear }); // 365 or 366, depending on leap year

dayOfWeek

type WeekDay = 'mon' | 'tue' | 'wed' ...
const dayOfWeek: ({ year, month, day }: CalendarDate) => WeekDay;

Returns a three letter abbreviation of the day of week the CalendarDate represents. Think of this string as an enum, the idea is that you write a formating function or otherwise transform this string into a useable format.

const isWeekend = dayOfWeek(today) === 'sat' || dayOfWeek(today) === 'sun';

lastDateInMonth

const lastDateInMonth: ({ year, month }: CalendarMonth) => CalendarDate;

Gives the last date of the month you give it, whether that is a CalendarDate (in which case you get a different, that is the last, date in that month) or just a CalendarMonth. This function is implemented by setting the day to the last day of the month, calculated using numberOfDaysInMonth.

const firstOfNextMonth = addDays(lastDateInMonth(today), 1); // Hack to easily get next month's first date?

periodOfDates

const periodOfDates: (a: CalendarDate, b: CalendarDate) => CalendarDate[];

Produces a list of CalendarDates from a upto and including b. The result is an ordered list of sequential dates. The range is inclusive in both ends.

const allDatesInMonth = periodOfDates(firstOfMonth, lastOfMonth);

periodOfMonths

const periodOfMonths: (a: CalendarMonth, b: CalendarMonth) => CalendarMonth[];

Produces a list of CalendarMonths from a upto and including b. The result is an ordered list of sequential months. The range is inclusive in both ends. This is also a function where you can pass in a CalendarDate.

const allMonthsInYear = periodOfMonth(firstOfYear, lastOfYear);

monthName

const monthName: (n: number) => Month;

Used to generate the three letter abbreviation of the month, such as 'jan' for 1. One indexed, in other words. This function wraps around, allowing you to pass in values greater than 12 (although I'm not sure why you would).

const january = monthName(1);

monthNumber

const monthNumber: (m: Month) => number;

Used to produce the number of the month based on the three letter abbreviation. This is actually typed in such a way that you cannot send in just any string, it has to match one of the months.

const january = monthNumber('jan'); // 1

parseIso8601String

const parseIso8601String: (date: string): CalendarDate;

Parse a string of the form YYYY-MM-DD into a CalendarDate. This is especially useful when receiving data from an API where dates are represented as strings. Note that it needs to be specifically formatted as ISO 8601 strings, so 2 instead of 02 to represent February is not permitted.

This matches the start of the string; if there is more data, for instance timezone data in the form of 2020-02-04T00:00Z, this is ignored. Only the literal first date part is considered.

const calendarDate = parseIso8601String('2020-02-24'); // { year: 2020, month: 'feb', day: 24 }

If there are too many days in the month, or for some other reason the string cannot be parsed as a valid ISO 8601 string, the function throws an error message explaining what went wrong.


serializeIso8601String

const serializeIso8601String: (date: CalendarDate) => string;

Used to serialize a CalendarDate to a proper ISO 8601 string. Numbers less than 10 will be start-padded with 0 to fit the YYYY-MM-DD format.

const dateString = serializeIso8601String({ year: 2020, month: 'feb', day: 24 }); // "2020-02-24"

calendarDateFromJsDateObject

const calendarDateFromJsDateObject: (jsDate: Date) => CalendarDate;

This function is maybe useful if you already have a JavaScript Date object. However, this should usually not be necessary. For instance, remember that all data you receive from the backend or otherwise through JSON consist entirely of primitive values, i.e. typically strings of the form "2020-01-01" to represent dates. In this case it is much better to parse this string directly using parseIso8601String! And in general you should avoid using Date objects as much as possible.

One legitimate usecase might be an easy way to generate a CalendarDate representing today's date (as observed by who ever runs the code at that moment in time), as shown in the following example.

const now = new Date();
const todaysCalendarDate = calendarDateFromJsDateObject(now);

This works, but beware: this only works sofar the client running the code (be that a web browser, or a server in some data center) has their clocks configured correctly, and even then you are subject to all sorts of timezone issues, especially around daylight saving and other anomalies. Please use another means of determening the exact date if accuracy matters to you, and in any case, consider it only an approximation of the actual date.