TK
Home

Fun with Dates

9 min read
Two girls sitting together and eating icecream in restaurantPhoto by Jed Villejo

As an ex-Rubyist, I always liked to work with Ruby dates (mental note: not the timezone part). I liked the human way on how Ruby and Rails provide methods to handle the Date object.

In Ruby, we can get the current date by doing:

require 'date'

Date.today # #<Date: 2020-04-05 ((2458945j,0s,0n),+0s,2299161j)>

This is pretty cool! I can send a simple message to the Date object "hey, provide me the today date" by calling the today method.

Or simply get the year, month, day.

date = Date.today
date.year # 2020
date.month # 4
date.day # 5

Using Rails, it is also possible to call the yesterday method.

Date.yesterday

Rails also provides other interesting APIs: beginning_of_month, minutes.ago, days.ago.

So after a long time with Ruby and Rails, I started using JavaScript more and more. But the JavaScript Date object was really strange for me. I wanted to use all the Ruby/Rails date APIs but in JavaScript and Typescript.

I didn't want to monkey patch or build new methods in the JavaScript Date object. I could just provide some simple functions and handle the Date internally.

Dating dates

First things first: I wanted to better understand the Date object. How do we create it?

new Date();

By simply instantiate the Date object. We get the representation of now (the current date).

The other APIs I need to try was: getDate, getMonth, and getFullYear. These are all methods to handle the date.

const day: number = now.getDate(); // 5
const month: number = now.getMonth(); // 3
const year: number = now.getFullYear(); // 2020

We could experiment with a whole bunch of other methods here, but I think we are good to move to the next part.

Fun with dates

In this part, we will build functions! I wanted to try creating this API:

  • day
  • month
  • year
  • today
  • yesterday
  • beginningOfDay
  • beginningOfMonth
  • beginningOfYear
  • get(1).dayAgo
  • get(2).daysAgo
  • get(1).monthAgo
  • get(2).monthsAgo
  • get(1).yearAgo
  • get(2).yearsAgo

day, month, and year

In this case, we provide a date and it will return the day of this date we provided.

const day = (date: Date): number => date.getDate();
const month = (date: Date): number => date.getMonth();
const year = (date: Date): number => date.getFullYear();

And we can use it like:

const now = new Date();

day(now); // 5
month(now); // 3
year(now); // 2020

today and yesterday

With today function, we could just return the new Date() and we are good. But this returns the representation of now with "time" included.

new Date(); // 2020-04-05T18:58:45

But it would be great to return the beginning of the day. We could simply pass the day, month, and year to the Date and it will generate this for us.

const today = (): Date => {
  const now: Date = new Date();
  const day: number = now.getDate();
  const month: number = now.getMonth();
  const year: number = now.getFullYear();

  return new Date(year, month, day);
};

Great. The yesterday function would work very similarly. Just subtract the day and we are good to go.

const yesterday = (): Date => {
  const now: Date = new Date();
  const day: number = now.getDate();
  const month: number = now.getMonth();
  const year: number = now.getFullYear();

  return new Date(year, month, day - 1);
};

But what happens when we subtract the day if the day is the first day of the month?

// date to handle
new Date(2020, 3, 1); // 2020-04-01

// when subtracting the day: from 1 to 0
new Date(2020, 3, 0); // 2020-03-31

And what happens if it is the first day of the year?

// date to handle
new Date(2020, 0, 1); // 2020-01-01

// when subtracting the day: from 1 to 0
new Date(2020, 0, 0); // 2019-12-31

Yes, JavaScript can be pretty smart too!

With these two new functions, we can also refactor the logic to get the separated date into a separate function.

const getSeparatedDate = (): { day: number; month: number; year: number } => {
  const now: Date = new Date();
  const day: number = now.getDate();
  const month: number = now.getMonth();
  const year: number = now.getFullYear();

  return { day, month, year };
};

Let's improve this! This returned type could be a Typescript type.

type SeparatedDate = {
  day: number;
  month: number;
  year: number;
};

Less verbose now:

const getSeparatedDate = (): SeparatedDate => {
  const now: Date = new Date();
  const day: number = now.getDate();
  const month: number = now.getMonth();
  const year: number = now.getFullYear();

  return { day, month, year };
};

In this case, we are always returning the day, month, and year attributes of the current date. But what if we want to pass a different date? A new argument to the rescue:

const getSeparatedDate = (now: Date = new Date()): SeparatedDate => {
  const day: number = now.getDate();
  const month: number = now.getMonth();
  const year: number = now.getFullYear();

  return { day, month, year };
};

Now we have a function that can receive a new date, but if it doesn't, it just uses the default value: the representation of now.

How does our functions today and yesterday look like now?

const today = (): Date => {
  const { day, month, year }: SeparatedDate = getSeparatedDate();

  return new Date(year, month, day);
};

const yesterday = (): Date => {
  const { day, month, year }: SeparatedDate = getSeparatedDate();

  return new Date(year, month, day - 1);
};

Both functions use the getSeparatedDate function to get the Date attributes and return the appropriate date.

The beginning of everything

To build the beginningOfDay, it would look exactly of the today function, as we want to the current date but at the beginning of the day.

const beginningOfDay = (date: Date = new Date()): Date => {
  const { day, month, year }: SeparatedDate = getSeparatedDate();

  return new Date(year, month, day);
};

Nothing special here.

But just minor comment if you didn't notice: At first, I'm built this function to get the beginning of the day of the current day. But I wanted to make it flexible enough to get the beginning of the day of other days too.

So "argument", right? Now the function receives a date, but it is flexible to not receive it too. I just handle it with a default value of the current date.

For the beginningOfMonth, it will look pretty much the same, but instead of using the day, we just set it to 1.

const beginningOfMonth = (date: Date = new Date()): Date => {
  const { month, year }: SeparatedDate = getSeparatedDate();

  return new Date(year, month, 1);
};

You got it, the beginningOfYear is similar. But also changing the month attribute.

const beginningOfYear = (date: Date = new Date()): Date => {
  const { year }: SeparatedDate = getSeparatedDate();

  return new Date(year, 0, 1);
};

Traveling back in time

Now the get(1).dayAgo API. We could build a get function that receives a number and return an object like:

{
  dayAgo, monthAgo, yearAgo;
}

For each attribute of this object, it would be the returned value we expect.

const get = (n: number): { dayAgo: Date; monthAgo: Date; yearAgo: Date } => {
  const { day, month, year }: SeparatedDate = getSeparatedDate();

  const dayAgo: Date = new Date(year, month, day - n);
  const monthAgo: Date = new Date(year, month - n, day);
  const yearAgo: Date = new Date(year - n, month, day);

  return { dayAgo, monthAgo, yearAgo };
};

What about a DateAgo type?

type DateAgo = {
  dayAgo: Date;
  monthAgo: Date;
  yearAgo: Date;
};

And now using the new type:

const get = (n: number): DateAgo => {
  const { day, month, year }: SeparatedDate = getSeparatedDate();

  const dayAgo: Date = new Date(year, month, day - n);
  const monthAgo: Date = new Date(year, month - n, day);
  const yearAgo: Date = new Date(year - n, month, day);

  return { dayAgo, monthAgo, yearAgo };
};

We build each attribute: dayAgo, monthAgo, and yearAgo by basically handling the Date object as we know.

But now we also need to implement the object in the plural: daysAgo, monthsAgo, and yearsAgo. But only for a number greater than 1.

For these new attributes, we don't need to create a whole new date again. We can use the same value from the singular attributes.

We also need to handle the number received.

  • if it is greater than 1: return the object with plural attributes
  • otherwise: return the object with singular attributes
const get = (n: number): DateAgo | DatesAgo => {
  const { day, month, year }: SeparatedDate = getSeparatedDate();

  const dayAgo: Date = new Date(year, month, day - n);
  const monthAgo: Date = new Date(year, month - n, day);
  const yearAgo: Date = new Date(year - n, month, day);

  const daysAgo: Date = dayAgo;
  const monthsAgo: Date = monthAgo;
  const yearsAgo: Date = yearAgo;

  return n > 1
    ? { daysAgo, monthsAgo, yearsAgo }
    : { dayAgo, monthAgo, yearAgo };
};
  • In this case, I also created the DatesAgo type and used the Typescript Union Type feature.
  • We reuse the singular values.
  • And do a simple ternary to handle the number received.

But what if we pass a 0 or negative value? We can throw an error:

const get = (n: number): DateAgo | DatesAgo => {
  if (n < 1) {
    throw new Error('Number should be greater or equal than 1');
  }

  const { day, month, year }: SeparatedDate = getSeparatedDate();

  const dayAgo: Date = new Date(year, month, day - n);
  const monthAgo: Date = new Date(year, month - n, day);
  const yearAgo: Date = new Date(year - n, month, day);

  const daysAgo: Date = dayAgo;
  const monthsAgo: Date = monthAgo;
  const yearsAgo: Date = yearAgo;

  return n > 1
    ? { daysAgo, monthsAgo, yearsAgo }
    : { dayAgo, monthAgo, yearAgo };
};

The Date can be fun too. Learn the basic concepts and just play around with it, you'll like! I hope this post was valuable to you!

Resources

Javascript

Twitter Github