4

this is not a duplicate. please see my comment below!

does somebody knows an more efficient solution than for loops in ES6?

I have written the following, which lacks of performance. Any improvement ideas? Highly appreciated.

Basically i have an object regarding cars and an array about user preferences. Expected behavior is to push all relevant car names into an array.

Users can give any amount of preferences. A car name should be only pushed, if ALL specifications are mentioned in preferences. Therefore some preferences will be "leftovers".

For that reason in the following example Honda appears, but not BMW, which is the expected (but very slow behavior).

// Car objects
const cars = [{
    name: "Honda",
    category: "eco",
    specs: {
      0: "green",
      1: "fast",
      2: "automatic"
    }
  },
  {
    name: "BMW",
    category: "sport",
    specs: {
      0: "blue",
      1: "fast",
      2: "automatic"
    }
  }
]

// User preferences
const preferences = ["green", "fast", "4x4", "automatic", "panorama"]

// function to get length/amount of car specifications
function objsize(Myobj) {
  var osize = 0,
    key;
  for (key in Myobj) {
    if (Myobj.hasOwnProperty(key)) osize++;
  }
  return Object(osize);
};


//function to check if ALL specifications are included in the user preferences
function checkSpecs(spec_item) {
  return preferences.includes(spec_item)
}

// main function
function filter_func() {

  //final results
  let matched_cars = []


  for (i = 0; i < objsize(cars); i++) {

    let specs_collector = []

    for (j = 0; j < objsize(cars[i].specs); j++) {
      specs_collector.push(cars[i].specs[j])
    }

    if (specs_collector.every(checkSpecs) === true) {
      matched_cars.push(cars[i].name)
      specs_collector = []
    }

  }
  console.log(matched_cars)
}

filter_func()
Umut885
  • 165
  • 1
  • 15
  • 2
    Possible duplicate of [Check if every element in one array is in a second array](https://stackoverflow.com/questions/8628059/check-if-every-element-in-one-array-is-in-a-second-array) – Heretic Monkey Oct 16 '18 at 23:29
  • @HereticMonkey please remove "possible duplicate label". The initial title name was for another question on stackoverflow. I am looking for an more efficient way than for loops. The suggested solution uses for loops. – Umut885 Oct 16 '18 at 23:41
  • 2
    *"I have written the following, which lacks of performance."* What's the performance? How did you measure it? Whenever you have a collection values that needs to be processed, you have to use some kind of loop to process each value, whether it is explicit or implicit. – Felix Kling Oct 16 '18 at 23:49
  • Is this strictly performance-related? What is the desired behavior exactly? – Dom Oct 16 '18 at 23:52
  • @Dom exactly. The desired solution is without any or at least 1 "for loop" (if possible)which are slow by design. I guess there are more efficient built in ES6 features which i couldn't figure out yet. – Umut885 Oct 16 '18 at 23:54
  • 1
    FWIW, `objsize` is unnecessary and wrong. `Object(osize)` creates an a Number object, which means that `5 === Object(5)` is `false`. To iterate over an array, use `for(var i = 0; i < array.length; i++)` or `for (var item of arr)` or `arr.forEach(function(item) {})`). To iterate over an object, use `for (var prop in obj)`. But it looks like `cars[i].specs` should really be an array, not an object. – Felix Kling Oct 16 '18 at 23:54
  • 2
    *"The desired solution is without any "for loops" which are slow by design."* They are not slow "by design". What makes you think that? `for` loops are heavily optimized in browsers. Of course a loop might not be the "best" solution for a problem, but that's a different issue. – Felix Kling Oct 16 '18 at 23:56
  • Cache objsize before for loop if you insist on using it, maybe you could use Object.keys().length instead? – PeS Oct 16 '18 at 23:56
  • Another example `specs_collector.every` also loops over the array. Do you want to remove that call as well? – Felix Kling Oct 16 '18 at 23:58
  • In relational algebra terms that is the "division operator". I don't think there is a general purpose efficient algorithm to calculate it but in many domains you can find a pretty good algorithm. https://pdfs.semanticscholar.org/9211/3556cef6e582357c26c2458e0ab8b66267d8.pdf – SQL Hacks Oct 16 '18 at 23:59
  • Use of `specs_collector` seems unnecessary. You seem to be just copying `cars[i].specs` into a new array. You can leave our that loop and just do `if (cars[i].specs.every(checkSpecs)) { ... }`. My point is: before you start claiming that some parts of the language are "slow", verify that your own logic is optimal. – Felix Kling Oct 17 '18 at 00:00
  • @Felix Kling. Thank you so much for your help. Currently i am trying your suggestion "for (var prop in obj)" but still the objects are stored in an array. I got really confused now. Could you please update my snippet? Highly appreciated. – Umut885 Oct 17 '18 at 00:02
  • @FelixKling You are right. I just started with JavaScript and i know there is a lot to learn. I feel kinda sorry for my question. I expected my snippet has improvement potential. I didn't want to claim about a language at all. Just wanted to know if there is a better solution from someone experienced. – Umut885 Oct 17 '18 at 00:05

3 Answers3

9

You can't really avoid looking at every car and you can't avoid looking at every spec in the car because you want to test each of those. You can avoid looping over the preferences every time by using a Set.

So this may or may not be faster, but it's much simpler and much easier to understand because the code almost reads like English: filter cars where every spec is in the preferences:

// Car objects
const cars = [{
    name: "Honda",
    category: "eco",
    specs: ["green", "fast","automatic"]
    },
  {
    name: "BMW",
    category: "sport",
    specs: ["blue", "fast","automatic"]
    }
]

const preferences = new Set(["green", "fast", "4x4", "automatic", "panorama"])

let filtered = cars.filter(car => car.specs.every(spec => preferences.has(spec)))
console.log(filtered)
Mark
  • 90,562
  • 7
  • 108
  • 148
  • "...this may or may not be faster" it is `preferences.length` times faster than the original because of the `Set`. – slider Oct 17 '18 at 00:08
  • @slider, yes…but sometimes the es6 functions like `every` don't perform as well as a simple `for` loop in practice. I suppose I mentioned that because I didn't want to make any claims that I hadn't tested. – Mark Oct 17 '18 at 00:11
  • @MarkMeyer your snippet is beautiful and faster. awesome. I just tried it out in my code and it works like a charm and it "feels" indeed faster. I will try to measure my solution and compare it to yours. Afterwards I will post the results. Thanks! – Umut885 Oct 17 '18 at 00:40
2

-- EDIT --

Using the data in the OP:

const array_intersect = (a, b) => a.filter( i => (b.indexOf(i) >= 0) )
const a_contains_b = (a, b) => array_intersect(a, b).length == b.length

var cars = [{
    name: "Honda",
    category: "eco",
    specs: ["green", "fast", "automatic"]
  },
  {
    name: "BMW",
    category: "sport",
    specs: ["blue", "fast", "automatic"]
  }
]

const preferences = ["green", "fast", "4x4", "automatic", "panorama"]

let filtered = cars.filter(car => a_contains_b(preferences, car.specs))
console.log(filtered);
Yoshimitsu
  • 370
  • 2
  • 9
  • Although you don't have `for` loops, your `array_intersect` function is *O(n^2)*, so not any more efficient. – slider Oct 17 '18 at 00:04
  • 1
    @slider in principle, you are right. But the only way to know is to test in similar conditions this will be used, as tests have shown there are widely divergent results. Check https://github.com/dg92/Performance-Analysis-JS – Yoshimitsu Oct 17 '18 at 01:24
2

There is no way to escape at least one loop. You always have to loop through all the cars, be it with a for... or with another construct like array.filter(). But there is another way to gain performance. You can use bitmasks. This would require changing the data structure of the car object so that each car already contains the bitmask corresponding to its specs, and when the user chooses the desired specs, likewise the spec codes should be added. (However, I suspect this might be too much hassle for little gain.)

// Let's pretend there are preset binary digits corresponding 
// to each one of the available preferences:
//
//     "blue" => 1
//     "green" => 2
//     "red" => 4
//     "fast" => 8
//     "slow" => 16
//     "automatic" => 32
//     "4x4"  => 64
//     "panorama" => 128
//
// You would encode this into the data before processing

var cars = [{
    name: "Honda",
    category: "eco",
    specs: ["green", "fast", "automatic"],
    bin_specs: 42 // 2 + 8 + 32
  },
  {
    name: "BMW",
    category: "sport",
    specs: ["blue", "fast", "automatic"],
    bin_specs: 41 // 1 + 8 + 32
  }
]

const preferences = ["green", "fast", "4x4", "automatic", "panorama"]
const bin_preferences = 234 // 2 + 8 + 64 + 32 + 128]

let filtered = cars.filter(car => (car.bin_specs & bin_preferences) === car.bin_specs)

console.log(filtered);
Yoshimitsu
  • 370
  • 2
  • 9
  • Thats a really interesting answer but only possible if I do know every possible user preference element . In my case ist would not work but surely for other use cases. Thanks for that. – Umut885 Oct 17 '18 at 07:38
  • Yes, you must have these values pre-assigned to each preference If you can't control the backend, then forget about it. – Yoshimitsu Oct 17 '18 at 08:44