Response time with JavaScript Date - Part 2

In the last article we introduced the problem of maintenance response times. It's a puzzle that makes an extensive use of JavaScript dates. We will create a bunch of helper functions in this post, which will help us create the final function that calculates the result.

We concluded Part 1 of the response time problem by solving the easiest scenario when the response and report times fall on the same day and no difficult calculations are needed to determine the result. Now it’s time to dive into more complicated situations.

Report and response times fall on different days

Companies rarely fix the issues of your air conditioning within 3 hours. I can imagine a much more realistic scenario when you need to wait 12 or even more hours. (My internet service provider has a 72-hour response time but internet is far less important in the 21st century than air conditioning.)

In this section we will see how to calculate the response date when the response time is longer than the length of a working day.

Test

Let’s create a test case, which covers this situation:

it('returns correct answer if response time does not fall on the same day', () => {
  const reportTime = new Date(2018, 2, 8, 11, 15);
  const responseTime = 16;
  const expected = new Date(2018, 2, 12, 11, 15);

  const result = getResponseDate(reportTime, responseTime);
  expect(result).to.be.eql(expected);
});

The setup is the following: We report the error at 11:15am on 6 March 2018 (which was a Thursday) but the response time is now 16 working hours. A working day is 8 hours long, so we need to move forward 2 days (i.e. 16 hours) in time to 11:15am on 12 March 2018, which was last Monday. This will be the response date.

Of course, if you run the test command, you will see thath only one test passes (the last one) and one fails (this one). Let’s write some code and make the test pass.

Analysis of an example

We are facing some issues that need to be handled. Let’s quickly sum them up.

If the issue was reported at 11:15am, we will have 4 hours and 45 minutes left from that day. This needs to be taken away from the response time (16 hours), so we have left with 11 hours and 15 minutes of buffer.

We need to ignore the time from 4pm on the report day to 8am on the following working day and any time period leftover from the total response time continues ticking at 8am on the following day, which is Friday in this case. But alas, air conditioning guys only work for 8 hours on Friday, which means that we will still have 3 hours and 15 minutes left by the end of the day.

The weekend comes and no one works on Saturday and Sunday. Hard work resumes at 8am on the following Monday, so we need to add those 3 hours and 15 minutes to that time and will get the result, which is 11:15am on 12 March 2018.

Let’s organize the most important to-dos in an unordered list:

  • We have to take note of weekdays and weekends, start and end of working days, which can be combined to working hours and non-working hours.
  • We have to make sure that our response time counter continues at 8am on the following day if we run out of time, so we will need to add one day to the current time when this happens.
  • We should skip the whole weekend, so will need an algorithm that handles this task.
  • Finally, when the leftover time is less than the length of a working day (like in the example above on Monday), we need to add this leftover to 8am (already done in Part 1).

Creating variables and adding one day

Let’s start by declaring variables for the weekend, start and end of days. They will be needed later, so it’s a good idea to store them in some constants:

const ONE_HOUR = 60 * 60 * 1000;
const SATURDAY = 6;
const SUNDAY = 0;
const START_OF_WORK = 8; // o'clock
const END_OF_WORK = 16; // o'clock

Since days are zero-based and Date object starts the week on Sunday, this day will receive the 0 index, and Saturday will be index 6. You will get these numbers by calling the getDay method on any valid JavaScript date. We also save the start and the end of work, which are 8 (8am) and 16 (4pm), respectively.

We have made start and end of days configurable, so these values can be changed any time without breaking the code.

Next, we create functions that automatically calculate weekend, start and end of work times:

const isWeekend = date => date.getDay() === SATURDAY || date.getDay() === SUNDAY;
const startOfWorkingDay = date => new Date(new Date(date).setHours(START_OF_WORK, 0, 0, 0));
const endOfWorkingDay = date => new Date(new Date(date).setHours(END_OF_WORK, 0, 0, 0));

I think these short functions above are fairly straightforward. If the day is a Saturday or Sunday, it’s a weekend. The startOfWorkingDay function surprisingly sets the time to 8:00am on any given day using the setHours method. The rest of the parameters (all of them are 0 here) refer to the minutes, seconds and milliseconds. endOfWorkingDay does the same with the end of the day.

Let’s create now a helper function that adds one day to the current date when needed:

const addOneDay = date => new Date(date.getTime() + ONE_HOUR * 24);

getTime returns the time in milliseconds since 00:00, 1 January 1970 and we add one day by multiplying ONE_HOUR (also given in milliseconds) by 24. This way we can be sure that the measurement units are the same throughout the calculations (milliseconds).

I hope that all of this so far makes sense. If not, read it again, because the next function is by far one of the most important parts of the algorithm.

We will create the function that skips the non-working hours and returns the start (8am) of the following working day. Remember, this is needed to continue counting how much time is left from the total response time.

I call this function getNextWorkingDay and it looks like this:

// returns 8am of the next working day in ms
const getNextWorkingDay = date => {
  let day;
  for (day = addOneDay(date); isWeekend(day); day = addOneDay(day));
  return startOfWorkingDay(day);
};

getNextWorkingDay has one argument, the date. Inside the function you can see an “incomplete” for statement. It’s definitely not the typical use case of for loop as It goes deep in the definition of the for statement. I learned this trick from a good friend of mine and here’s how it works.

For statements consist of three (optional) expressions in parentheses and a statement written in curly braces. As you can see, we don’t have curly braces here, because we don’t want to do anything else but moving on the following day, that is increasing time.

What does increasing mean here? Think about how JavaScript Date works. Date is defined in milliseconds since 00:00, 1 January 1970. When we add one day to the current date, we will add one day of milliseconds (86,400,000 milliseconds) to it, so we will increase the number of milliseconds elapsed since the Big Bang of computing.

This increase happens in the third expression of the for loop, which is the usual i++ part called final expression.

Let’s go over the structure of the for statement step-by-step. The initial expression of the for statement is date (the argument of the function) plus one day, received by using the addOneDay function. We already add that one day here.

The second expression inside the parentheses is the condition. Our condition is that the new date (day, we receive it by adding one day to the input date) falls on a weekend (isWeekend(day)). If so, we will do the operation in the final expression, that is we will add another day and make day equal to it. This is the increase of day we were talking about above.

The loop starts over. It again checks if day is a weekend day. If not, then the condition won’t apply, so we don’t add more days to day.

Finally, we return the startOfWorkingDay of the new day.

I wouldn’t be surprised if all of this sounded confusing, so let me explain it again with two examples.

First, imagine that it’s a Wednesday today, and date can be any time on Wednesday. We immediately add one day to it and make the new date equal to day in the initial expression of the for statement. This means that day already refers to the same time on Thursday. The next step is the condition: Is Thursday a weekend day? Unfortunately not, so the condition doesn’t apply, and the for loop ends. The value of day is that specific time on Thursday now, so we return the start of that Thursday.

Assume next that it’s a Friday now and a time on this Friday is going to be the input of getNextWorkingDay. The for loop immediately adds one day and stores the result in the variable called day. If we add one day to Friday, we’ll get Saturday (yay!). It’s a weekend day, so the condition applies (isWeekend) and we add a day again (final expression, day = addOneDay(day) part). day is now Sunday, and the for loop checks again if it’s a weekend. It is, so it adds another day and day will be equal to a time on Monday. It’s a working day, so we return the start of that Monday.

Well, this wasn’t easy.

Time left to the end of day

One more thing left before we start creating the function that calculates the result, and this is the step when we figure out the time difference between the report time and the end of day (the 4 hours and 45 minutes in the example way above).

All we need to do is to subtract the report time from the end of the day and, of course, it also needs to be calculated in milliseconds:

const timeToEndOfDay = date => endOfWorkingDay(date) - date; // in ms

Our code should look like this now:

const ONE_HOUR = 60 * 60 * 1000;
const SATURDAY = 6;
const SUNDAY = 0;
const START_OF_WORK = 8; // o'clock
const END_OF_WORK = 16; // o'clock

const isWeekend = date => date.getDay() === SATURDAY || date.getDay() === SUNDAY;
const startOfWorkingDay = date => new Date(new Date(date).setHours(START_OF_WORK, 0, 0, 0));
const endOfWorkingDay = date => new Date(new Date(date).setHours(END_OF_WORK, 0, 0, 0));

const addOneDay = date => new Date(date.getTime() + ONE_HOUR * 24);

// returns 8am of the next working day in ms
const getNextWorkingDay = date => {
  let day;
  for (day = addOneDay(date); isWeekend(day); day = addOneDay(day));
  return startOfWorkingDay(day);
};

const timeToEndOfDay = date => endOfWorkingDay(date) - date; // in ms

function getResponseDate(reportDate, responseTimeInHours = 8) {
  let responseTimeLeftInMs = responseTimeInHours * ONE_HOUR;

  return new Date(reportDate.getTime() + responseTimeLeftInMs);
}

Conclusion

We have everything in our hand now to extend getResponseDate and create the complete algorithm that returns the time and date by when the company needs to fix the broken air conditioning. As for the code, it’s not easy to figure out how to organize the functions and it’s completely OK to refactor them a few times.

The last part of this tutorial will cover the main function which calculates the response time. Enjoy coding and see you then!