How do you sort an array on multiple columns?

2018-12-31 20:46发布

问题:

I have a multidimensional array. The primary array is an array of

[publicationID][publication_name][ownderID][owner_name] 

What I am trying to do is sort the array by owner_name and then by publication_name. I know in JavaScript you have Array.sort(), into which you can put a custom function, in my case i have:

function mysortfunction(a, b) {
    var x = a[3].toLowerCase();
    var y = b[3].toLowerCase();

    return ((x < y) ? -1 : ((x > y) ? 1 : 0));
}

This is fine for just sorting on the one column, namely owner_name, but how do I modify it to sort on owner_name, then publication_name?

回答1:

If owner names differ, sort by them. Otherwise, use publication name for tiebreaker.

function mysortfunction(a, b) {

  var o1 = a[3].toLowerCase();
  var o2 = b[3].toLowerCase();

  var p1 = a[1].toLowerCase();
  var p2 = b[1].toLowerCase();

  if (o1 < o2) return -1;
  if (o1 > o2) return 1;
  if (p1 < p2) return -1;
  if (p1 > p2) return 1;
  return 0;
}


回答2:

I think what you\'re looking for is thenBy.js: https://github.com/Teun/thenBy.js

It allows you to use the standard Array.sort, but with firstBy().thenBy().thenBy() style.

An example can be seen here.



回答3:

Came across a need to do SQL-style mixed asc and desc object array sorts by keys.

kennebec\'s solution above helped me get to this:

Array.prototype.keySort = function(keys) {

keys = keys || {};

// via
// https://stackoverflow.com/questions/5223/length-of-javascript-object-ie-associative-array
var obLen = function(obj) {
    var size = 0, key;
    for (key in obj) {
        if (obj.hasOwnProperty(key))
            size++;
    }
    return size;
};

// avoiding using Object.keys because I guess did it have IE8 issues?
// else var obIx = function(obj, ix){ return Object.keys(obj)[ix]; } or
// whatever
var obIx = function(obj, ix) {
    var size = 0, key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) {
            if (size == ix)
                return key;
            size++;
        }
    }
    return false;
};

var keySort = function(a, b, d) {
    d = d !== null ? d : 1;
    // a = a.toLowerCase(); // this breaks numbers
    // b = b.toLowerCase();
    if (a == b)
        return 0;
    return a > b ? 1 * d : -1 * d;
};

var KL = obLen(keys);

if (!KL)
    return this.sort(keySort);

for ( var k in keys) {
    // asc unless desc or skip
    keys[k] = 
            keys[k] == \'desc\' || keys[k] == -1  ? -1 
          : (keys[k] == \'skip\' || keys[k] === 0 ? 0 
          : 1);
}

this.sort(function(a, b) {
    var sorted = 0, ix = 0;

    while (sorted === 0 && ix < KL) {
        var k = obIx(keys, ix);
        if (k) {
            var dir = keys[k];
            sorted = keySort(a[k], b[k], dir);
            ix++;
        }
    }
    return sorted;
});
return this;
};

sample usage:

var obja = [
  {USER:\"bob\",  SCORE:2000, TIME:32,    AGE:16, COUNTRY:\"US\"},
  {USER:\"jane\", SCORE:4000, TIME:35,    AGE:16, COUNTRY:\"DE\"},
  {USER:\"tim\",  SCORE:1000, TIME:30,    AGE:17, COUNTRY:\"UK\"},
  {USER:\"mary\", SCORE:1500, TIME:31,    AGE:19, COUNTRY:\"PL\"},
  {USER:\"joe\",  SCORE:2500, TIME:33,    AGE:18, COUNTRY:\"US\"},
  {USER:\"sally\",    SCORE:2000, TIME:30,    AGE:16, COUNTRY:\"CA\"},
  {USER:\"yuri\", SCORE:3000, TIME:34,    AGE:19, COUNTRY:\"RU\"},
  {USER:\"anita\",    SCORE:2500, TIME:32,    AGE:17, COUNTRY:\"LV\"},
  {USER:\"mark\", SCORE:2000, TIME:30,    AGE:18, COUNTRY:\"DE\"},
  {USER:\"amy\",  SCORE:1500, TIME:29,    AGE:19, COUNTRY:\"UK\"}
];

var sorto = {
  SCORE:\"desc\",TIME:\"asc\", AGE:\"asc\"
};

obja.keySort(sorto);

yields the following:

 0: {     USER: jane;     SCORE: 4000;    TIME: 35;       AGE: 16;    COUNTRY: DE;   }
 1: {     USER: yuri;     SCORE: 3000;    TIME: 34;       AGE: 19;    COUNTRY: RU;   }
 2: {     USER: anita;    SCORE: 2500;    TIME: 32;       AGE: 17;    COUNTRY: LV;   }
 3: {     USER: joe;      SCORE: 2500;    TIME: 33;       AGE: 18;    COUNTRY: US;   }
 4: {     USER: sally;    SCORE: 2000;    TIME: 30;       AGE: 16;    COUNTRY: CA;   }
 5: {     USER: mark;     SCORE: 2000;    TIME: 30;       AGE: 18;    COUNTRY: DE;   }
 6: {     USER: bob;      SCORE: 2000;    TIME: 32;       AGE: 16;    COUNTRY: US;   }
 7: {     USER: amy;      SCORE: 1500;    TIME: 29;       AGE: 19;    COUNTRY: UK;   }
 8: {     USER: mary;     SCORE: 1500;    TIME: 31;       AGE: 19;    COUNTRY: PL;   }
 9: {     USER: tim;      SCORE: 1000;    TIME: 30;       AGE: 17;    COUNTRY: UK;   }
 keySort: {  }

(using a print function from here)

here is a jsbin example.

edit: cleaned up and posted as mksort.js on github.



回答4:

This is handy for alpha sorts of all sizes. Pass it the indexes you want to sort by, in order, as arguments.

Array.prototype.deepSortAlpha= function(){
    var itm, L=arguments.length, order=arguments;

    var alphaSort= function(a, b){
        a= a.toLowerCase();
        b= b.toLowerCase();
        if(a== b) return 0;
        return a> b? 1:-1;
    }
    if(!L) return this.sort(alphaSort);

    this.sort(function(a, b){
        var tem= 0,  indx=0;
        while(tem==0 && indx<L){
            itm=order[indx];
            tem= alphaSort(a[itm], b[itm]); 
            indx+=1;        
        }
        return tem;
    });
    return this;
}

var arr= [[ \"Nilesh\",\"Karmshil\"], [\"Pranjal\",\"Deka\"], [\"Susants\",\"Ghosh\"],
[\"Shiv\",\"Shankar\"], [\"Javid\",\"Ghosh\"], [\"Shaher\",\"Banu\"], [\"Javid\",\"Rashid\"]];

arr.deepSortAlpha(1,0);


回答5:

A good way to sort on many fields that are strings is to use toLocaleCompare and the boolean operator ||.

Something like:

// Sorting record releases by name and then by title.
releases.sort((oldRelease, newRelease) => {
  const compareName = oldRelease.name.localeCompare(newRelease.name);
  const compareTitle = oldRelease.title.localeCompare(newRelease.title);

  return compareName || compareTitle;
})

If you wanted to sort on more fields, you could simply chain them off the return statement with more boolean operators.



回答6:

You could concat the 2 variables together into a sortkey and use that for your comparison.

list.sort(function(a,b){
   var aCat = a.var1 + a.var2;
   var bCat = b.var1 + b.var2;
   return (aCat > bCat ? 1 : aCat < bCat ? -1 : 0);
});


回答7:

I suggest to use a built in comparer and chain the wanted sort order with logical or ||.

function customSort(a, b) {
    return a[3].localeCompare(b[3]) || a[1].localeCompare(b[1]);
}

Working example:

var array = [
    [0, \'Aluminium\', 0, \'Francis\'],
    [1, \'Argon\', 1, \'Ada\'],
    [2, \'Brom\', 2, \'John\'],
    [3, \'Cadmium\', 3, \'Marie\'],
    [4, \'Fluor\', 3, \'Marie\'],
    [5, \'Gold\', 1, \'Ada\'],
    [6, \'Kupfer\', 4, \'Ines\'],
    [7, \'Krypton\', 4, \'Joe\'],
    [8, \'Sauerstoff\', 3, \'Marie\'],
    [9, \'Zink\', 5, \'Max\']
];

array.sort(function (a, b) {
    return a[3].localeCompare(b[3]) || a[1].localeCompare(b[1]);
});

document.write(\'<pre>\');
array.forEach(function (a) {
    document.write(JSON.stringify(a) + \'<br>\');
});



回答8:

I was working with ng-grid and needed to to multiple column sorting on an array of records returned from an API, so I came up with this nifty, dynamic multi-sort function.

First of all, ng-grid fires an \"event\" for \"ngGridSorted\" and passes this structure back, describing the sort:

sortData = {
    columns:    DOM Element,
    directions: [], //Array of string values desc or asc. Each index relating to the same index of fields
    fields:     [], //Array of string values
};

So I built a function that will dynamically generate a sort function based on the sortData as shown above (Don\'t be scared by the scroll bar! It\'s only about 50 lines long! Also, I\'m sorry about the slop. It prevented a horizontal scrollbar!):

function SortingFunction(sortData)
{
    this.sortData = sortData;

    this.sort = function(a, b)
    {
        var retval = 0;

        if(this.sortData.fields.length)
        {
            var i = 0;

            /*
                Determine if there is a column that both entities (a and b)
                have that are not exactly equal. The first one that we find
                will be the column we sort on. If a valid column is not
                located, then we will return 0 (equal).
            */
            while(  (   !a.hasOwnProperty(this.sortData.fields[i]) 
                    ||  !b.hasOwnProperty(this.sortData.fields[i]) 
                    ||  (a.hasOwnProperty(this.sortData.fields[i]) 
                        && b.hasOwnProperty(this.sortData.fields[i]) 
                        && a[this.sortData.fields[i]] === b[this.sortData.fields[i]])
                    ) && i < this.sortData.fields.length){
                i++;
            }

            if(i < this.sortData.fields.length)
            {
                /*
                    A valid column was located for both entities
                    in the SortData. Now perform the sort.
                */
                if(this.sortData.directions 
                && i < this.sortData.directions.length 
                && this.sortData.directions[i] === \'desc\')
                {
                    if(a[this.sortData.fields[i]] > b[this.sortData.fields[i]])
                        retval = -1;
                    else if(a[this.sortData.fields[i]] < b[this.sortData.fields[i]])
                        retval = 1;
                }
                else
                {
                    if(a[this.sortData.fields[i]] < b[this.sortData.fields[i]])
                        retval = -1;
                    else if(a[this.sortData.fields[i]] > b[this.sortData.fields[i]])
                        retval = 1;
                }
            }
        }

        return retval;
    }.bind(this);
}

I then sort the results of my API (results) like so:

results.sort(new SortingFunction(sortData).sort);

I hope somebody else enjoys this solution as much as I do! Thanks!



回答9:

I found multisotr. This is simple, powerfull and small library for multiple sorting. I was need to sort an array of objects with dynamics sorting criteria:

const criteria = [\'name\', \'speciality\']
const data = [
  { name: \'Mike\', speciality: \'JS\', age: 22 },
  { name: \'Tom\', speciality: \'Java\', age: 30 },
  { name: \'Mike\', speciality: \'PHP\', age: 40 },
  { name: \'Abby\', speciality: \'Design\', age: 20 },
]

const sorted = multisort(data, criteria)

console.log(sorted)
<script src=\"https://cdn.rawgit.com/peterkhayes/multisort/master/multisort.js\"></script>

This library more mutch powerful, that was my case. Try it.



回答10:

function multiSort() {

    var args =$.makeArray( arguments ),
        sortOrder=1, prop=\'\', aa=\'\',  b=\'\';

    return function (a, b) {

       for (var i=0; i<args.length; i++){

         if(args[i][0]===\'-\'){
            prop=args[i].substr(1)
            sortOrder=-1
         }
         else{sortOrder=1; prop=args[i]}

         aa = a[prop].toLowerCase()
         bb = b[prop].toLowerCase()

         if (aa < bb) return -1 * sortOrder;
         if (aa > bb) return 1 * sortOrder;

       }

       return 0
    }

}
empArray.sort(multiSort( \'lastname\',\'firstname\')) Reverse with \'-lastname\'


回答11:

I had a similar problem while displaying memory pool blocks from the output of some virtual DOM h-functions composition. Basically I faced to the same problem as sorting multi-criteria data like scoring results from players around the world.

I have noticed that multi-criteria sorting is:

- sort by the first column
- if equal, sort by the second
- if equal, sort by the third
-  etc... nesting and nesting if-else

And if you don\'t care, you could fail quickly in a if-else nesting hell... like callback hell of promises...

What about if we write a \"predicate\" function to decide if which part of alternative using ? The predicate is simply :

// useful for chaining test
const decide = (test, other) => test === 0 ? other : test

Now after having written your classifying tests (byCountrySize, byAge, byGameType, byScore, byLevel...) whatever who need, you can weight your tests (1 = asc, -1 = desc, 0 = disable), put them in an array, and apply a reducing \'decide\' function like this:

const multisort = (s1, s2) => {
  const bcs = -1 * byCountrySize(s1, s2) // -1 = desc 
  const ba =  1 *byAge(s1, s2)
  const bgt = 0 * byGameType(s1, s2) // 0 = doesn\'t matter
  const bs = 1 * byScore(s1, s2)
  const bl = -1 * byLevel(s1, s2) // -1 = desc

  // ... other weights and criterias

  // array order matters !
  return [bcs, ba, bgt, bs, bl].reduce((acc, val) => decide(val, acc), 0)
}

// invoke [].sort with custom sort...
scores.sort(multisort)

And voila ! It\'s up to you to define your own criterias / weights / orders... but you get the idea. Hope this helps !

EDIT: * ensure that there is a total sorting order on each column * be aware of not having dependencies between columns orders, and no circular dependencies

if, not, sorting can be unstable !



回答12:

Try this:

t.sort( (a,b)=> a[3].localeCompare(b[3]) || a[1].localeCompare(b[1]) );

let t = [
    //[publicationID, publication_name, ownderID, owner_name ]
    [1, \'ZBC\', 3, \'John Smith\'],
    [2, \'FBC\', 5, \'Mike Tyson\'],
    [3, \'ABC\', 7, \'Donald Duck\'],
    [4, \'DBC\', 1, \'Michael Jackson\'],
    [5, \'XYZ\', 2, \'Michael Jackson\'],
    [6, \'BBC\', 4, \'Michael Jackson\'],
  ]; 
  
  // owner_name subarrray index = 3
  // publication_name subarrray index = 1

t.sort( (a,b)=> a[3].localeCompare(b[3]) || a[1].localeCompare(b[1]) );

console.log(t.join(\'\\n\'));

I assume that your data in array let t = [ [publicationID, publication_name, ownderID, owner_name ], ... ] where index of owner_name = 3 and publication_name =1.