JavaScript Sorting Gotchas

Home / JavaScript Sorting Gotchas

After my recent post on sorting within an Angular Component, I was chatting with another developer about JavaScript sorting. Some of my assumptions about data types and formats allow some edge cases to present themselves in the form of inaccurate sorting.


When we have have an array of objects with any sort of data in each member/property, the data type of the property becomes important. By letting the data source tell us each property’s data type, we can at least make a better effort at sorting correctly without edge cases interfering.

Dates can be problematic. Our data can tell us that it’s a date, but in what format is the date? This is where we rely on not only a data type, but a data format. In the simplest cases, in lieu of a data format, we can trust that the user’s data is in a format that JavaScript can parse directly into a new Date object. Converting a date based on a specific format, assuming the data is a String, can look like this with a prototype method:

String.prototype.toDate = function(dateFormat) {
	var dateStr = formattedStr = this;
	if (dateFormat) {
		var delimiter = dateFormat.match(/\W/g)[0];
		var arr = dateFormat.split(delimiter);
		var replaceStr = '$' + (arr.indexOf('YYYY') + 1) + '-$' + (arr.indexOf('MM')+1) + '-$' + (arr.indexOf('dd')+1);
		formattedStr = dateStr.replace(/(\d+)-(\d+)-(\d+)/, replaceStr);
		console.log(replaceStr + ' ' + formattedStr);
	}
	if (formattedStr.indexOf(':') === -1)
        	formattedStr += ' 00:00';	
	var date = new Date(formattedStr);
	if (date.getTime() === date.getTime())
		return date;
	return new Date(-8640000000000000);
}

// Convert like this:
var myDateStr = '04/05/2017';
var convDate = myDateStr.toDate('MM/dd/YYYY');

This converter is moving around the month, date, and year into a ISO8601 format so that we know JavaScript will recognize the date when we create a new Date object. It’s appending “00:00” onto the end of the date to simply force the date to the local midnight time. Of course, if this fails, we return, effectively, the minimum date that JavaScript allows.

Numerics can also be an issue due to locale rules and formats. For example, in the U.S., fractional parts of a number are separate by a decimal point and the thousands separator is the comma. The European standard is the opposite of this. As such, we can’t simply call “parseFloat” without knowing how to parse the number or what format the number is in. However, we can infer the format directly.

var euroTest = /[-+]?\s[0-9]{1,3}(.[0-9]{3}),[0-9]+/;
var replaceFunc = function (x) { return x == "," ? "." : ","; }
String.prototype.toFloat = function() {
    var str = this;
    var isEuroFormat = euroTest.test(str);
    // Swap commas and decimals
    if (isEuroFormat) { str = str.replace(/[,.]/g, replaceFunc(x)); }
    var retValue = parseFloat(str.replace(/[^0-9.-]+/g, ''));
    return isNaN(retValue) ? 0.0 : retValue;                           
};

// Convert like this:
var myNumericStr = '1,000,000.6754';
var convFloat = myNumericStr.toFloat();

The code above is, for the most part, using a regular expression to determine if the number is in the European format, as described above. If it is, then the commas and decimals are swapped accordingly. After the swapping, non-numeric characters are stripped out, and we can finally parse the value to a float.

Pulling these two prototype methods into a sort method for arrays where the dataType and dataFormat are passed in, it would then look like this:

var sort = function (array, fieldName, direction, dataType, dateFormat) {
    var sortFunc = function (field, rev, primer) {
        // Return the required a,b function
        return function (a, b) {
            // Reset a, b to the field
            a = primer(pathValue(a, field)), b = primer(pathValue(b, field));
            // Do actual sorting, reverse as needed
            return ((a < b) ? -1 : ((a > b) ? 1 : 0)) * (rev ? -1 : 1);
        }
    };

    // Have to handle deep paths
    var pathValue = function (obj, path) {
        for (var i = 0, path = path.split('.'), len = path.length; i < len; i++) {
            obj = obj[path[i]];
        };
        return obj;
    };
    
    var primer;
    switch (dataType) {
        case 'numeric':
            primer = function (a) {
                    var str = String(a);
                    return str.toFloat();                               
                };
            break;
        case 'date':
        case 'date-time':
        case 'datetime':
            primer = function (a) {
                var dateStr = String(a);
                return dateStr.toDate(dateFormat);
            }
            break;
        default:
            primer = function (a) { return String(a).toUpperCase(); };
            break;
    };

    array.sort(sortFunc(fieldName, direction === 'desc', primer));
};

Note that the switch statements are used to conditionally define the “primer” function. That function primes the values to be sorted – or prepares/converts them so that they can be compared. This comparison is what drives the Array.prototype.sort method.

These two changes, afaik, ensure that dates and numerics are sorted properly regardless of the end-users locale and provides flexibility if the server provides data in not easily discernible formats. I’ve already update my older Angular 1.x CustomTable directive’s client-side sorting routines with these methods.

Leave a Reply