Response time with JavaScript Date - Part 1
The following problem is more complex than most of the previous ones, so I decided to break it up into multiple articles.
Problem statement
You are receiving a new job from an air conditioning company. They make maintenance contracts with their clients and should fix broken devices within a time period set by the contract.
The response time given in hours is defined in terms of working hours, because maintenance guys have a life too, so they don’t work before or after business or on weekends. Their working hours are from 8am to 4pm from Monday to Friday (working hours should be configurable).
Given the report time, your job is to create a small app that calculates the exact date the company needs to fix the broken air conditioning by.
For example, if the response time is 6 hours and a client reports a malfunction at 2:30pm, the maintenance team must solve the problem by 12:30pm on the following day (1.5 hours left from the current working day, so the remaining 4.5 hours need to be added to 8am - the start of work - on the following day).
To make it simple, you don’t need to worry about public holidays this time, so every weekday from Monday to Friday counts as a regular working day.
A note on writing tests
I will show a solution using test-driven approach.
Why?
When I started out coding, I read a lot about the importance of testing but articles and tutorials didn’t really show any examples of how to do it. It seemed to me a bit like the weather in the quote.
Still, writing tests is very important to make our code more robust. That’s why I decided to write a few test cases while creating the algorithm. If you still don’t want to bother with writing tests, you can skip the sections marked with Test and follow only the coding part of the tutorial.
However, if you haven’t written tests yet and feel that this is the right time to start, you can read this quick introduction and write some simple test cases as practice.
I’ll also try to follow the functional direction and extract most parts of the logic into separate functions.
Breaking down the problem
The ultimate goal of the project is to return a date (year, month, day, hour and minutes) by when the maintenance team should fix the problem of the device. This means that we will have a date input (report date) and will need another date as the output (response time).
The maintenance guys have regular working hours, which means that we need to let the function know of the working and non-working hours as well.
Work stops at 4pm, so the function needs to stop counting time then and continue counting at 8am on the following working day. We have to make sure that non-working hours are ignored.
As air conditioning errors can occur outside working hours (they usually do), we have to handle cases when the malfunction was reported in non-working hours. These reports may not be lost and should be treated as if the client pressed the “Report error” button at the start of work (8am here) on the nearest working day.
Confusing? Here’s an example. If the error is reported at 6am, the nearest working day is the same day, so the clock starts ticking at 8am on that day. On the contrary, if someone breaks the air conditioning at 9:30 pm, right after the evening movie, the nearest working day becomes the following working day, so we have to start to count down from 8am on that day. If the problem was reported on Friday night, the following working day becomes the Monday coming after. I hope it makes sense.
We also make the app configurable, so that the company can change the response time and the working hours if they need to.
Translating it into JavaScript
As we have to work with times and dates, we will use JavaScript’s built in Date objects. Alternatively, date libraries like Moment.js can also be used but I’ll stick with native JavaScript when creating the logic.
The input is the date and time when the error was reported. The report time should be the argument of the function that calculates the response time. I would also add the response time as the second argument of the function to make it more configurable. We will set a default response time, so the application will work without providing that value each time when the function is called (after all, air conditioning companies don’t change their response time very often).
As for working and non-working hours, it’s a good idea to have some constants for the basic data, like start of the day and close of business. We will extensively use them and giving them expressive names will help us simplifying the code and making it more readable.
As I mentioned above, I will use a functional approach in this project, which means that we will have a bunch of small helper functions. These will only do one thing at a time. This approach is useful because these small functions can be tested in isolation and are reusable as well.
One such function will be a roll-over function which automatically jumps to the next working day. We will need this function to handle non-working hours and days.
Finally, the main function will calculate the response date. More on that later as we progress with the logic.
Setting up
If you follow me in writing tests for this mini-project, you will need Node.js and Mocha on your computer. If zou are unsure how to set them up, you can find some help here.
Now create a file called response-time.js
and response-time.test.js
if you want to write tests.
Creating the logic
That was a long introduction, and if any of the above was confusing, it hopefully becomes clear when we start writing the code.
Our main function will be called getResponseDate
, so declare it in response-time.js
:
function getResponseDate(reportDate, responseTimeInHours = 8) {
// stuff comes here...
}
As discussed above, the first parameter is the report date and the second is the response time the company set in their maintenance contract.
We use the ES6 default parameter feature and set it to 8 hours. If we don’t specify the second parameter when calling the function, the default value will be taken and the logic will be applied to this value.
Of course, if we provide a valid number as second argument, the entered value will override the default 8 hours, hence that will be used to return the result by the algorithm.
Test
Open response-time.test.js
and require the necessary dependencies:
const expect = require('chai').expect;
const { getResponseDate } = require('./response-time');
I’ll use Chai’s expect feature to create assertions this time and we will of course need the getResponseDate
function.
We need to export getResponseDate
in order to test it, so let’s do so in response-time.js
:
function getResponseDate(reportDate, responseTimeInHours = 8) {
// stuff comes here...
}
module.exports.getResponseDate = getResponseDate;
Let’s wrap our test cases into a describe
block in response-time.test.js
:
describe('#getResponseDate', () => {
// test cases come here...
}
describe
blocks help cluster similar test cases when the same test file is used to test multiple functions. The first parameter is a string usually referring to the function we want to test (you can write anything here but it’s a best practice to be descriptive), and the second parameter is a callback function, where we put the assertions.
Now that assertions have been mentioned, let’s write the first one inside the callback function:
describe('#getResponseDate', () => {
it('should return correct response time if it is within the same day', () => {
const reportTime = new Date(2018, 2, 6, 11, 15);
const responseTime = 3;
const expected = new Date(2018, 2, 6, 14, 15);
const result = getResponseDate(reportTime, responseTime);
expect(result).to.be.eql(expected);
});
}
We start with the simplest situation: The issue was reported at 11:15am on 6 March 2018 within working hours (reportTime
), the response time given in hours (responseTime
) is less than the time left to the end of the day, so we can calculate the response date by adding 3 hours to 11:15am (2:15pm, 6 March 2018), and we store this date in the expected
variable.
Next, we call the getResponseDate
function with the given parameters and store the result in a creatively name variable result
.
Our first assertion is that we expect the result
to be equal to our calculated date, which is 2:15pm, 6 March 2018.
If you run this test, it will of course fail, so let’s code the first few lines to make the test pass.
The easiest case: response time on the same day
So the situation is the following: The error is being reported some time during the day and the response time is a very short time period, so the repair can fit in the very same day.
We already declared the main function, getResponseDate
, so all we have to do is just add reportDate
and responseTimeInHours
together and return the result:
function getResponseDate(reportDate, responseTimeInHours = 8) {
return reportDate + responseTimeInHours;
}
Now let’s run the function by plugging in a report time of 11:15am, 6 March 2018 and a response time of 3 hours:
const now = new Date(2018, 2, 6, 11, 15);
getResponseDate(now, 3); // Tue Mar 06 2018 11:15:00 GMT+0100 (Central Europe Standard Time)3
Let’s save the report date in a variable called now
. One way of defining a date in JavaScript is to enter the year, month (0-based, so 0 is January, 1 is February and 2 is March), date, hour and minute. Optionally, you can also enter the seconds and milliseconds as well but we won’t go crazy here. now
represents the moment when the user reports their issue.
We don’t receive any error, so if you don’t look at the result carefully, you won’t see that something is really not OK.
But if you are following along with the test (I hope you are) and run it again, the test will fail with the following error message:
AssertionError: expected 'Tue Mar 06 2018 11:15:00 GMT+0100 (Central Europe Standard Time)3' to deeply equal Tue, 06 Mar 2018 13:15:00 GMT
Did you notice that 3
after the CET? What happened is that we wanted to add a number to a Date
object, so they got concatenated. This is definitely far from ideal. We need to find the common denominator of the two values in order to add them. (Note: With libraries like Moment.js it can easily be done but I want to use native JS here.)
One solution could be that we convert both the Date
object and the number of hours into milliseconds. Date
has a method called getTime
and this method does just that: converts the current date to milliseconds elapsed since midnight, 1 January 1970 UTC, and it’s formally called the Unix time.
We can also easily convert responseTimeInHours
into milliseconds: Let’s calculate how many milliseconds are in one hour and multiply responseTimeInHours
by that number. We might use this number later, so save it into a constant called ONE_HOUR
and declare it at the top of the file:
const ONE_HOUR = 60 * 60 * 1000; // in ms
(Quick explanation: 1 hour = 60 minutes, 1 minute = 60 seconds and 1 second = 1000 milliseconds. These numbers need to be multiplied to get 1 hour in milliseconds.)
Allright, let’s modify getResponseDate
accordingly, so the code looks like this so far:
const ONE_HOUR = 60 * 60 * 1000;
function getResponseDate(reportDate, responseTimeInHours = 8) {
let responseTimeLeftInMs = responseTimeInHours * ONE_HOUR;
return new Date(reportDate.getTime() + responseTimeLeftInMs);
}
We save the response time in milliseconds in the responseTimeLeftInMs
variable. After calling getTime
on reportDate
and adding responseTimeLeftInMs
to it (both are in milliseconds by now), we wrap it in the new Date
constructor, which creates a Date
instance. If we didn’t do this, we would get a really large number, the number of milliseconds since 1 January 1970 and we definitely don’t want to display that.
Let’s run the function again and we are receiving the correct date now (2:15pm, 6 March 2018).
Test
You need to change the expected
variable to reflect the change in the output (we can do this, the logic won’t break):
it('should return correct response time if it is within the same day', () => {
const reportTime = new Date(2018, 2, 6, 11, 15);
const responseTime = 3;
const expected = new Date(2018, 2, 6, 14, 15);
const result = getResponseDate(reportTime, responseTime);
expect(result).to.be.eql(expected);
});
If you run the test now, it should pass. Good job!
Conclusion
Now we can sit back, at least for a while. The hardest part is still left to do - but I will leave it for next time as this post is already very long.
I hope you find the project challenging. If so, see you in Part 2!