For many people, JavaScript Math in general, and most especially Date Math, is imposing and inhibitive to utilizing the language in this way – especially for those coming in from AMPScript.
When you are coming from a language that has specific functions built to do all the ‘nitty gritty’ for you, it can be upsetting to be expected to know and build your own formulas and functions to handle these actions. As well being expected to know what the correct properties and objects in SSJS you need to use and reference.
Many will just switch between languages inside the script to pass these values through AMPScript functions and then switch back. Although this is an option, it is not recommended because each time you switch languages you exponentially increase processing time and lower efficiencies of your script.
SSJS date capabilities are basically the same as ‘Vanilla’ JS Date object. Which means it is nothing like AMPScript or SQL date interactions. Rather than go into details about the difference, I wanted to present a solution to help those that are experiencing this issue with dates.
I wanted to share two functions that I ‘Frankensteined’ together over the years to mimic the DateAdd and DateDiff AMPScript functions which should help alleviate the good portion of anxiety people have with SSJS date math.
DateAdd
The AMPScript DateAdd function is created with the intention of adding a set datePart
of time to a specific part of the date. This will allow you to add hours, days, years and more to a date by 3 simple inputs.
To replicate this capability in SSJS, we have to create our own function to handle this. Below is the full function:
function dateAdd(date, interval, datePart) { //Backfill of Array.includes Array.includes = function(val,arr) { for(i=0;i<arr.length;i++){ if (!r || r == false){r = val == arr[i] ? true: false;} } return r; } // Verify if date is a date if(!(typeof date.getMonth === 'function')) return 'Not a valid Date'; var ret = new Date(date); //don't change original date // Verify if interval are positive integers var typeInt = typeof(interval); if (typeInt == 'number' && typeInt != 'NaN' && typeInt != 'Infinity') { return 'Unit needs to be an integer'; } // Verify if correct datePart || case insensitive var arr = ["y","q","m","w","d","h","mi","s","year","quarter","month","week","day","hour","minute","second"] if (!(Array.includes(datePart.toLowerCase(),arr))) { return 'Incorrect datePart passed'; } var checkRollover = function() { if(ret.getDate() != date.getDate()) ret.setDate(0);}; switch(String(datePart).toLowerCase()) { case 'y' : ret = new Date(date.getFullYear() + interval, date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); checkRollover(); break; case 'year' : ret = new Date(date.getFullYear() + interval, date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); checkRollover(); break; case 'q' : ret.setMonth(ret.getMonth() + 3*(interval+1); checkRollover(); break; case 'quarter': ret.setMonth(ret.getMonth() + 3*(interval+1); checkRollover(); break; case 'm' : ret.setMonth(ret.getMonth() + (interval+1)); checkRollover(); break; case 'month' : ret.setMonth(ret.getMonth() + (interval+1)); checkRollover(); break; case 'w' : ret.setDate(ret.getDate() + 7*interval); break; case 'week' : ret.setDate(ret.getDate() + 7*interval); break; case 'd' : ret.setDate(ret.getDate() + interval); break; case 'day' : ret.setDate(ret.getDate() + interval); break; case 'h' : ret.setTime(ret.getTime() + interval*3600000); break; case 'hour' : ret.setTime(ret.getTime() + interval*3600000); break; case 'mi' : ret.setTime(ret.getTime() + interval*60000); break; case 'minute' : ret.setTime(ret.getTime() + interval*60000); break; case 's' : ret.setTime(ret.getTime() + interval*1000); break; case 'second' : ret.setTime(ret.getTime() + interval*1000); break; } return ret; }
Any decent JS dev would probably vomit from looking at the above as it is pretty poor JS coding. As any good SFMC dev will tell you though, it is required to be ‘messy’ for it to work inside the SFMC SSJS environment.
SFMC SSJS Switch():
To allow for easier conditions, I opted to use a switch instead of a bunch of IF statements. This allows me to change the formula to match the datePart entered. Of note, the way this had to be implemented is outside best practice for the general JS swtich usage as you can see in my statement above.
For instance, in general the switch() statement should not require duplicated entries for the cases that share the same function. So in JS it should show:
switch(String(datePart).toLowerCase()) { case 'y' : case 'year' : ret = new Date(date.getFullYear() + interval, date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()); checkRollover(); break; case 'q' : case 'quarter': ret.setMonth(ret.getMonth() + 3*interval); checkRollover(); break; (etc...)
But inside of SFMC SSJS, it has to have an action following every case or it provides unexpected behavior.
Array.includes Backfill (from function):
Another quirk to consider is that SFMC SSJS uses ECMA 3, so native functions like Array.includes
is NOT native in SFMC SSJS and will instead need to be ‘backfilled’ inside the code to be utilized.
Array.includes = function(val,arr) { for(i=0;i<arr.length;i++){ if (!r || r == false){r = val == arr[i] ? true: false;} } return r; }
Function Call:
Now let’s break down how this function works. To use the function, you would follow the same process you would use for the AMPScript version:
dateAdd(1,2,3)
Ordinal | Type | Required | Description |
1 | Date | True | The date to adjust |
2 | Number | True | The interval to add |
3 | String | True | The date part to add on |
The date parts available to use are: (case insensitive)
Date Part | Valid Entries |
Year | ‘y’, ‘year’ |
Quarter | ‘q’, ‘quarter’ |
Month | ‘m’, ‘month’ |
Week | ‘w’, ‘week’ |
Day | ‘d’, ‘day’ |
Hour | ‘h’, ‘hour’ |
Minute | ‘mi’, ‘minute’ |
Second | ‘s’, ‘second’ |
Putting this into effect, you can see the below example of using this function:
var date = new Date(); var interval = 'Q'; var units = 1; var newDate = dateAdd(date,interval,units); Write('date: ' + date + '<br>'); Write('interval: ' + interval + '<br>'); Write('units: ' + units + '<br>'); Write('newDate: ' + newDate + '<br>');
Which would output:
date: Mon, 07 Dec 2020 07:13:23 GMT-06:00 interval: Q units: 1 newDate: Sun, 07 Mar 2021 07:13:23 GMT-06:00
Input Verification
To make sure that the function does not toss an error or incorrect information, we need to validate the inputs to be correct and alert the user to the issue if it is not.
To do this, we add some conditionals at the very top of the function:
// Verify if date is a date if(!(typeof date.getMonth === 'function')) return 'Not a valid Date'; var ret = new Date(date); //don't change original date // Verify if interval are positive integers if (interval < 1) { return 'Unit needs to be an integer of 1 or higher'; } // Verify if correct datePart || case insensitive var arr = ["y","q","m","w","d","h","mi","s","year","quarter","month","week","day","hour","minute","second"] if (!(Array.includes(datePart.toLowerCase(),arr))) { return 'Incorrect datePart passed'; }
These conditions will verify that the ordinal parameters passed are the correct format, data-type or fit within the required
DateDiff
The AMPScript DateDiff function is created with the intention of finding the numerical difference based on an interval between two date. This will allow you to know the number of hours, days, years and more between two dates by 3 simple inputs.
To replicate this capability in SSJS, we have to create our own function to handle this. Below is the full function:
function dateDiff(startDate, subtractDate, interval) { //Backfill of Array.includes Array.includes = function(val,arr) { for(i=0;i<arr.length;i++){ if (!r || r == false){r = val == arr[i] ? true: false;} } return r; } // Verify if Start Date is a date if(!(typeof startDate.getMonth === 'function')) { return 'Not a valid Start Date'; } // Verify if Subtract Date is a date if(!(typeof subtractDate.getMonth === 'function')) { return 'Not a valid Subtraction Date'; } // Verify if correct interval || case insensitive var arr = ["y","q","m","w","d","h","mi","s","year","quarter","month","week","day","hour","minute","second"] if (!(Array.includes(interval.toLowerCase(),arr))) { return 'Incorrect interval passed'; } //Convert both dates to milliseconds var startDateMS = startDate.getTime(); var subtractDateMS = subtractDate.getTime(); var diffMS = startDateMS - subtractDateMS; switch(String(interval).toLowerCase()) { case 'y' : diff = (((subtractDate.getFullYear() - startDate.getFullYear()) * 12) - subtractDate.getMonth() + startDate.getMonth())/12; break; case 'year' : diff = (((subtractDate.getFullYear() - startDate.getFullYear()) * 12) - subtractDate.getMonth() + startDate.getMonth())/12; break; case 'q' : diff = (((subtractDate.getFullYear() - startDate.getFullYear()) * 12) - subtractDate.getMonth() + startDate.getMonth())/3; break; case 'quarter': diff = (((subtractDate.getFullYear() - startDate.getFullYear()) * 12) - subtractDate.getMonth() + startDate.getMonth())/3; break; case 'm' : diff = ((subtractDate.getFullYear() - startDate.getFullYear()) * 12) - subtractDate.getMonth() + startDate.getMonth(); break; case 'month' : diff = ((subtractDate.getFullYear() - startDate.getFullYear()) * 12) - subtractDate.getMonth() + startDate.getMonth(); break; case 'w' : diff = diffMS / (1000*60*60*24*7); break; case 'week' : diff = diffMS / (1000*60*60*24*7); break; case 'd' : diff = diffMS / (1000*60*60*24); break; case 'day' : diff = diffMS / (1000*60*60*24); break; case 'h' : diff = diffMS / (1000*60*60); break; case 'hour' : diff = diffMS / (1000*60*60); break; case 'mi' : diff = diffMS / (1000*60); break; case 'minute' : diff = diffMS / (1000*60); break; case 's' : diff = diffMS / (1000); break; case 'second' : diff = diffMS / (1000); break; } var arr2 = ["y","q","m","year","quarter","month"] if ((Array.includes(interval.toLowerCase(),arr2)) && diff < 0) { diff = Math.ceil(diff) } else { diff = Math.floor(diff) } return diff; }
Switch and Backfill
This has the same eccentricities and requirements as DateAdd above involving the switch statement and back fill of Array.includes.
Function Call:
To utilize this function, you would follow the same process you use for the AMPScript version:
dateDiff(1,2,3)
Ordinal | Type | Required | Description |
1 | Date | True | The start date |
2 | Date | True | The date to subtract |
3 | String | True | The date part to add on |
The date parts available to use are: (case insensitive)
Date Part | Valid Entries |
Year | ‘y’, ‘year’ |
Quarter | ‘q’, ‘quarter’ |
Month | ‘m’, ‘month’ |
Week | ‘w’, ‘week’ |
Day | ‘d’, ‘day’ |
Hour | ‘h’, ‘hour’ |
Minute | ‘mi’, ‘minute’ |
Second | ‘s’, ‘second’ |
Putting this into effect, you can see the below example of using this function:
var startDate = new Date('09-02-2020') var subtractDate = new Date('02-01-2020') var interval = 'mi' var dateDiff = dateDiff(startDate,subtractDate,interval) Write('startDate: ' + startDate + '<br>') Write('subtractDate: ' + subtractDate + '<br>'); Write('interval: ' + interval + '<br>'); Write('dateDiff: ' + dateDiff + '<br>');
Which would output:
startDate: Wed, 02 Sep 2020 00:00:00 GMT-06:00 subtractDate: Sat, 01 Feb 2020 00:00:00 GMT-06:00 interval: mi dateDiff: 308160
Input Verification
To make sure that the function does not toss an error or incorrect information, we need to validate the inputs to be correct and alert the user to the issue if it is not. To do this, we add some conditionals at the very top of the function:
// Verify if Start Date is a date if(!(typeof startDate.getMonth === 'function')) { return 'Not a valid Start Date'; } // Verify if Subtract Date is a date if(!(typeof subtractDate.getMonth === 'function')) { return 'Not a valid Subtraction Date'; } // Verify if correct interval || case insensitive var arr = ["y","q","m","w","d","h","mi","s","year","quarter","month","week","day","hour","minute","second"] if (!(Array.includes(interval.toLowerCase(),arr))) { return 'Incorrect interval passed'; }
These conditions will verify that the ordinal parameters passed are the correct format, data-type or fit within the requirements.