0

This is my array, I have an object and then a count of how many repeats Id have for example the first object has the Id 2 repeated 3 times.

[{
  Id: 1,
  Info: "Info",
  Category: [
    { Id: 2, count: 3 },
    { Id: 4, count: 1 },
    { Id: 8, count: 1 },
    { Id: 18, count: 1 },
    { Id: 9, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 2,
  Info: "Info 2",
  Category: [
    { Id: 2, count: 3 },
    { Id: 9, count: 2 },
    { Id: 21, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 3,
  Info: "Info 3",
  Category: [
    { Id: 4, count: 1 }, 
    { Id: 11, count: 1 }, 
    { Id: 9, count: 1 },
  ],
}]

Now I need to order this array based on an Id for example the number "9" so if the first object has the Maximus count of the id 9 of all it will be the first and the others whit minus count would be bellow, like this, the number 9 will be a random number.

[{
  Id: 2,
  Info: "Info 2",
  Category: [
    { Id: 2, count: 3 },
    { Id: 9, count: 2 },
    { Id: 21, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 1,
  Info: "Info",
  Category: [
    { Id: 2, count: 3 },
    { Id: 4, count: 1 },
    { Id: 8, count: 1 },
    { Id: 18, count: 1 },
    { Id: 9, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 3,
  Info: "Info 3",
  Category: [
    { Id: 4, count: 1 }, 
    { Id: 11, count: 1 }, 
    { Id: 9, count: 1 },
  ],
}]
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37

3 Answers3

2

Using Array.prototype.sort one needs to write a function which compares two array/list items according to the OP's requirements.

Such a comparator is expected to return a number value either grater than Zero or lower than Zero or Zero itself in case of item equality.

Thus one needs to find two different counts, one count for each item which will be found by searching an item's Category array by an additionally provided id value.

In order to keep the compare function reusable it is implemented as a function which allows a context to be bound to it which in the OP's case is an object that features the id one is looking for ... e.g. something like ... { id: 9 } or { id: 4 } ...

function compareByBoundIdCountOfItemCategoryList(a, b) {
  const { id } = this;

  const aCount = a.Category.find(ctgry => ctgry.Id === id)?.count ?? -1;
  const bCount = b.Category.find(ctgry => ctgry.Id === id)?.count ?? -1; 

  // in case of equal counts compare the `Category` array's lengths'.
  return (bCount - aCount) || (b.Category.length - a.Category.length);
}

const sampleList = [{
  Id: 1,
  Info: "Info",
  Category: [
    { Id: 2, count: 3 },
    { Id: 4, count: 1 },
    { Id: 8, count: 1 },
    { Id: 18, count: 1 },
    { Id: 9, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 2,
  Info: "Info 2",
  Category: [
    { Id: 2, count: 3 },
    { Id: 9, count: 2 },
    { Id: 21, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 3,
  Info: "Info 3",
  Category: [
    { Id: 4, count: 1 }, 
    { Id: 11, count: 1 }, 
    { Id: 9, count: 1 },
  ],
}];

console.log(
  '{ id: 9 } ...',
  sampleList
    .sort(compareByBoundIdCountOfItemCategoryList.bind({ id: 9 }))
);
console.log(
  '{ id: 4 } ...',
  sampleList
    .sort(compareByBoundIdCountOfItemCategoryList.bind({ id: 4 }))
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

As one of the comments does point out, the above code requires a version of at least node 14.0.0 due to the function compareByBoundIdCountOfItemCategoryList which uses both the Optional Chaining Operator / ?. and the Nullish Coalescing Operator / ??.

In order to let the script not break one has to replace the line ...

... aCount = a.Category.find(ctgry => ctgry.Id === id)?.count ?? -1;

... with this alternative ...

... aCount = (a.Category.find(ctgry => ctgry.Id === id) || { count: -1 }).count;

function compareByBoundIdCountOfItemCategoryList(a, b) {
  const { id } = this;

  const aCount = (
    a.Category.find(ctgry => ctgry.Id === id) ||
    { count: -1 }
  ).count;

  const bCount = (
    b.Category.find(ctgry => ctgry.Id === id) ||
    { count: -1 }
  ).count; 

  // in case of equal counts compare the `Category` array's lengths'.
  return (bCount - aCount) || (b.Category.length - a.Category.length);
}

const sampleList = [{
  Id: 1,
  Info: "Info",
  Category: [
    { Id: 2, count: 3 },
    { Id: 4, count: 1 },
    { Id: 8, count: 1 },
    { Id: 18, count: 1 },
    { Id: 9, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 2,
  Info: "Info 2",
  Category: [
    { Id: 2, count: 3 },
    { Id: 9, count: 2 },
    { Id: 21, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 3,
  Info: "Info 3",
  Category: [
    { Id: 4, count: 1 }, 
    { Id: 11, count: 1 }, 
    { Id: 9, count: 1 },
  ],
}];

console.log(
  '{ id: 9 } ...',
  sampleList
    .sort(compareByBoundIdCountOfItemCategoryList.bind({ id: 9 }))
);
console.log(
  '{ id: 4 } ...',
  sampleList
    .sort(compareByBoundIdCountOfItemCategoryList.bind({ id: 4 }))
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • i got a sintax error in const aCount = a.Category.find(ctgry => ctgry.Id === id)?.count ?? -1; SyntaxError: Unexpected token '.' – Luis Monsalve Feb 05 '21 at 16:41
  • @LuisDanielMonsalveRuiz ...thats due to the JavaScript environment you're running the script in (btw which one is it?, cause the above snippet works perfectly fine in the currently available up to date browsers). I will provide an alternative version which will run in JS engines which are neither capable of the [*Optional Chaining Operator / `?.`*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) nor of the [*Nullish Coalescing Operator / `??`*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) – Peter Seliger Feb 05 '21 at 16:57
  • i am using node in v12.18.4 – Luis Monsalve Feb 05 '21 at 16:58
  • @LuisDanielMonsalveRuiz ... both operators are available from **node v14.0.0** ... I will provide a second version, give me some minutes. – Peter Seliger Feb 05 '21 at 17:01
  • i can update to node 14.0.0 no problem men and then i will try – Luis Monsalve Feb 05 '21 at 17:03
  • 1
    I'm curious why you would choose to use `bind` for this rather than a simple closure. Is there a strong reason? I would expect something more like `const byCategoryId = (id) => (a, b) => {/* compare using a, b, and id */}` used like `sampleList .sort (byCategoryId (9))` – Scott Sauyet Feb 05 '21 at 19:34
  • 1
    @ScottSauyet ... 1/2 ... I'm not quite sure, but I think it is in parts due to (1) having no problem of using from a math point of view a less elegant *weapon* (rather sabre than rapier) because of (2) also knowing other programmers being not comfortable/familiar with a tool chain, based on higher order functions (functions returning functions) and (3 / matter of taste) being opportunistic enough to take advantage of baptizing such a function with a precise enough, long but boring name like `compareByBoundIdCountOfItemCategoryList`. ... – Peter Seliger Feb 05 '21 at 20:16
  • 1
    @ScottSauyet ... 2/2 ... Most of the JS programmers I met so far had more an OO than a FP background. They also tend to know `bind`. Thus it is easier for them to comprehend such a function as a method that's target object (this context) is configurable. Thus your *rapier approach* and my *sabre one* do have one thing in common ... both functions are factories, each creating the compare function for (most probably a one time use by) `sort`. Yours by directly invoking it like with e.g. `(byCategoryId (9))`, mine more indirectly via invoking `bind`. – Peter Seliger Feb 05 '21 at 20:17
  • Interesting. I would assume an OO-focused choice would look more like `new CategoryCountSorter (9) .sort (sampleList)` and that `bind`, which is all about restricting the context and arguments of an otherwise free function, would be more of an anathema to OO folks. But I've been a decade+ away from that world. Regardless, it's an interesting approach, one I would never have thought of. – Scott Sauyet Feb 05 '21 at 20:34
  • @ScottSauyet ...OO does not need classes, just objects and methods. And JS's core features are as clearly arranged as they are beauti- and powerful ...mostly it's all about functions ...functions as first class citizens, functions as closures (hence encapsulation), functions with (bound) context (hence methods), functions as factories and constructors, and ...killer feature ...delegation ...and there are two kinds of it; once explicitly via `call`/`apply` and secondly the build in automatic prototypal delegation (hence inheritance). Did I mention, I love this language for it's clear simplicity – Peter Seliger Feb 05 '21 at 20:55
  • Yes, the language, for all its warts, does have an elegant core. And I do like the prototypal inheritance JS borrowed from Self. I have nothing against your approach, and would love to see how it applies to other scenarios, but it seems in this case to be more cumbersome than my alternative without offering any additional familiarity for OO developers. If programming with functions interests you, check out Reg Braithwaite's [JavaScript Allongé](https://leanpub.com/javascript-allonge) or the [ES6 updated version](https://leanpub.com/javascriptallongesix). Highly recommended! – Scott Sauyet Feb 05 '21 at 21:09
  • 1
    @ScottSauyet ... thanks, Reginald is amongst the authors I'm constantly following over the years, what also applies to *Ramda* from its beginning. – Peter Seliger Feb 05 '21 at 21:57
2

I find this format a simpler alternative to the answer from Peter Seliger. We simply store the sought id in a closure when creating the comparator function we pass to sort:

const byCategoryCount = (categoryId) => ({Category: c1}, {Category: c2}) => 
  (c2 .find (({Id}) => Id === categoryId) ?.count ?? -1) - 
  (c1 .find (({Id}) => Id === categoryId) ?.count ?? -1)

const input = [{Id: 1, Info: "Info", Category: [{Id: 2, count: 3}, {Id: 4, count: 1}, {Id: 8, count: 1}, {Id: 18, count: 1}, {Id: 9, count: 1}, {Id: 3, count: 1}]}, {Id: 2, Info: "Info 2", Category: [{Id: 2, count: 3}, {Id: 9, count: 2}, {Id: 21, count: 1}, {Id: 3, count: 1}]}, {Id: 3, Info: "Info 3", Category: [{Id: 4, count: 1}, {Id: 11, count: 1}, {Id: 9, count: 1}]}]

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

If you don't have the nullish coalescing operator available in your environment, this variant is not much worse:

const byCategoryCount = (categoryId) => ({Category: c1}, {Category: c2}) => 
  (c2 .find (({Id}) => Id === categoryId) || {count: -1}) .count -
  (c1 .find (({Id}) => Id === categoryId) || {count: -1}) .count

We could also choose to write a wrapper function that returns a sorted version without mutating the original list. It might look like this:

const sortByCategoryCount = (categoryId, xs) =>
  [... xs] .sort (byCategoryCount (categoryId))

But at that point we might start to wonder whether the helper function is offering us anything and we might choose to refactor to

const sortByCategoryCount = (categoryId, xs) =>
  [... xs] .sort (({Category: c1}, {Category: c2}) => 
    (c2 .find (({Id}) => Id === categoryId) || {count: -1}).count -
    (c1 .find (({Id}) => Id === categoryId) || {count: -1}).count
  )
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • I like the explanation. Hopefully you will lure more people into FP concepts. – Peter Seliger Feb 05 '21 at 20:28
  • 1
    @Peter: That is one of my goals, I guess. I started a functional programming library ([Ramda](https://ramdajs.com)) some years ago to help me work in an FP manner, and it's become fairly popular. I also teach/mentor junior devs, and gently push them toward FP solutions. – Scott Sauyet Feb 05 '21 at 20:37
0

This should work for you sortByCount:

var my_arr = [{
  Id: 1,
  Info: "Info",
  Category: [
    { Id: 2, count: 3 },
    { Id: 4, count: 1 },
    { Id: 8, count: 1 },
    { Id: 18, count: 1 },
    { Id: 9, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 2,
  Info: "Info 2",
  Category: [
    { Id: 2, count: 3 },
    { Id: 9, count: 2 },
    { Id: 21, count: 1 },
    { Id: 3, count: 1 },
  ],
}, {
  Id: 3,
  Info: "Info 3",
  Category: [
    { Id: 4, count: 1 }, 
    { Id: 11, count: 1 }, 
    { Id: 9, count: 1 },
  ],
}];

function sortByCount(arr, targetId){
  var arr_temp = [];
  arr.forEach(el => {
    var elem = el.Category.filter(e => e.Id === targetId)[0];
    var value = elem ? elem.count : -1;
    arr_temp.push({
      value: value,
      obj: el
    });
  });
  arr_temp.sort((a,b)=> b.value - a.value);
  return arr_temp.map(el => el.obj);  
}
var sortedArr = sortByCount(my_arr, 9);

console.log(sortedArr)
Anarion
  • 1,048
  • 6
  • 16