Sort object elements by string, where each string is an array

Viewed 68

I have an object where each string contains an array:

var users = {
    name   : ["joe" , "tim" , "bob" , "joe"],
    age    : [23    ,  22   ,  21   ,  20  ],
    other  : ["AA"  , "BB"  , "CC"  , "DD"]
}

How can I sort these elements, for example by age, so I get this as a result?

var users = {
    name   : ["joe" , "tim" , "bob" , "joe"],
    age    : [ 20   ,  21   ,  22   ,  23  ],
    other  : ["DD"  , "CC"  , "BB"  , "AA" ]
}

I nserted a duplicate name on purpose, since it should work in cases like these (should keep the relationship positions).

Note: In case someone else has this same issue, after reading the answers I now recommend changing the data structure to:

var users = [ // note this is now an array
    name   : {"joe" , "tim" , "bob" , "joe"},
    age    : {23    ,  22   ,  21   ,  20  },
    other  : {"AA"  , "BB"  , "CC"  , "DD"}
]

This way, more simple approaches like .sort can be used.

4 Answers

Explanation Provided all the values in the object are arrays.

  1. Firstly, duplicate the original array to prevent changing the original object.

  2. Using a while loop to find the index of the smallest passed property using the Math.min function.

  3. Splicing the index from all the keys of the passed object and pushing it into the new object.

let users = {name:["joe", "tim", "bob", "joe"], age:[23, 22, 21, 20], other  : ["AA", "BB", "CC", "DD"]};

function sortObject(obj, prop) {
  obj = {...obj}; // To avoid changing the initial object
  const keys = Object.keys(obj);
  const newObj = keys.reduce((a, b) => {
    a[b] = [];
    return a;
  }, {});

  while (obj[prop].length) {
    const index = obj[prop].findIndex(age => age == Math.min(...obj[prop]));

    keys.forEach(key => newObj[key].push(obj[key].splice(index, 1)[0]));
  }

  return newObj;
}

console.log(sortObject(users, 'age'));

One way is group the user names by age in a Map or object keyed by ages.

Then after sorting the ages, map the ages array to names array plucking the name values from the groupBy object/Map

var users = {
  name: ["bob", "joe", "tim", "bob", "joe"],
  age: [20, 23, 22, 21, 20]// added duplicate age also
}
// Map using ages as keys , empty array for values
const grouped = new Map(users.age.map((age, i) => [age, []]))
// push each name to appropriate age array in map
users.name.forEach((name, i) => grouped.get(users.age[i]).push(name));

const sortedAges = users.age.slice().sort((a, b) => a - b),
      // rebuild names array from items in Map
      sortedNames = sortedAges.map(age => grouped.get(age).shift());

const sortedUsers = {
  name:sortedNames, 
  age: sortedAges
}


for(prop in sortedUsers){
console.log(JSON.stringify(sortedUsers[prop]))
}

First, we can take the data you are starting with and create a new array of objects where we map each individual to their assigned age, and then we sort that array of objects by its age property value:

let users = {
  name: ["joe", "tim", "bob", "joe"],
  age: [23, 22, 21, 20]
};

const associateUserDetails = users => Array(users.name.length).fill().map((e,i) => ({name: users.name[i], age: users.age[i]})).sort((a,b) => a.age - b.age);

console.log(associateUserDetails(users));
/* -> [
        { name: "joe", age: 20 },
        { name: "bob", age: 21 },
        { name: "tim", age: 22 },
        { name: "joe", age: 23 }
      ]
*/

Once we this array of users sorted by their age, all that's left to do is split the array of objects back up into two nested arrays for name and age, like this:

let users = {
  name: ["joe", "tim", "bob", "joe"],
  age: [23, 22, 21, 20]
};

const associateUserDetails = users => Array(users.name.length).fill().map((e,i) => ({name: users.name[i], age: users.age[i]})).sort((a,b) => a.age - b.age);

const splitByDetail = users => users.flatMap(user => Object.entries(user)).reduce((a,c) => (a[c[0]] ? a[c[0]].push(c[1]) : a[c[0]] = [c[1]], a), {});

console.log(splitByDetail(associateUserDetails(users)));
/* -> {
        name: ["joe", "bob", "tim", "joe"],
        age: [20, 21, 22, 23]
      }
*/

Now let's combine this all together:

let users = {
  name: ["joe", "tim", "bob", "joe"],
  age: [23, 22, 21, 20]
};

const sortUsersByAge = users => Array(users.name.length).fill().map((e,i) => ({name: users.name[i], age: users.age[i]})).sort((a,b) => a.age - b.age).flatMap(user => Object.entries(user)).reduce((a,c) => (a[c[0]] ? a[c[0]].push(c[1]) : a[c[0]] = [c[1]], a), {});

console.log(sortUsersByAge(users));
/* -> {
        name: ["joe", "bob", "tim", "joe"],
        age: [20, 21, 22, 23]
      }
*/

BONUS

Now for a bonus— let's say you have an object like the one you provided and…

  • are working with many more user details, maybe even a variable number of details, and you would rather list them all explicitly
  • want the ability to sort by any of the user details listed, name (alphabetical sorting), age (numerical sorting), ID number (numerical sorting), or some other property (default to alphabetical sorting unless all values are numbers)
  • would like to add a boolean true/false switch to quickly reverse the order of any sorting function like sortUsers(users, true) where the second parameter would reverse each final array's state after sorting
  • you want to use this function as an Object.prototype method, like users.sortBy('age') instead of sortObject(users, 'age')

Let's make all of those improvements and put it all together into one prototype function Object.sortBy():

let users = {
  name: ["joe", "tim", "bob", "joe"],
  age: [23, 22, 21, 20],
  color: ["blue", "green", "red", "yellow"]
};

Object.prototype.sortBy = function(sortBy, descending = false) { return sortBy && Object.keys(this).includes(sortBy) ? [Array(this[Object.keys(this)[0]].length).fill().map((e,i) => Object.fromEntries(Object.keys(this).map(key => [key, this[key][i]])))].map(obj => obj.sort((a,b) => obj.every(user => typeof user[sortBy] === "number") ? a[sortBy] - b[sortBy] : (a[sortBy] > b[sortBy] ? 1 : -1)))[0].flatMap(user => Object.entries(user)).reduce((a,c) => (a[c[0]] ? a[c[0]].push(c[1]) : a[c[0]] = [c[1]], a), {}) : this };

users.sortBy('age');
/* -> {
        name: ["joe", "bob", "tim", "joe"],
        age: [20, 21, 22, 23],
        color: ["yellow", "red", "green", "blue"]
      }
*/

users.sortBy('name');
/* -> {
        name: ["bob", "joe", "joe", "tim"],
        age: [21, 20, 23, 22],
        color: ["red", "yellow", "blue", "green"]
      }
*/

users.sortBy('color', true); // true here will sort colors by name in descending order
/* -> {
        name: ["joe", "bob", "tim", "joe"],
        age: [20, 21, 22, 23],
        color: ["yellow", "red", "green", "blue"]
      }
*/

Now found this:

/**
 *  Sorts all arrays together with the first. Pass either a list of arrays, or a map. Any key is accepted.
 *     Array|Object arrays               [sortableArray, ...otherArrays]; {sortableArray: [], secondaryArray: [], ...}
 *     Function comparator(?,?) -> int   optional compareFunction, compatible with Array.sort(compareFunction) 
 */
function sortArrays(arrays, comparator = (a, b) => (a < b) ? -1 : (a > b) ? 1 : 0) {
  let arrayKeys = Object.keys(arrays);
  let sortableArray = Object.values(arrays)[0];
  let indexes = Object.keys(sortableArray);
  let sortedIndexes = indexes.sort((a, b) => comparator(sortableArray[a], sortableArray[b]));

  let sortByIndexes = (array, sortedIndexes) => sortedIndexes.map(sortedIndex => array[sortedIndex]);

  if (Array.isArray(arrays)) {
    return arrayKeys.map(arrayIndex => sortByIndexes(arrays[arrayIndex], sortedIndexes));
  } else {
    let sortedArrays = {};
    arrayKeys.forEach((arrayKey) => {
      sortedArrays[arrayKey] = sortByIndexes(arrays[arrayKey], sortedIndexes);
    });
    return sortedArrays;
  }
}

Example:

let people = ["john", "benny", "sally", "george"];
let peopleIds = [10, 20, 30, 40];

sortArrays([people, peopleIds]);
[["benny", "george", "john", "sally"], [20, 40, 10, 30]] // output

sortArrays({people, peopleIds});
{"people": ["benny", "george", "john", "sally"], "peopleIds": [20, 40, 10, 30]} // output

Source: https://gist.github.com/boukeversteegh/3219ffb912ac6ef7282b1f5ce7a379ad

Related