Friday, August 19, 2022

Calculating Temporal Intervals with Luxon Business Days

man looking at javascript on two monitors

A short while ago, in May of 2021, I wrote about Luxon’s status as heir apparent to Moment.js as the later went into maintenance mode. Since then, creators of plugins for Moment.js have been scrambling to migrate their libraries to Luxon, as more and more organizations adopt it as their de facto JS Date library.

One such plugin is Luxon Business Days. As the name implies, it’s a library for calculating and manipulating business days that is based on its Moment Business Days fore-runner.

In this tutorial, we’ll use Luxon Business Days to access an array of daily stock prices without having to iterate over every element ourselves to find a specific day.

The Task Explained

Suppose that you had an object that stored a daily timeseries and their related stock prices:

const DAY_TIMESERIES = {
  close: [
    174.92, 
    172, 
    172.17, 
    172.19, 
    175.08, 
    175.53, 
    172.19, 
    173.07, 
    173.07, 
    169.8, 
    166.23, 
    164.51, 
    162.41, 
    161.62, 
    159.78, 
    159.69, 
    159.22, 
    170.33, 
    174.78, 
    174.61, 
    175.84, 
    172.9, 
    172.39, 
    171.66, 
    174.83, 
    176.28, 
    172.12, 
    168.64, 
    168.88, 
    172.79, 
    172.55, 
    168.88, 
    167.3, 
    167.3, 
    169.32, 
    170.07, 
    162.74
  ],
  timestamps: [
    "2022-01-05T00:00:00",
    "2022-01-06T00:00:00",
    "2022-01-07T00:00:00",
    "2022-01-10T00:00:00",
    "2022-01-11T00:00:00",
    "2022-01-12T00:00:00",
    "2022-01-13T00:00:00",
    "2022-01-14T00:00:00",
    "2022-01-17T00:00:00",
    "2022-01-18T00:00:00",
    "2022-01-19T00:00:00",
    "2022-01-20T00:00:00",
    "2022-01-21T00:00:00",
    "2022-01-24T00:00:00",
    "2022-01-25T00:00:00",
    "2022-01-26T00:00:00",
    "2022-01-27T00:00:00",
    "2022-01-28T00:00:00",
    "2022-01-31T00:00:00",
    "2022-02-01T00:00:00",
    "2022-02-02T00:00:00",
    "2022-02-03T00:00:00",
    "2022-02-04T00:00:00",
    "2022-02-07T00:00:00",
    "2022-02-08T00:00:00",
    "2022-02-09T00:00:00",
    "2022-02-10T00:00:00",
    "2022-02-11T00:00:00",
    "2022-02-14T00:00:00",
    "2022-02-15T00:00:00",
    "2022-02-16T00:00:00",
    "2022-02-17T00:00:00",
    "2022-02-18T00:00:00",
    "2022-02-21T00:00:00",
    "2022-02-22T00:00:00",
    "2022-02-23T00:00:00",
    "2022-02-24T00:00:00"
  ]
};

The Beauty of Parallel Arrays in Luxon

For this tutorial, we’ll keep the timeseries length brief and cap them at about two months’ worth. In a real-life scenario, the timeseries could extend back for years, so it be nice to avoid iterating over both the timestamps and close arrays. Luckily, we don’t have to. To understand why, imagine that we were looking for the stock price on February 21st, 2022. To do that, we would first have to find which timestamps element contained the date that we’re looking for. From there, we could retrieve the price from the close array using the same element index, because both the timestamps and close are Parallel Arrays. Also known as Structure of Arrays (SoA), these are multiple arrays of the same size such that the nth element of each array is related so that all nth elements together represent an object or entity. You’ll find them everywhere in application code, but one example of a parallel array is two arrays that represent x and y coordinates of n points.

Hence, finding the index of one element automatically points us to the related data in the other array.

Read: TimeZone Conversion with Luxon and JavaScript

Calculating the Number of Days between Two Dates Using Luxon

One of the things that Luxon is extremely good at is calculating the duration between dates. In fact, it would only take three lines of code to find the number of days between the first timestamp and the February 21st, 2022 target date that we’re looking for:

const firstClosingPriceDate = DateTime.fromISO(DAY_TIMESERIES.timestamps[0]); //"2022-01-05T00:00:00"
const targetPriceCalcDate = DateTime.fromISO("2022-02-21T00:00:00");

let diffInDays = targetPriceCalcDate.diff(firstClosingPriceDate, 'days').days;  //47

The diff() method returns a Duration object, so we need to access the days property if we only want the days and no other intervals, such as weeks or months.

Calculating the Number of Business Days between Two Dates

While the above result (47) is exactly right, there is one problem: daily stock prices are only provided on business days! If you look closely at the timestamps, you’ll notice that there is a two-day gap every five days. As it happens, the Moment Business Days library has the businessDiff() method, which conveniently skips non-business days in its calculations. Of course, since we’re using Luxon, it only makes sense that we pair it with the Luxon Business Days plugin.

This is probably a good time to mention that the Luxon Business Days plugin is not a full port of its Moment.js counterpart. And, as fate would have it, the businessDiff() method is conspicuously absent from the documentation, global DateTime namespace object, and source code. Does this mean that our plan is dead in the water? Far from it. We can port over the Moment.js businessDiff() method to our own project. Sure, it won’t work with Luxon straight away, but, thanks to Luxon’s For Moment Users page, we can easily substitute Luxon equivalents to Moment method invocations. Here is the brand new Luxon-compatible businessDiff() method:

DateTime.prototype.businessDiff = function(d2, relative) {
  var d1 = this;
  var positive = d1 >= d2;
  var start = d1 < d2 ? d1 : d2;
  var end = d2 > d1 ? d2 : d1;
  var daysBetween = 0;

  if (start.hasSame(end, 'day')) {
    return daysBetween;
  }

  while (start.startOf('day') < end.startOf('day')) {
    if (start.isBusinessDay()) {
      daysBetween += 1;
    }
    start = start.plus({ days: 1 })
  }

  if (!end.isBusinessDay()) {
    daysBetween -= 1;
  }

  if (relative) {
    return (positive ? daysBetween : -daysBetween);
  }

  return daysBetween;
};

The businessDiff() method returns 33, which equates to the index to fetch the price data:

let diffInDays = targetPriceCalcDate.businessDiff(firstClosingPriceDate); // 33

document.write(pricesFromTargetPriceCalcDate[diffInDays]); //172.79
document.write(
  DateTime.fromISO(daysFromTargetPriceCalcDate[diffInDays]).toLocaleString(DateTime.DATE_MED)
); //Feb 15, 2022

Read: Parsing Dates and Times Using Luxon

The Demo

In the codepen.io demo, the businessDiff() method is utilized to slice the close and timestamps arrays to the day after the targetPriceCalcDate, so that we can search for the first day that the stock’s closing price matches or exceeds the target price, using the native Array.findIndex() method:

const targetPriceAchievedIndex 
  = pricesFromTargetPriceCalcDate.findIndex(price => price >= targetPrice) + 1;

Conclusion

Not a fan of looping? Why not let someone else manage the burden of array iteration for you? Thanks to methods like diff(), businessDiff(), find(), and findIndex(), you can forget all about loops and code in a more method-oriented fashion that is closer to functional programming than your typical JavaScript.

Read more JavaScript programming and web development tutorials.

Rob Gravelle
Rob Gravelle
Rob Gravelle resides in Ottawa, Canada, and has been an IT guru for over 20 years. In that time, Rob has built systems for intelligence-related organizations such as Canada Border Services and various commercial businesses. In his spare time, Rob has become an accomplished music artist with several CDs and digital releases to his credit.

Popular Articles

Featured