views:

69

answers:

3

I'm working with an array of objects in Javascript and need to sort them by date and time. Here's the setup:

place

  • title
  • date (optional)
  • time (optional)

Conceptually, the application allows users to create a list of places they're planning to go. The array of events is manually ordered at first, and users can optionally add date and time values to places. I'm providing an button to sort by date...places with null dates need to be placed at the bottom of the list.

Right now, it's behaving differently across browsers. Here's the code (assume I have a handle on the _places array and the _list object):

var _orderByDate = function (e) {
    YUE.preventDefault(e); // yui
    _places.sort(function (a, b) {
        var dateA = new Date(a.date),
            dateB = new Date(b.date);
        if ((!dateA === null) && (dateB === null)) return 0; //they're both null and equal
        else if ((dateA === null) && (dateB != null)) return -1; //move a downwards
        else if ((dateA != null) && (dateB === null)) return 1; //move b downwards
        else if ((dateA == dateB)) return (a.time > b.time) ? 1 : ((b.time > a.time) ? -1 : 0);
        else return (dateA > dateB) ? 1 : ((dateB > dateA) ? -1 : 0);
    });
    _list.updatePlaces(_places);
}

If you recognize the sort code above, that's because I got the basics from another post, but I felt this one deserved it's own since it deals with dates...the other was just dealing with null values and text.

Anyway, in Chrome, the list seems to sort in a random order, and it keeps sorting differently every time I execute the _orderByDate function. In Safari, it sorts mostly correct the first time, but puts one null date place at the top of the list. In Firefox, nothing happens at all.

I'm a bit of a beginner, and I don't have a CS background at all, so I'm not adept at the basics like arrays, dates, times etc...and my debugging skills are limited to the Firebug console. No errors are being reported, so I really have no idea what's going wrong.

One thing to note, if I eliminate the date type from the function so it sorts the items as strings, it works correctly...but that means 1/10/2011 would sort before 1/9/2011, so I think I need the date type in there.

Any ideas what's going wrong? Is there a smarter way to do what I'm trying to do?

EDIT: Adding log values

First sort (Chrome):

  • 08/01/2010
  • null
  • null
  • 08/03/2010
  • null
  • null
  • null
  • null
  • 7/01/2010
  • null
  • null
  • null

Second sort (Chrome):

  • 08/01/2010
  • null
  • null
  • null
  • 07/01/2010
  • null
  • null
  • null
  • null
  • null
  • 8/03/2010
  • null
  • null
+1  A: 

[See it in action]

_places.sort(function (a, b) {
    var dateA = new Date(a.date + a.time), // merge the date & time
        dateB = new Date(b.date + b.time); // depending on the format
    if (!a.date && b.date) return 1;
    else if (a.date && !b.date) return -1;
    else if (dateA === dateB) return 0;
    else return (dateA > dateB) ? 1 : (dateB > dateA ? -1 : 0);
});
galambalazs
hmm, same results...maybe has to do with the null dates?
byron
oh, forgot to mention...i need the null dates to be at the bottom of the list.
byron
see my updated answer (and link).
galambalazs
thanks for the update. we were having a lot of problems with the JS Date object, so we built a function that breaks the strings apart into component numbers...see my answer below
byron
A: 

You can simplify the sorting algorithm by a great deal if you pre-process your array so that it will have a numeric representation of the column you want to sort by.

Add a column to the table that contains the UTC equivalent of the dates for instance. Then you can safely sort the array by the UTC property, but you will still display the string value.

for (var idx in _places)
    _places[idx].UTC = _places[idx].date ? new Date(_places[idx].date).UTC() : 0;

_places.sort(function(a, b)
{
    return a.UTC > b.UTC ? 1 : a.UTC < b.UTC ? -1 : 0;
});

If you don't want to use the Date object (pre-1970 dates):

for (var idx in _places)
{
    var row = _places[idx];
    if (!row.date)
    {
        row.sortable = 0;
        continue;
    }
    var date = row.date.split('/');
    row.sortable = 10000 * parseInt(date[2]) + 100 * parseInt(date[0]) + parseInt(date[1]); // year, month, day
}

_places.sort(function(a, b)
{
    return a.sortable > b.sortable ? 1 : a.sortable < b.sortable ? -1 : 0;
});

Of course this assumes that your dates will always have the same M/D/Y format.

Here's the above algorithm in action: http://jsfiddle.net/krNnn/

Dan Stocker
Interesting. Have a look at the solution we came up with...we needed to support non unix dates as well (distant past), so we removed all of the Date objects and just broke the strings apart into component numbers.
byron
Still I think it's less complex to doe a pre-process. You don't have to use the Date object, you can create a numeric representations like this: _places[idx].sortable = year * 10^9 + month * 10^6 + day * 10^4 + hour * 10^2 + minute. The whole thing comes down to 6 lines of code, maybe 10 because of the string splitting.
Dan Stocker
Very nice. I'll give it a try today.
byron
A: 

Ok, we got busy and built a rather complex function that works across all browsers. Some of the requirements we had were pretty special (dates in the distant past, need to sort null dates at the bottom, subsort by time). Here's what we did:

         var _orderByDate = function(e) {
            YUE.preventDefault(e);
            _places.sort(function(a,b) {

                var Ay, Am, Ad, By, Bm, Bd;
                var Ah, Am, Bh, Bm;

                var dateA = a.date.split("/"); 
                if( !dateA.length || dateA.length != 3 || isNaN(dateA[0]) ||
                    isNaN(dateA[1]) || isNaN(dateA[2]) ) {
                    dateA = -1;
                } else {
                    Ay = parseInt(dateA[2]);
                    Am = parseInt(dateA[0]);
                    Ad = parseInt(dateA[1]);
                }

                var dateB = b.date.split("/"); 
                if( !dateB.length || dateB.length != 3 || isNaN(dateB[0]) || 
                    isNaN(dateB[1]) || isNaN(dateB[2]) ) {
                    dateB = -1;
                } else {
                    By = parseInt(dateB[2]);
                    Bm = parseInt(dateB[0]);
                    Bd = parseInt(dateB[1]);
                }

                // null checks
                if(dateA == -1 && dateB == -1) return 0;
                if(dateA == -1 && dateB != -1) return 1;
                if(dateA != -1 && dateB == -1) return -1;

                // year check
                if(Ay > By) return  1;
                if(By > Ay) return -1;

                // month check
                if(Am > Bm) return  1;
                if(Bm > Am) return -1;

                // day check
                if(Ad > Bd) return  1;
                if(Bd > Ad) return -1;


                var timeA = a.time.split(":");
                if( !timeA.length || timeA.length != 2 || isNaN(timeA[0]) ) {
                    timeA = -1;
                } else {
                    if( timeA[1].match(/am/) ) {
                        Ah = parseInt(timeA[0]);
                        Am = parseInt(timeA[1].match(/\d+/));
                    } else if( timeA[1].match(/pm/) ) {
                        Ah = parseInt((timeA[0] * 1) + 12);
                        Am = parseInt(timeA[1].match(/\d+/));
                    }
                }
                var timeB = b.time.split(":");
                if( !timeB.length || timeB.length != 2 || isNaN(timeB[0]) ) {
                    timeB = -1;
                } else {
                    if( timeB[1].match(/am/) ) {
                        Bh = parseInt(timeB[0]);
                        Bm = parseInt(timeB[1].match(/\d+/));
                    } else if( timeB[1].match(/pm/) ) {
                        Bh = parseInt((timeB[0] * 1) + 12);
                        Bm = parseInt(timeB[1].match(/\d+/));
                    }
                }

                // null time checks
                if(timeA == -1 && timeB == -1) return 0;
                if(timeA == -1 && timeB != -1) return 1;
                if(timeA != -1 && timeB == -1) return -1;

                // hour check
                if(Ah > Bh) return  1;
                if(Bh > Ah) return -1;

                // minute check
                if(Am > Bm) return  1;
                if(Bm > Am) return -1;

                return 0;
            } );

            _list.updatePlaces(_places);
        }
byron