0

I've got two arrays: hobbieList and hobbieTypes. I want to separate the elements in hobbieList, depending on what's the value of their index, in the hobbieTypes array. Something like this:

hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
hobbieTypes= ["music", "music", "sport", "music", "sport"];

... so the typical apporach with imperative programming and using fors, would be:

let musicHobbies = [];
for(let i=0;i<hobbieList.length;i++){
   if(hobbieTypes[i] === "music"){
      musicHobbies.push(hobbieList[i]);
   }
}
// Now "musicHobbies" has only the hobbies which type is "music".

But I want to do this with functional programming, using the map() function, and trying to keep the code as short and efficient as possible. Something like this:

let musicHobbies = 
     hobbieList.map( (elem, ind) => (hobbieTypes[ind] === "music") ? elem : null;

I think it looks nice, but there is a problem: the null positions are not erased, so the array shows a "null" where the condition is not satisfied.


The second apporach I could get into, which works nicely but I don't know if it's the proper way to code in a functional way, is this:

 let musicHobbies = [];
 rawHobbies.map( (elem, ind) => (hobbiesTypes[ind] === "music") ? musicHobbies.push(elem) : elem);

Is this second solution, well designed? Or should I change something? And also, should I use the array that the map() function returns (as in case 1), or is it ok if I just get my result in an external array (like in case 2)?

Thank you for your help!

EDIT:

What about this other approach?

      rawHobbies.map( (elem, ind) => {
        switch(hobbiesTypes[ind]){
           case "music":    this.hobbies.music.push(elem); break;
           case "plan":    this.hobbies.plans.push(elem); break;
           case "hobbie":  this.hobbies.hobbies.push(elem); break;
        }
      });

I guess I'd do this better with forEach. Do you think I should use this, use this with forEach, or do something like this...?

this.hobbies.music = rawHobbies.filter( (x,i) => hobbieTypes[i] == "music");
this.hobbies.plans = rawHobbies.filter( (x,i) => hobbieTypes[i] == "plan");
this.hobbies.hobbies = rawHobbies.filter( (x,i) => hobbieTypes[i] == "hobbie");

I'm not pretty sure which one of these options would work the best. Opinions?

EDIT 2:

I ended up using imperative programming for perfomance purposes, but thank you all for the different approaches I can use with functional programming. My final solution is just a simple foor loop:

     for(let i=0;i<rawHobbies.length;i++){
          switch(hobbiesTypes[i]){
           case "music":   this.hobbies.music.push(rawHobbies[i]); break;
           case "plan":    this.hobbies.plans.push(rawHobbies[i]); break;
           case "hobbie":  this.hobbies.hobbies.push(rawHobbies[i]); break;
        }
     }
3
  • why not use filter? I think its more suitable than map Commented Jan 5, 2017 at 16:00
  • Don't use the word "efficiently" unless you actually mean "efficiently". Commented Jan 5, 2017 at 16:19
  • Your last remark: "My final solution is just a simple foor loop" – this solution is terrible as the data values are hard-coded into the loop. If another type of hobby is ever added, the code would have to be adjusted. Commented Jan 6, 2017 at 5:06

5 Answers 5

4

After your edit, it looks like you want something more like groupBy.

You can achieve that with .reduce(). That should work for you:

const hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
const hobbieTypes= ["music", "music", "sport", "music", "sport"];
var groupedHobbies = hobbieTypes.reduce( (acc, item, index) => { 
        acc[item] = [...(acc[item] || []), hobbieList[index]];
        return acc;
 }, {});

console.log(groupedHobbies);


Usually you want the least side effects as possible and to not do a lot of things at the same time on your map function (which should Map/Convert/Transform each element to something new, without changing the original). By having small functions you can combine them later (you will probably read more about that if you keep following the functional approach)

I would you recommend to use the .filter() method instead of the .map().

Something like that:

hobbieList.filter((elem, ind) => hobbieTypes[ind] === "music")

which will return you an array like:

[
  "Rock",
  "Pop",
  "Blues"
]
Sign up to request clarification or add additional context in comments.

9 Comments

Read my edit! I guess that the filter is pretty nice, but for a case like this, in which I have to feed 3 different arrays with just 1... wouldn't it better to write the first of the two new solutions that I provided in my edit, but with .forEach() instead of .map() ? Since it would iterate the array just once, but in the last solution (with 3 filters), would itearte it three times. What do you think?
I just updated my answer. Reduce is quite a nice way to handle that
I don't understand how your reduce() example works :/ can you extend it a little?
Instead of the concat() method you can use the spread syntax: [...(acc[item] || []), hobbieList[index]].
But basically: we start with an empty object literal and iterate over all the hobbieTypes elements, assiging to a property with our current item value(e.g: "sport") the the value of the hobbieList that is in the same position as our current index.
|
3

I would just use the filter() method:

const hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
const hobbieTypes = ["music", "music", "sport", "music", "sport"];
console.log(hobbieList.filter((x, i) => hobbieTypes[i] === "music"));

As for your edit: I think that using filter() is much cleaner than map() (which you're actually using as forEach()). This might not be the fastest way, but it's definitely more functional.

7 Comments

@Zerok Edited.​
So with forEach() it seems to be faster, doesn't it? Since the array is iterated just once.
@Zerok Yes, but functional approaches will always be slower. If speed is the priority, use a regular loop.
@Zerok Yes, forEach() is much slower than a regular for loop. See jsperf.com/for-vs-foreach/37
@Zerok But unless you're processing a large amount of data, it doesn't really matter.
|
2

What you seem to want to do is to group hobbies by type. What should the output of this grouping process be? Creating separate variables of lists of hobbies for each type is not very scalable. It's going to be better to create an object, whose keys are types, and whose values are lists of hobbies of that type:

hobbiesByType = {
  music: ["Rock", "Pop", "Blues"],
  sport: ["Surf", "Soccer"]
};

Now I can refer to all the music hobbies as hobbiesByType.music.

What should the input to this grouping process be? Currently you have two parallel arrays:

hobbieList = ["Rock", "Pop", "Surf", "Blues", "Soccer"];
hobbieTypes= ["music", "music", "sport", "music", "sport"];

but this is not the best way to represent this information. Any time I want to check something, I have to reference both arrays. I may have to iterate across them in parallel. It would be much better to have a single representation, and a usual approach would be an array of objects:

hobbies = [
  {name: "Rock", type: "music"},
  ...
];

Now let's think about how to do the grouping. You will find many examples and approaches here on SO. Here's a very basic one using for loops:

var hobbiesByType = {};

for (var i = 0; i < hobbies.length; i++) {
  var hobby = hobbies[i];
  var hobbyName = hobby.name;
  var hobbyType = hobby.type;
  if (!(hobbyType in hobbiesByType)) hobbiesByType[hobbyType] = [];
  hobbiesByType[hobbyType].push(hobby.name);
}

But you want to use "functional" array methods. The most compact form of the above would use reduce and go like this:

hobbiesByType = hobbies.reduce((result, hobby) => {
  if (!(hobby.type in result)) result[hobby.type] = [];
  result[hobby.type].push(hobby.name);
  return result;
}, {});

That's all ES5, not ES6. You could streamline it a bit using ES6 parameter destructuring:

hobbiesByType = hobbies.reduce((result, type, name}) => {
  (result[type] = result[type] || []).push(name);
  return result;
}, {});

However, rather than creating arrays of the name of the hobby under each type, it is likely to be better to create arrays of the hobby objects themselves. Then if hobbies ever contain additional information--skillLevel perhaps--then you easily can a access that from the grouped representation.

By the way, the singular form is "hobby", not "hobbie".

1 Comment

The info comes in two arrays because of my backend structure, which is designed this way (I receive these two arrays from a Parse table). I think the best apporach is to use a regular for as an user suggested in other answer. Still, thank you for the extended explanation :)
2

Using variant data as object keys would be a terrible misuse of objects. Instead, a Map would make a much better choice for your desired data.

A zip function in combination with Array.prototype.reduce will produce a very usable result

let hobbyList = ["Rock", "Pop", "Surf", "Blues", "Soccer"]
let hobbyTypes= ["music", "music", "sport", "music", "sport"]

const zip = ([x,...xs], [y,...ys]) => {
  if (x === undefined || y === undefined)
    return []
  else
    return [[x,y], ...zip(xs, ys)]
}

let result = zip(hobbyTypes, hobbyList).reduce((acc, [type, hobby]) => {
  if (acc.has(type))
    return acc.set(type, [...acc.get(type), hobby])
  else
    return acc.set(type, [hobby])
}, new Map())

console.log(result.get('music'))
// => [ 'Rock', 'Pop', 'Blues' ]

console.log(result.get('sport'))
// => [ 'Surf', 'Soccer' ]

If you really must use an object, though you really shouldn't be, you can do so using map.entries and Array.from.

Array.from(result.entries())
  .reduce((acc, [type, hobbies]) =>
    Object.assign(acc, { [type]: hobbies }), {})

// => { music: [ 'Rock', 'Pop', 'Blues' ], sport: [ 'Surf', 'Soccer' ] }

4 Comments

I really love codes like this, but I actually don't find them any readable. I don't even understand it, but would love to. Can you comment your code a little? I would like to know how it works.
@Zerok I'd love to offer some additional help. Can you let me know some of the things you're not understanding?
const zip = ([x,...xs], [y,...ys]) --> I don't get the three points operator!
let result = zip(hobbyTypes, hobbyList).reduce((acc, [type, hobby]) => { if (acc.has(type)) return acc.set(type, [...acc.get(type), hobby]) else return acc.set(type, [hobby]) }, new Map()) --> And I don't understand this method at all.. I still don't know well how reduce() works, but aside of that, the whole syntax here, the final "new Map()" and the repetitive use of "[]" freaks me out a little.
1

Map is used to convert something in something else and not to get element that match a condition. In this case you have to use a filter function.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.