7
\$\begingroup\$

Is this the correct and an elegant way to sort the dates in the format (DD-MM-YYYY) in ascending order?

let dates = ['01-02-2018', '02-01-2018', '01-01-2018', '31-12-2017'];

dates.sort((date1, date2) => {
    date1 = date1.split('-'), date2 = date2.split('-');
    let day1 = parseInt(date1[0]);
    let day2 = parseInt(date2[0]);
    let month1 = parseInt(date1[1]);
    let month2 = parseInt(date2[1]);
    let year1 = parseInt(date1[2]);
    let year2 = parseInt(date2[2]);
    if (year1 !== year2) {
        return year1 - year2;
    } else if (month1 !== month2) {
        return month1 - month2;
    } else {
        return day1 - day2;
    }
});

There is also this way, as Cris Luengo said in the comments, I can directly compare the strings.

let dates = ['01-02-2018', '02-01-2018', '01-01-2018', '31-12-2017'];

dates.sort((date1, date2) => {
    date1 = date1.split('-').reverse().join('-');
    date2 = date2.split('-').reverse().join('-');
    if (date1 < date2) {
        return -1;
    } else if (date1 > date2) {
        return 1;
    } else {
        return 0;
    }
});

Which way is more performant? How do I measure that?

\$\endgroup\$
13
  • 3
    \$\begingroup\$ You don't need to parse the strings at all. A sort on characters is sufficient. If the dates were stored in the proper order (ISO format, YYYY-MM-DD), then a direct string comparison would work. Here you can compare strings date1[2] to date2[2], if those are equal, compare [1], etc. \$\endgroup\$ Jan 4 '18 at 4:37
  • \$\begingroup\$ Right, but in my case, the days come first. So this comparison '31-12-2017' < '02-01-2018'; will return false. \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 4:47
  • \$\begingroup\$ Do you suggest reordering the date to be in this format (YYYY-MM-DD) and then comparing the strings? \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 4:53
  • 12
    \$\begingroup\$ Don't store dates as strings within your application. At the earliest possible time, convert them to proper Date objects, and if a string representation is necessary, convert them to a String at the latest possible time. This way, you have the benefit of easy handling all throughout your applicaiton. This code would just be dates.sort ( (a, b) => return a - b ); \$\endgroup\$
    – Alexander
    Jan 4 '18 at 7:26
  • 1
    \$\begingroup\$ @Alexander I hate JavaScript date implementation and normally am staying away from them. It's much better IMO, to use ISO 8601 date representation for as long as possible (which is until you need to do date arithmetic) -- YYYY-MM-DDTHH:mm:ss. And when it comes to date arithmetic, use a library like moment.js or date-fns, anything but the damn native Date with horrible API. \$\endgroup\$ Jan 4 '18 at 18:26
9
\$\begingroup\$

UPDATE: You can run performance tests I prepared based on various code samples from the answers to this question: https://jsperf.com/cr-se-date-array-sorting/1. When I ran them, I see that my code is second best performing. The best is the one by @Kevin Cline (up voted!).

enter image description here


Here's my take on it. The following code uses the same idea about the string representation of date that is directly comparable for sorting.

It is not possible to tell in advance how performant will this solution be compared to one you presented above. It all depends a lot on how many dates are being sorted. If you deal with tons of dates, there will be many more invocations to the arrow function we're passing to sort(...). Therefore, it's not good to keep translating the date via split() every time our arrow function is being used.

Instead, I recommend three steps:

  1. Translate dates into a sortable representation (one time). Use .map().
  2. Do the sorting with .sort(). The values will be put in a lexicographical order.
  3. Translate the sorted dates back into original representation.

This will guarantee that each date is translated at most twice, which makes the entire solution more performant for large Ns.

Also, Notice that steps #1 and #3 can use same exact implementation, which I extracted.


const reverseDateRepresentation = date => {
  let parts = date.split('-');
  return `${parts[2]}-${parts[1]}-${parts[0]}`;
};

const sortedDates = ['01-02-2018', '02-01-2018', '01-01-2018', '31-12-2017']
  .map(reverseDateRepresentation)
  .sort()
  .map(reverseDateRepresentation);

console.log(sortedDates);

Produces result:

["31-12-2017", "01-01-2018", "02-01-2018", "01-02-2018"]

Little note. I think, that from the "big O" point of view we haven't improve the algorithm. Since .map(reverseDateRepresentation) is \$O(n)\$, the performance of the entire solution is limited by the .sort() (which is probably \$O(n * {\log n})\$). The way we're potentially improving solution is by making sure that the constants in our "big O" cost are as small as we can achieve.

Nevertheless, if we'd put the performance as top criteria of the solution, I'd learn as much as I can about the real data being processed; as well as conducted a thorough performance test.

In real life scenario, however, I personally never put performance above readability, because I believe that lack of readability is eventually the same thing as lack of correctness ...and correctness is almost always more important than performance (few exceptions are known though).

\$\endgroup\$
12
  • \$\begingroup\$ I don't think this could be any better, Thanks! \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 5:44
  • 1
    \$\begingroup\$ I'm not quite sure I understand what's happening here, How does String.localeCompare function know that it should compare the first argument with the second one, not the opposite? Is that the default behavior? \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 5:58
  • 1
    \$\begingroup\$ @MarkAdel yeah, that's how functions work in JavaScript. But scp713 made a good point. Even the String.localeCompare is not needed. We can just have .sort() without any arguments, since String.localeCompare is default \$\endgroup\$ Jan 4 '18 at 6:00
  • \$\begingroup\$ Awesome! You can edit your answer now to reflect his suggestion. \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 6:04
  • \$\begingroup\$ Read and fully understood the note, it makes sense. Thank you for your time! \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 6:12
5
\$\begingroup\$

Simply compare the characters in order from most significant to least significant:

let indices = [ 6, 7, 8, 9, 3, 4, 0, 1 ];
var dates = ["02-02-2018", "02-01-2018", "01-02-2018", "01-01-2018", "12-31-2017" ];

dates.sort((a, b) => {
    var r = 0;
    indices.find(i => r = a.charCodeAt(i) - b.charCodeAt(i));
    return r;
});

For each comparison this examines the minimum number of characters. Hard to imagine any other solution being faster, except maybe using a for loop instead of find.

\$\endgroup\$
7
  • \$\begingroup\$ Thank you, but I added the question under JavaScript tag, I expect the answers to be in JavaScript. I'm confused with this Java code, I'd appreciate it if you re-wrote it in JavaScript. \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 9:24
  • \$\begingroup\$ Sorry I missed that. I have replaced the original code with tested Javascript. \$\endgroup\$ Jan 4 '18 at 9:30
  • \$\begingroup\$ It's simpler in Javascript :-) \$\endgroup\$ Jan 4 '18 at 9:30
  • \$\begingroup\$ It's clear now, That's a smart approach I like it! Check Igor's answer too it's very efficient. \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 11:54
  • 1
    \$\begingroup\$ I implemented it using a for loop and ran a performance test, It's ~4 times faster! Check jsperf.com/date-array-sorting/1 \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 22:06
3
\$\begingroup\$

Looking at your update answer, I would like to offer a couple points. You could minimize the amount of 'custom' logic by using some bult-in functions. For example, your comparisons to determine which integer to return is functionally the same as String.prototype.localeCompare(). Instead of if/elses you could write return date1.localeCompare(date2);.

Also, at the start of your comparison function, in plain English you are essentially joining the array, but in reverse order. Therefore, why not utilize join and reverse? In fact, you could knock out the whole string preparation in a one-liner: date1.split('-').reverse().join('').

So the updated function would look like:

dates.sort((date1, date2) => {
    date1 = date1.split('-').reverse().join('');
    date2 = date2.split('-').reverse().join('');
    return date1.localeCompare(date2);
});
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Doing date.split('-').reverse().join('') on each invocation to the sort arrow function is too costly. See my answer with an alternative. \$\endgroup\$ Jan 4 '18 at 5:35
  • \$\begingroup\$ @Igor Soloydenko I can't comment on your answer (too low rep) but now that I think about it, you could omit String.localeCompare and just use .sort() due to sort's default behavior. \$\endgroup\$
    – csp713
    Jan 4 '18 at 5:53
  • 1
    \$\begingroup\$ Sure thing. One last thing is that the Array.prototype.sort() docs say that "If omitted, the array is sorted according to each character's Unicode code point value, according to the string conversion of each element", it doesn't mention localeCompare specifically \$\endgroup\$
    – csp713
    Jan 4 '18 at 6:12
2
\$\begingroup\$

As the code is reviewed in other answers, I'll suggest you to different approach.

let getTimestamp = str => +new Date(...str.split('-').reverse());
dates.sort((a, b) => getTimestamp(a) - getTimestamp(b));

The function getTimestamp will give the timestamp from the given string. This timestamp can be used in the sort function for comparing with other dates.

let dates = ['01-02-2018', '02-01-2018', '01-01-2018', '31-12-2017'];

console.time('tushar');
let getTimestamp = str => +new Date(...str.split('-').reverse());
let sortedDates = dates.sort((a, b) => getTimestamp(a) - getTimestamp(b));

console.log(sortedDates);
console.timeEnd('tushar');

References:

  1. Date
  2. Array#sort
  3. ... Spread syntax

Which way is more performant? How do I measure that?

To measure the performace, you may use jsperf.com. Or simply, use console.time as shown in the demo.

\$\endgroup\$
4
  • \$\begingroup\$ That's nice! But why not use new Date().getTime() instead of +new Date() which looks hacky. \$\endgroup\$
    – Mark Adel
    Jan 4 '18 at 6:40
  • \$\begingroup\$ @MarkAdel Yes. You can use getTime() as well, it is readable. + is short. \$\endgroup\$
    – Tushar
    Jan 4 '18 at 7:11
  • \$\begingroup\$ Unfortunately, string->Date parsing is costly, and it will limit the performance of the end result. \$\endgroup\$ Jan 4 '18 at 19:36
  • \$\begingroup\$ @IgorSoloydenko Yes. I saw the comparison in your answer. \$\endgroup\$
    – Tushar
    Jan 5 '18 at 4:33

Not the answer you're looking for? Browse other questions tagged or ask your own question.