4

I am trying to sort an array of objects by a property. I run:

array.sort(function(a, b){
  var textA = a.name.toUpperCase();
  var textB = b.name.toUpperCase();
  return (textA < textB) ? -1 : (textA > textB) ? 1: 0
});

To alphabetically sort the array objects first and then I run an array.sort with a custom compare function as below:

array.sort(function(a, b){
  if(a.name === b.name){
    return -1;
  }
  return 1;
});

It seems to work with anything object that does not have a duplicate, however, as soon as there are doubles it pushes them all to the end of the array instead of just the extras.

Example:

[
  {name: 'Amy'},
  {name: 'Amy'},
  {name: 'Clark'},
  {name: 'Clark'},
  {name: 'Dan'},
  {name: 'Dave'}
  {name: 'Joe'},
  {name: 'Joe'}
]

Expected Output:

  • Amy
  • Clark
  • Dan
  • Dave
  • Joe
  • Amy
  • Clark
  • Joe

Actual Result:

  • Dan
  • Dave
  • Amy
  • Amy
  • Clark
  • Clark
  • Joe
  • Joe

Sort Code To try and get Expected Result

array.sort(function(a,b){
  if(a.name === b.name){return -1}

  return 1;
});

I have a feeling the array.sort with a compare function can handle this however I keep playing with return values of 0, -1, 1 and cannot seem to get it to work fully as I would like.

Update

Expected Result Criteria:

If an object has the same name the duplicate should go to the end of the array. For example if there are two 'Amy' one stays at the begining of the array and the duplicate goes to the end. So that all first occurrences of the names wil be at the begining of the array and all the doubles, triples etc will will be reordered each time at the end of the array. So that it could potentially arrange alhpabetical multiple items.

Example:

[
  {name: 'Amy'},
  {name: 'Amy'},
  {name: 'Clark'},
  {name: 'Clark'},
  {name: 'Clark'},
  {name: 'Dan'},
  {name: 'Dave'},
  {name: 'Joe'},
  {name: 'Joe'},
  {name: 'Joe'},
]

Expected result:

Amy Clark Dan Dave Joe Amy - Duplicate Clark - Duplicate Joe - Duplicate Clark - Had a third Joe - Had a third

As you can see it orders the first occurrence of all names alphabetically. Then orders the second occurrence alphabetically, and then the third. Until all duplicates are resolved.

After talking in comments it has come to my understanding that it cannot be done in an array.sort function alone. Sort alone with a compare function seems to be great for single or grouping doubles but not for putting doubles at the end of the array.

halfer
  • 19,824
  • 17
  • 99
  • 186
L1ghtk3ira
  • 3,021
  • 6
  • 31
  • 70
  • 1
    what is the wanted output? the expected output? – Nina Scholz Dec 29 '17 at 15:34
  • 2
    Wait why should the second "Amy" be so far down in the list? The name "Amy" should be *before* "Dan" and "Joe" in all cases, or else the question doesn't make sense. – Pointy Dec 29 '17 at 15:35
  • See https://stackoverflow.com/a/6712058/6383857 – StaticBeagle Dec 29 '17 at 15:36
  • That is only single sort alphabetically. This is alphabetical with duplicates at end of the array. – L1ghtk3ira Dec 29 '17 at 16:20
  • @L1ghtk3ira "duplicates at the end of the array" — what does that mean? Like, in exact terms, what is it that you expect? – Pointy Dec 29 '17 at 16:36
  • @Pointy: I have updated the question to try and be more explanatory as requested. Please see the 'update' section. Thanks again for you help. – L1ghtk3ira Dec 29 '17 at 16:47
  • Ah OK, so if the "count" of a particular name is greater than 1, that's all you care about; you don't need to distinguish between a name that's in the list twice from one that's in the list five times. But in that case, what should the ordering of the items with duplicates be? The code in my answer groups them by the number of duplicates. Is there some other result you want? – Pointy Dec 29 '17 at 16:51
  • Each order should be alphabetical. So first list is alphabetical, the next with first set of duplicates will be alhpebtical respected to themselves. and so on. To get the expected result provided. – L1ghtk3ira Dec 29 '17 at 17:03

5 Answers5

6

Your comparator function is incorrect. The function must:

  • Return a negative number when the first argument should sort before the second;
  • Return a positive number when the first argument should sort after the second;
  • Return zero when the two items have equivalent sort keys.

Because yours is not consistent, the sort process gets confused. For your case, the simplest thing to use is the .localeCompare() function, which returns exactly the sort of result you need:

array.sort(function(a, b) { return a.name.localeCompare(b.name); });

From your "expected output", your ordering criteria are unclear. In any case, the sort comparator, whatever it does, has to be consistent: when two items are passed to it in either order, the function should report the same ordering.

edit if the original ordering has some semantic meaning, and the "doubles" (I'd call them "duplicates") should sort further down in the array, you can add another property to each object that captures that original status:

var keyMap = {};
array.forEach(function(item) {
  if (item.name in keyMap)
    item.position = ++keyMap[item.name];
  else
    keyMap[item.name] = item.position = 1;
});

Now you can sort:

array.sort(function(a, b) {
  var c = a.position - b.position;
  if (c) return c;
  return a.name.localeCompare(b.name);
});

If the "position" values are the same, the items will be ordered by name. Items that were duplicates in the original array will be sorted after items that weren't (and triplicates will be sorted after those, etc).

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • 1
    @31piy the "expected output" doesn't make any sense without considerably more explanation from the OP. – Pointy Dec 29 '17 at 15:38
  • What is it you need to know? – L1ghtk3ira Dec 29 '17 at 15:42
  • @L1ghtk3ira what is your ordering criteria? Why would "Amy" come *after* "Dan" or "Joe"? If you were ordering the list by hand, how would you decide when one item should be before or after another? – Pointy Dec 29 '17 at 15:43
  • There are some object that will have the same name but rest of data will be different. The first of every name should come first. Any additional by the same name should be at the end of the array. – L1ghtk3ira Dec 29 '17 at 15:44
  • It is a weird criteria but not my choice. – L1ghtk3ira Dec 29 '17 at 15:45
  • @L1ghtk3ira so what you're saying is that each item should have an "occurrence count" that's derived from its position in the original array? To do that, you'd have to make a pass through the array first and explicitly add that key. Then your comparator would order by *that* as the primary key, and perform the name comparison as the *secondary* key. – Pointy Dec 29 '17 at 15:46
  • Sounds to make sense and looks to be implemented that way by @Nina Scholz. I will try and see if there's works. I am not used to making sorting algorithms so finding it a bit confusing. – L1ghtk3ira Dec 29 '17 at 15:50
  • Your solution almost works fully. My list is much longer than the one I provided. The 4th and 5th elements are duplicate beside each other. Other than that it works. So maybe issue on my side – L1ghtk3ira Dec 29 '17 at 16:01
  • @L1ghtk3ira When I run the code I posted with your sample array, I get exactly the ordering in your "expected output". – Pointy Dec 29 '17 at 16:03
  • I updated list. I get this in mine: - Amy - Joe - Dave - Clark - Clark - Dan - Amy - Joe – L1ghtk3ira Dec 29 '17 at 16:15
  • The list is almost correct but then I get a double before the last single. – L1ghtk3ira Dec 29 '17 at 16:16
  • @L1ghtk3ira your expected output now does not make sense. You'll have to do more to explain what the criteria are, because I just do not understand. Why should "Dan" come before "Clark"? – Pointy Dec 29 '17 at 16:35
  • I have updated the post and I think fixed those issues. Please look again and if you see please state which one it is in, Expected list or actual list. Thanks – L1ghtk3ira Dec 29 '17 at 16:37
  • OK, well with your new sample array my code gives "Amy Clark Dan Dave Joe Amy Clark Joe Clark Joe" – Pointy Dec 29 '17 at 16:53
  • I know the answer provided by Nina below works as expected. I gave you both an up vote as his works as expected and yours is detail oriented. If you can get yours to work the same but if possible cleaner then I will mark yours correct instead – L1ghtk3ira Dec 29 '17 at 17:08
  • @L1ghtk3ira if the original list had the first "Amy" after "Joe", you'd want that first "Amy" to come *before* the first "Joe" in the result, right? Nina's answer does not sort by names. – Pointy Dec 29 '17 at 17:37
  • I know it is sorting by occurences. Which just happens to work for any data I believe. Its working 100% for this question. I believe it will work with all arrays of objects. – L1ghtk3ira Dec 29 '17 at 17:40
  • @L1ghtk3ira from one of your comments above: *Each order should be alphabetical.* – Pointy Dec 29 '17 at 17:44
  • Yes, I still use my previous array.sort to alphabetize before running his solution. – L1ghtk3ira Dec 29 '17 at 17:48
4

You could use sorting with map by using a temporary object with a hash table for the same group array. Take from it the length of the used array as group for sorting.

The sorting happens with the group and index.

The result is mapped with index of the sorted temporary array.

Tge first part generates an array with an index of the original array and their group which is taken from pushing a value into the same group. Actually we need oly the array length after pushing of the group. If more items are in the same group, the items will be sorted later.

[
    {
        index: 0, // Amy
        group: 1
    },
    {
        index: 1, // Amy
        group: 2
    },
    {
        index: 2, // Dan
        group: 1
    },
    {
        index: 3, // Joe
        group: 1
    },
    {
        index: 4, // Joe
        group: 2
    }
]

The above given array is then sorted by group and index, both ascending.

At the last part, a new array is mapped with the index value of the sorted array.

var array = [{ name: 'Amy' }, { name: 'Amy' }, { name: 'Dan' }, { name: 'Joe' }, { name: 'Joe' }],
    groups = Object.create(null),
    result = array
        // this part is only necessary if the names should be in ascending order
        // for keeping the given order, remove the part until next comment
        .sort(function (a, b) {
            return a.name.localeCompare(b.name);
        })
        // remove until here, if necessary
        .map(function (a, i) {
            return { index: i, group: (groups[a.name] = groups[a.name] || []).push(0) };
        })
        .sort(function (a, b) {
            return a.group - b.group || a.index - b.index;
        })
        .map(function (o) {
            return array[o.index];
        });

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Example for unsorted data.

var array = [{ name: 'Joe', i: 0 }, { name: 'Dan', i: 1 }, { name: 'Amy', i: 2 }, { name: 'Joe', i: 3 }, { name: 'Amy', i: 4 }],
    groups = Object.create(null),
    result = array
        .map(function (a, i) {
            return {
                index: i,
                group: (groups[a.name] = groups[a.name] || []).push(0),
                value: a.name
            };
        })
        .sort(function (a, b) {
            return a.group - b.group || a.value.localeCompare(b.value);
        })
        .map(function (o) {
            return array[o.index];
        });

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Tested and this works. Can you provide additional information explaining it? Thanks – L1ghtk3ira Dec 29 '17 at 16:05
  • After adding more information describing how it is sorting (like how Pointy does) then I will mark as excepted answer. As it works, seems clean, and small. – L1ghtk3ira Dec 29 '17 at 16:18
  • Subtracting the `.group` arrays seems pretty weird. And I don't see how this sorts by name. – Pointy Dec 29 '17 at 17:27
  • @Pointy, it takes the order of the given names and keeps it for the same sequence. – Nina Scholz Dec 29 '17 at 17:34
  • Right, but it doesn't actually *sort* by the names. I am very confused by the sort criteria but I *think* that's part of it: so if "Amy" is *after* "Dan" in the original array, the result should have the first "Amy" *before* "Dan" in the result. – Pointy Dec 29 '17 at 17:36
  • @Pointy, in example (grey box), the names are in the wanted order but grouped by same name. the result keeps the order but only one name and moves the other of the same names to bottom (as i understand the question). – Nina Scholz Dec 29 '17 at 17:39
  • @NinaScholz from a comment from the OP: *Each order should be alphabetical.* – Pointy Dec 29 '17 at 17:45
  • @Pointy, ok. i added the wanted (or not (who knows?)) sort order as option deleting. – Nina Scholz Dec 29 '17 at 17:52
  • Nina is correct, as I already have the array alphabetized his solution goes by the occurences next and keeps the values alphabetized. To get the expected array Eg. Amy, Don, Dave, John, Don, Dave, John, Dave, John, John – L1ghtk3ira Dec 29 '17 at 17:53
  • I appreciate both of your help for this question and think there is a lot of good information for future visitors. – L1ghtk3ira Dec 29 '17 at 17:55
  • You are correct, with a simple change you can easily add the "Duplicate" and "Had a thirt" strings. – HMR Dec 29 '17 at 18:38
  • @L1ghtk3ira you cannot rely on the JavaScript `.sort()` routine to leave items in their original order when their keys are equal. The language does not stipulate that the sort is "stable", which is what that property of a sort algorithm is called. It might work *sometimes*, but it's risky to rely on it. – Pointy Dec 29 '17 at 18:39
  • you could skip the part of previous sorting and use another property in the temporary array with the name and sort it after sorting by group without sorting by index, because the value sorting gives the sorted name. it still keeps the original sorting for same named objects. – Nina Scholz Dec 29 '17 at 19:08
  • Sounds interesting although I am not sure how to implement – L1ghtk3ira Dec 30 '17 at 00:11
0

You may join duplicates first and count their occurences:

 const array = [
  {name: 'Amy'},
  {name: 'Amy'},
  {name: 'Dan'},
  {name: 'Joe'},
  {name: 'Joe'}
 ];
 const counted = [], byName = {};
 for(var {name} of array){
  if(byName[name]){
   byName[name].count++;
  }else{
   counted.push(byName[name] = {name, count:1});
  }
 }

Now that the names are unique we can sort them alphabetically:

 counted.sort((a, b) => a.name.localeCompare(b.name));

Finally, we need to spread the names again:

 const result = [];
 while(counted.length){
   for(var i = 0; i < counted.length; i++){
     const name = counted[i];
     result.push(name.name);
     name.count--;
     if(!name.count){
       counted.splice(i, 1);
       i--;
    }
  }
}
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
0
function compareSimple(a, b) {
  if (a > b) {
    return 1;
  } else if (a < b) {
    return -1;
  }
  return 0;
}

function compareAlphabetic(a, b) {
  return compareSimple(a.name.toUpperCase(), b.name.toUpperCase());
}

let input = [
  { name: 'Amy' },
  { name: 'Amy' },
  { name: 'Clark' },
  { name: 'Clark' },
  { name: 'Dan' },
  { name: 'Clark' },
  { name: 'Dave' },
  { name: 'Joe' },
  { name: 'Joe' },
];

let output = input
  .sort(compareAlphabetic)
  .reduce(function(acc, curr) {
    let rank = 0
    let prev = acc.length > 0 ? acc[acc.length-1] : null
    if (prev && compareAlphabetic(prev.value, curr) === 0) {
      rank = prev.rank + 1
    }
    acc.push({ value: curr, rank: rank });
    return acc
  }, [])
  // now we have an array like this
  // [
  // { value: Amy, rank: 0},
  // { value: Amy, rank: 1},
  // { value: Clark, rank: 0},
  // ...]
  // Now let's sort it by rank AND name
  .sort(function(a, b) {
    let result = compareSimple(a.rank, b.rank);
    if (result !== 0) {
      return result;
    }
    return compareAlphabetic(a.value, b.value);
  })
  // we have to unpack it all back:
  .map(function(obj) {
    return obj.value;
  });

console.log(output);
// [ { name: 'Amy' },
//   { name: 'Clark' },
//   { name: 'Dan' },
//   { name: 'Dave' },
//   { name: 'Joe' },
//   { name: 'Amy' },
//   { name: 'Clark' },
//   { name: 'Joe' },
//   { name: 'Clark' } ]
Hero Qu
  • 911
  • 9
  • 10
0

A little late to the party but this should definitely do it:

var arr = [
  {name: 'Amy'},
  {name: 'Amy'},
  {name: 'Clark'},
  {name: 'Clark'},
  {name: 'Clark'},
  {name: 'Dan'},
  {name: 'Dave'},
  {name: 'Joe'},
  {name: 'Joe'},
  {name: 'Joe'},
  {name: 'Joe'},
  {name: 'Joe'}
];

const o = arr.reduce(
  (acc,item)=>{
    (acc[item.name]===undefined)
      ? acc[item.name]=1
      : acc[item.name]+=1
    return acc;
  }
  ,{}
);
const highest = Object.keys(o).reduce(
  (acc,item)=>
    (o[item]>acc)
      ? o[item]
      : acc
  ,1
);
const sort = (all,level=1,results=[]) => {
  const dub = [""," - Duplicate"," - Had a third"," - Had a fourth"];
  if(level>highest){
    return results;
  }
  results = results.concat(
    all.filter(
      item=>
        //if you don't want Clark and Joe to show up 3 times:
        // level===1
        //   ? level<=o[item]
        //   : level===o[item]
        level<=o[item]
    )
    .filter((item,index,all)=>all.indexOf(item)===index)
    .sort()
    .map(x=>
      x+
      (dub[level-1]===undefined
        ? " - more than four"
        : dub[level-1]
      )
    )
  );
  return sort(all,level+1,results)
}
console.log(
  sort(
    arr.map(x=>x.name)
  )
);
HMR
  • 37,593
  • 24
  • 91
  • 160