Sorting an array of categories and subcategories

Viewed 5156

Suppose we have and array of objects where each object represents a category or a subcategory, like this one:

var data = [
    {type: "parent-category", order: 1, categoryId: 1},
    {type: "parent-category", order: 2, categoryId: 2},
    {type: "child-category", order: 1, categoryId: 3, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 4, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 5, parentCategoryId: 2},
    {type: "child-category", order: 1, categoryId: 6, parentCategoryId: 2}
];

This array represent an example with two categories, where each one of they also have two subcategories.

Now, I'm facing with the problem of sort this array on some custom way. As you can note, the order attribute inside each object is related to the position (or ordering) of the object inside his level of hierarchy. And the relation between categories and subcategories is defined by the atributte parentCategoryId.

The expected sorting I need using as example the previous array of data should be this one:

var data = [
    {type: "parent-category", order: 1, categoryId: 1},
    {type: "child-category", order: 1, categoryId: 3, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 4, parentCategoryId: 1},
    {type: "parent-category", order: 2, categoryId: 2},
    {type: "child-category", order: 1, categoryId: 6, parentCategoryId: 2},
    {type: "child-category", order: 2, categoryId: 5, parentCategoryId: 2}   
];

My current approach to solve this problem is based on creating a map to numbers, analyzing the values of the attributes with the following conditions:

  • For an object that has the type parent-category we assign the value of categoryId * 1000.
  • For an object that has the class child-category we assign the value of (parentCategoryId * 1000) + order

This logic is shown on the next sort implementation:

let data = [
    {type: "parent-category", order: 1, categoryId: 1},
    {type: "parent-category", order: 2, categoryId: 2},
    {type: "child-category", order: 1, categoryId: 3, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 4, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 5, parentCategoryId: 2},
    {type: "child-category", order: 1, categoryId: 6, parentCategoryId: 2}
];

let orderedData = data.sort((a, b) =>
{
    var aCat = (a.type == "parent-category") ? a.categoryId : a.parentCategoryId;
    var aOrder = (a.type == "parent-category") ? 0 : a.order;
    var bCat = (b.type == "parent-category") ? b.categoryId : b.parentCategoryId;
    var bOrder = (b.type == "parent-category") ? 0 : b.order;

    return (aCat * 1000 + aOrder) - (bCat * 1000 + bOrder);
});

console.log(orderedData);

However, and ignoring the fact that the previous implementation works, my question is if there exists a better approach or alternative to solve this problem. I don't like the idea of depend on the mapping to numbers, because, for example, the previous implementation introduces a limitation on the numbers of subcategories (999 in this case) I can sort correctly under each category. Thanks!

5 Answers

Simple performant solution using Array#filter Array#sort and Array#map

var data=[{type:"parent-category",order:1,categoryId:1},{type:"parent-category",order:2,categoryId:2},{type:"child-category",order:1,categoryId:3,parentCategoryId:1},{type:"child-category",order:2,categoryId:4,parentCategoryId:1},{type:"child-category",order:2,categoryId:5,parentCategoryId:2},{type:"child-category",order:1,categoryId:6,parentCategoryId:2}]

let res = data
  .filter(({type}) => type === "parent-category")
  .sort((a,b) => a.order - b.order)
  .reduce((acc, curr) =>{
    const children = data
      .filter(({parentCategoryId}) => parentCategoryId === curr.categoryId)
      .sort((a,b) => a.order - b.order);

    acc.push(curr, ...children);
    return acc;
  }, []);

console.log(res);

If performance is not an issue, I'd prefer using a more step-by-step way:

/* ORIGINAL DATA */
const data = [
    {type: "parent-category", order: 1, categoryId: 1},
    {type: "parent-category", order: 2, categoryId: 2},
    {type: "child-category", order: 1, categoryId: 3, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 4, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 5, parentCategoryId: 2},
    {type: "child-category", order: 1, categoryId: 6, parentCategoryId: 2}
];


const sorted = [];
const byOrder = (a, b) => a.order - b.order;

// Get and sort parents first
const parents = data.filter(obj => obj.type === 'parent-category');
parents.sort(byOrder);

// Push to resulting array by parent order
parents.forEach(obj => {
  sorted.push(obj);
  
  // Get and sort corresponding children objs
  const children = data.filter(o => o.parentCategoryId === obj.categoryId);
  children.sort(byOrder);
  
  // Push the objs into resulting array together
  sorted.push.apply(sorted, children);
});


console.log(sorted);

This approach involves more steps, but is straightforward and can be easily understood compared to your complex sorting logic.

Using external libraries for only one function is overkill.

Based on @kemicofa answer. Just changed it to support more than two levels

function iterative(categories, parentCategory, level = 0) {
  return categories
    .filter((category) => parentCategory ? category.parent === parentCategory : !category.parent)
    .map(category => {
       category.level = level;
       return category;
    })
    .sort((a,b) => a.name.localeCompare(b.name))
    .reduce((acc, curr) =>{
      const children = iterative(categories, curr.id, level+1)

      acc.push(curr, ...children);
      return acc;
    }, [])
  ;
}

For easy way, use lodash#orderBy


If you don't prefer lodash, you can define your own specific function in sort

I have not fully understand your sorting criteria, but, for example,

function sort_categories(cat1,cat2) {
  if (cat1["order"] < cat2["order"])
    return -1;
  if (cat1["order"] > cat2["order"])
    return 1;
  return 0;
}

data.sort(sort_categories);

will simply sort by orders.

For details about return values, take a look at here

If compareFunction is supplied, all non-undefined array elements are sorted according to the return value of the compare function (all undefined elements are sorted to the end of the array, with no call to compareFunction). If a and b are two elements being compared, then:

  • If compareFunction(a, b) is less than 0, sort a to an index lower than b (i.e. a comes first).

  • If compareFunction(a, b) returns 0, leave a and b unchanged with respect to each other, but sorted with respect to all different elements. Note: the ECMAscript standard does not guarantee this behaviour, and thus not all browsers (e.g. Mozilla versions dating back to at least 2003) respect this.

  • If compareFunction(a, b) is greater than 0, sort b to an index lower than a (i.e. b comes first).

  • compareFunction(a, b) must always return the same value when given a specific pair of elements a and b as its two arguments. If inconsistent results are returned then the sort order is undefined.

This will work. You sort your categoryId first and then do your math by running the forEach function.

var data = [
    {type: "parent-category", order: 1, categoryId: 1},
    {type: "parent-category", order: 2, categoryId: 2},
    {type: "child-category", order: 1, categoryId: 3, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 4, parentCategoryId: 1},
    {type: "child-category", order: 2, categoryId: 5, parentCategoryId: 2},
    {type: "child-category", order: 1, categoryId: 6, parentCategoryId: 2}
];

data.sort( function(a, b){
    return a.categoryId - b.categoryId;
}).forEach( function(itm){  
    itm.categoryId = itm.categoryId * 1000;
    if( itm.parentCategoryId){
        itm.parentCategoryId = itm.parentCategoryId * 1000 + itm.order
    }
});

console.log(data);

Or in ES6 fashion...

let orderedData = data.sort( (a, b) => {
       return a.categoryId - b.categoryId;
    }).forEach( itm => {
        itm.categoryId = itm.categoryId * 1000;
        if( itm.parentCategoryId){
            itm.parentCategoryId = itm.parentCategoryId * 1000 + itm.order
        }
    });
Related