Date Handling by Csaba Kantor - 21022021
Date Handling by Csaba Kantor - 21022021
The topic of date handling carries a certain level of inherent complexity, however not enough that IT
tools and programming languages could not have implemented them in a uniform and
comprehensible way. Numerous Wikipedia and IT forum articles have been composed on the topic,
down to the details of date manipulation such as Determination of the day of the week - Wikipedia
and others.
These very different approaches and implementations clearly showcase the lack of consensus on
date operations, however common these tasks are.
The main assertion of this article is that a handful of methods are sufficient for handling date related
tasks entirely. The article also details example implementations.
Prerequisites
Gregorian Calendar
You know it already!
This article covers only the Gregorian calendar (a year is 365 days long, unless it is a leap year, seven
day long weeks etc.)
A certain level of caution is required when handling dates in the past because immediately after its
inception in '1582-10-04' , the next day was '1582-10-15' to correct the leap year problems of the
preceding Julian calendar and different countries made the switch at various later dates (sometimes
even centuries later).
ISO 8601
The ISO 8601 is an international standard for date interchange but it does not cover date type
methods.
Notes
1. One does wonder whether the 'T' is really useful in the human readable format of "2015-03-
25T12:00:00Z"?
Date type
1Disclaimer of code examples
The JavaScript examples are tested by me, however I do not claim they are flawless, effective,
exemplary or should even be taken as suggested implementations.
Date (timestamp) deserves to be a pre-defined (primitive) type, resulting from the aforementioned
handling requirements.
It requires an internal numeric representation of the elapsed time (in milliseconds for instance) from
a given starting point including negative numbers to represent dates before that.
A single date type is sufficient for implementing both:
● Calendar days (date only, time part is '00:00:00');
● Time only variations (date part is the starting date).
Although it is understandable why separate ones may be more convenient or even desired, however
they are not necessary. It is worth noting that the Year-Month arithmetic may deserve its own
(sub)type (see later).
Notes
2. When one or more date types have been implemented in a system, they are always numerically
represented.
3. Excel has one internal date type, representing days as an integer and time as a decimal fraction part from
1900-01-00. Yes, zero day is not a typo; 1900-01-01 converted to 1, 1989-12-31 (or earlier) is not a valid
date (check for yourself – i.e. enter 0 and format as date.
4. SQL standard (SQL:2019) defines 4 types – date, time, smalldatetime, datetime – MS SQL Server extends
this to 6.
5. JavaScript, PHP and other programming languages support date type (object); although Java does not.
6. JavaScript represents date as elapsed milliseconds as an integer from '1970-01-01' and earlier dates are
negative numbers.
7. HTML has <input type="date"> and a <time datetime="2017-02-14">Valentines day</time>
tag.
8. The widespread JSON (from 2001, after JavaScript) serialization format is without any date type, YAML
following suit.
Notes
1. Excel’s DATE function assembles a calendar day, there is no function to assemble a timestamp.
2. One of JavaScript’s date initialization options is to assemble: new Date(year, month, day, hours, minutes,
seconds, milliseconds).
3. MS SQL DATEFROMPARTS assembles calendar day, DATETIMEFROMPARTS assembles timestamp (one
function for each date type).
Leap year
A year is leap year if divisible by four, unless also divisible by hundred, except also divisible by 400.
Thus 2000 was leap year, but 1900 is not.
IsLeapYear(year)
Excel formula for leap year check:
=IF(And(Mod(Year;4) =0;Or(Mod (Year;100)<>0; Mod(Year;400)=0));
Non-success stories
Excel, Excel VBA have no function to check for leap year.
Moreover, back in history Lotus 1-2-3 as market leader of spreadsheets, recognized 1900-02-29 as valid date,
so to be compatible Excel also treat it as valid day (although VBA treat it correctly).
MS SQL Server also has no leap year function, but as Excel has function for the last day of the month:
EOMONTH.
JavaScript has no even last day of the month method.
Ordinal dates
An ordinal date is the number of days in that year (1st of January is one).
DayOfYear(year, month, day)
Possibly the easiest calculation to understand, when the days until each month number is stored in
an array.
JavaScript implementation
function DayOfYear(year,month,day) {
let totalDays = day;
// days in month: Jan-31, Feb-28/29, March-31, Apr-30, May-31, Jun-30, Jul-31, Aug-
31, Sep30, Oct-31, Nov-30, Dec-31
let monthDays = [31,59,90,120,151,181,212,243,273,304,334,365]
if ( IsLeapYear(year) ){
monthDays = [31,60,91,121,152,182,213,244,274,305,335,366];
}
return days;
}
Any regional format can be matched with a format code. For month and weekday names you can use
the environment settings or supply it as parameter.
For example:
FormatToISOString(’02-07-2014’, 'DD-MM-YYYY’)
The only hardship is to settle the possible format codes. Unfortunately, there are slight differences,
but considering the less than dozen date parts, mapping one tools formats to another can easily
done.
Logic to implement
The key to implement the date string transformation is to get the position for each date part code
from the format parameter and extract the same position from the date string.
JavaScript implementation
function FormatToISOString(_date,_formatFrom) {
return formatedDate;
}
JavaScript implementation
function RelDate(ISODateStr) {
const datePartRec = ISODateStringToParts(ISODateStr);
return
AssemblyDate(datePartRec.year,datePartRec.month,datePartRec.day,dateP
artRec.hours,datePartRec.minutes,datePartRec.seconds,datePartRec.mill
iseconds);
}
Convert string to date
Convert a date string to the default format, then it can be transformed to relative date with the
previous RelDate function. Again, let it be a named method.
ConvertToDate(dateStr, formatCode [, locale] ) =
RelDate( FormatToISOString(dateStr, formatCode [, locale] ) )
Notes
Excel automatically convert a string recognized as date and provides the DATEVALUE function for it. No
function for converting any date string (although a date can be formatted to any string).
MS SQL Server’s CONVERT accept roughly half hundred ‘date and time styles’ instead of format codes. If your
locale or target format not included, then create your custom logic! The style explanations as well as FORMAT
uses format codes for date-to-string conversion, which makes the styles hard to comprehend.
JavaScript's Date.parse() method convert valid ISO date string, there is no more general method.
The ISO 8601 syntax (YYYY-MM-DD) is also the preferred JavaScript date format.
For example:
var d = new Date("2015-03-25");
To be precise, there are four ways of instantiating a date in JavaScript:
var d = new Date();
var d = new Date(milliseconds);
var d = new Date(dateString);
var d = new Date(year, month, day, hours, minutes, seconds, milliseconds);
IsValidDate(ISODateStr)
If the string transformation functions return valid dates width date arithmetic, then validity check can
be implemented by converting from and back. If the result equals the original string, it was a valid
date.
JavaScript implementation
function IsValidDate(ISOString) {
return ( ISOString == DateToISOString(RelDate(ISOString)) );
}
Notes
Excel, MS SQL Server, JavaScript are lacks date-string validation.
Reverse operations on (relative) dates
The previous chapter discussed the string-to-date transformations, now do it in reverse!
The logic is basically reverse of the respective method or the opposite direction (for example:
DatePart <-> AssemblyDate), so we leave them out for brevity. (Yes, it was taken into consideration
for this article!)
We will use Year, Month, ... shorthands for GetDatePart( ..., 'Year'), etc.
JavaScript implementation
Notes
Excel and JavaScript implemented individual Year, Month, etc. methods.
MS SQL Server opted for individual and the general DATAPART function.
As with the reverse direction, start from default date string the generation of display formats.
Notes
Excel has no function to convert date to string, but from the Toolbar the ‘custom’ number formatting accepts
format codes.
MS SQL Server’ FORMAT function accept locale codes as well as format codes.
JavaScript only convert to its default (ISO full text string), to the locale and UTC formats.
Leap year
Depending on the particular language possibilities you need to function or one polymorphic for
accepting a year number and a date.
IsLeapYear( date | year )
JavaScript implementation
function IsLeapYear(yearOrDate) {
var year = (typeof yearOrDate === "number") ? yearOrDate : yearOrDate.getFullYear();
return (year % 4 == 0) && ( (year % 100 != 0) || (year % 400 == 0) ) ;
}
Days in a year
A variation of the leap year check is to return the number of days in the year.
DaysInYear( date | year )
JavaScript implementation
function LeapYearCnt(dateFrom, dateTo) { // with year parameters, end leap year counts
var yearFrom;
var yearTo;
var monthTo;
if ( typeof dateFrom == "number") {
yearFrom = dateFrom;
yearTo = dateTo;
monthTo = 12;
}
else {
yearFrom = dateFrom.getFullYear();
yearTo = dateTo.getFullYear();
monthTo = dateTo.getMonth();
}
if ( yearFrom < yearTo && monthTo <2) { yearTo -= 1} // end date's leap year count
only from March
return leapYearCnt;
}
Notes
It seems, leap year count does not worth to implement.
JavaScript implementation
function DayOfYear_(date){
return (Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) -
Date.UTC(date.getFullYear(), 0, 0)) / 24 / 60 / 60 / 1000;
}
Notes
Excel, JavaScript have no DayOfYear equivalent function.
MS SQL Server’s DATEPART with ‘dayofyear’ argument returns the day of year.
Days in a Month
Likely you know the number of days in any month, if not implemented it can be determined with a
case structure, or formula - both requires the leap year check as well.
EndOfMonth(date | year-month)
Calculation
With a case structure:
CASE When Month(<date>) in(1,3,5,7,8,10,12) 4 Then 31
When Month(<date>) =2 And IsLeapYear(<date>) Then 29
When Month(<date>) =2 And Not(IsLeapYear(<date>) ) Then 28
Else 30 END
Or even with a formula:
30+Mod( Month(<date>)+ Floor (Month(<date>)/8;1);2)-
IF(Month(<date>)=2;IF(IsLeapYear (<date>);1;2);0)
Notes
My SQL and Excel both have EOMONTH function, but JavaScript has not.
Hints to logic
Lookup the weekday of the chosen starting day – in our case Thursday for 1970-0-01.
The beginning of year shifts a day every year (365 % 7 -> 1), plus one for each leap years.
JavaScript implementation
// 0=Monday, …,6=Sunday
function StartWeekday(year) {
let dayNo = 3; // 5 <- StartWeekday(1970) - Thursday
dayNo += year-1970+ LeapYearCnt(1970,year);
dayNo += (IsLeapYear(year) ? -1 : 0); // end date's leap year count only from March in
LeapYearCnt
dayNo = dayNo % 7;
dayNo += 7 // JavaScript: -1 % 7 -> -1
return dayNo % 7;
}
Weekday
Once we have the week day for January 1st, add the day of the year and the reminder of the division
by seven gives the sequence number within the day.
DayOfWeek(year,month,day)
JavaScript implementation
// 0=Monday, …,6=Sunday
function DayOfWeek(year,month,day) {
let dayNo = StartWeekday(year);
dayNo += DayOfYear(year,month,day)-1;
dayNo = dayNo % 7;
dayNo += 7 // JavaScript: -1 % 7 -> -1
return dayNo % 7;
}
Notes
Excel’s WEEKDAY function corresponds to the DayOfWeek function.
MS SQL's DATEPART gives correct weekday, day of the year, week of the year with appropriate format code.
JavaScript has no DayOfWeek method.
Week dates
A year (including leap year) does not divisible by seven, thus we need a convention to number the
weeks.
ISO 8601 defines the first week with the majority (four or more) of its days in the starting year (the
week with 4 January in it, the first Thursday in it.
For example, the first week of year 2004 (2004W01) started on Monday, 29 Dec 2003.
WeekOfYear(year,month,day)
The weekday for the beginning of the year gives as the week number for it by the definition. Then we
take the remaider by dividing the day of the year by dividing 7 to get the week’s sequence number.
Alternatively (for example in USA), you include the 1st of January into the first week of the year. It
follows, that the year always have 52 weeks, although the 52nd can be the 1st of the following year
as well.
WeekOfYear(year,month,day, ‘January 1 is the first week’)
To calculate it, simply divide the DayOfYear by seven and get the Ceil of the result.
JavaScript implementation for ISO week
function WeekOfYear(year,month,day) {
let weekNo = (StartWeekday(year) < 3 ? 0 : 1);
weekNo += 1 + Math.floor( (DayOfYear(year,month,day)-StartWeekday(year))/7 );
Notes
Excel’s WEEKNUM function implement both the ISO standard and the January 1 is the first week counting.
MS SQL's DATEPART gives ISO week of the year with appropriate format code.
JavaScript has no DayOfWeek method.
Date arithmetic
With a numeric date representations, date arithmetic basically becomes regular number addition,
subtraction.
Differences in dates
As you represent both start and end dates in elapsed time from a starting point, their numeric
difference becomes the elapsed time in the representation scale.
DateDiff(startFrom, dateTo, datePart)
Hints to logic
Multiply the elapsed milliseconds with 1000 to get the difference in seconds, and so on.
For months and years, you get the difference of year and month parts, make adjustment if the day in
end date is smaller.
JavaScript solution
Notes
Excel mathematical minus results the same as the DAYS function, namely fraction, where the integer part is the
elapsed days. Because the 1900 leap year bug, DAYS returns 2 (instead of 1) from 1900-02-28 to 1900-03-01.
MS SQL's DATEDIFF_BIG (or DATEDIFF upto 32 bit integer results) corresponds the above DateDiff function..
No date difference function in JavaScript.
Date addition
With date part code as input, on function suffice here as well.
DateAdd(date, offset, datePart)
For the year-month addition another function can be called internally.
MonthAdd(date, year, month)
Examples:
MonthAdd(’2020-01-31’, 1) -> ’2020-02-29’
MonthAdd(’2021-01-31’, 1) -> ’2021-02-28’
Hints to logic
Adding date parts to a date also easy with numeric date representation for day, hours, etc.
As for months and years, there are some logical steps:
● Convert the months and years if necessary based on that 1 yaer = 12 months
● Calculate the result year and month
● Determine the result day of the month
If the parameter date has larger end-of-day than the result month, then use the result
month's end
JavaScript solution
One function for all date parts:
function DateAdd(date, offset, datePart) {
var inMillisec = {
week: 1000*60*60*24*7,
day: 1000*60*60*24,
hours: 1000*60*60,
minutes: 1000*60,
seconds:1000,
milliseconds: 1
};
if (datePart == "year") {
return MonthAdd(date, offset, 0);
}
else if (datePart == "month") {
return MonthAdd(date, 0, offset);
}
else {
return date + offset * inMillisec[datePart];
}
}
Notes
Excel's EDATE function gives April 30th as one month after Marc 31 st, thus implement MonthAdd. No function
for general date arithmetic but adding / subtracting a fraction to a date in formula works.
MS SQL's DATEADD corresponds the above DateAdd function..
JavaScript’s setYear, setMonth, setDate, setHour, … can be used for date arithmetic, f. e.:
daysAdded.setDate(daysAdded.getDate() + 4);
Setting date parts
So far, we have no operation to set a date part in a date variable.
SetDatePart(date, value, datePart)
Notice, that with date arithmetic we actually change date parts!
Considering this, the logic can be:
● get the data parts of the date parameter
● subtract the corresponding date part from the new value parameter
● add the result to the date parameter
JavaScript solution
Notes
Excel and MS SQL Server has no dedicated function to set date parts.
JavaScript has individual functions: setYear, setMonth, setDate, setHour, etc.
Storing dates
You expect at least hundred years to store - take the birth dates of living persons - this gives roughly
36,500 days, 876,000 hours, ...., 3,153,600,000 seconds.
So, we got around 70 years only with the possible range of the 32 bit integers (2,147,483,647), so
before the 64 bit operating systems some compromise was needed.
Considering local times, you also should store time zone and summertime / wintertime information
as well.
Unfortunately, your toolset possibly not support daylight saving awareness, requiring custom logic.
Notes
Unfortunately the limitations of 32 bit integers had already lead some real catastrophes, and possible problems
GMT 3.14.07am on Tuesday 19 January 2038, when we the number of seconds since 1 January 1970 (common
starting point) exceeds the 32 bit limit; see: The number glitch that can lead to catastrophe - BBC Future).
None of the six (!) date and time data types is daylight saving aware in MS SQL Server – in fact only one is
stores time zone (Date and Time Data Types and Functions - SQL Server (Transact-SQL) | Microsoft Docs).
Same with Oracle (Oracle Date Functions (oracletutorial.com)).
Actual date and time
Getting the actual date, time – time zone, daylight savings – complement the date operations.
Now()
Obviously, no computation can be provided here.
DatePart gives calendar date, time part; for convenience they can be individual methods.
The above Now() belongs to the client device, in an enterprise environment server time is also
necessary.
ServerTime()
Notes
Naturally, Excel, MS SQL Server and JavaScript have an actual day service.
Workdays
You can compute that a calendar day is workday if holidays are rule based.
In Hungary, when a national holiday is close to the weekend, the non-holiday Monday or Friday may
be added to the non-workdays. To compensate, some Saturday becomes workday. As it announced
yearly, the only option to store a yearly refreshed workday calendar.
Notes
Excel has WORKDAY and WORKDAY.INTL functions, both expecting the holidays as parameter.
Conclusions
The opening section included the figure of main date representations and their transformations,
below the figure remaining for completeness.
Reasoning
The basis of a date type to represent a calendar day, timestamp as number, namely as elapsed time
from a fix starting point: relative date.
The elapsed days depends on leap years and variable month lengths therefore we started three
helper calculations: the calculation of leap years (LeapYearCnt using IsLeapYear), the number of days
in a year (DayOfYear). Now it possible to compute the relative date from the year, etc. date parts
(AssemblyDate).
Apart from date parts, we need to compute relative date from a chosen string format
(ISODateStringToParts) and to transform any date string to this format (FormatToISOString).
For convenience allow to convert to date from the default string format (RelDate) and from any date
string (ConvertToDate) in one step.
Do the reverse direction as well: date to default string (DateToISOString), standard string to any
format (FormatFromISOString), any format directly from date (DateToString).
From a relative date we calculate date parts (DatePart), and derived properties (EndOfMonth,
DayOfWeek, WeekOfYear).
Date arithmetic requires difference between two dates (DateDiff) and adding to or subtracting from a
date an offset of some date part (DateAdd).
Beyond custom calculations, we expect to get the actual date and time (Now) and time zone, daylight
savings, locale information for multi-national applications.
Implementations
We saw several issues with date handling (see Notes):
1. no date type at all
2. too restricted range of calendar days
3. missing methods
4. bugs in date computations
5. missing date arithmetic, derived date computation
6. string-to-date conversion only for one format or just handful of predefined ones
7. missing time zone, daylight savings, locale information
Supplement
Test scripts
A date calculation / formula easily can work for a large number of cases meanwhile breaking on some
others. Therefore, it is highly advisable to test an implementation thoroughly.
An example of test cases for each day in twenty years for the DatePart function:
console.warn("DateParts Tests");
var testDates = [];
for (y = 1970; y < 1990; y++) {
for (m = 1; m < 13; m++) {
for (i = 1; i < 31; i++) {
ds = y + "-" + (m<10 ? "0": "") + m + "-" + (i<10 ? "0": "") + i + "T00:00:00Z";
relDay = RelDate(ds);
d = new Date(ds);
if ( DatePart(relDay,"day") != d.getDate() ) {
console.log(`m: ${ m } i: ${ i } relDay: ${ relDay }`);
console.error(`DateParts- date string: ${ ds }; DateToISOString: $
{ JSON.stringify(DatePart(relDay)) }`);
}
}
}
}