3

I have a doubt about grouping in Javascript.

I have an array like the one below, where each item is guaranteed to have only one of is_sweet, is_spicy, or is_bitter equal to true:

const foodsData = [{
      name: 'mie aceh',
      is_sweet: false,
      is_spicy: true,
      is_bitter: false,
    }, {
      name: 'nasi padang',
      is_sweet: false,
      is_spicy: true,
      is_bitter: false,
    }, {
      name: 'serabi',
      is_sweet: true,
      is_spicy: false,
      is_bitter: false,
    }, {
      name: 'onde-onde',
      is_sweet: true,
      is_spicy: false,
      is_bitter: false,
    }, {
      name: 'pare',
      is_sweet: false,
      is_spicy: false,
      is_bitter: true,   
}];

The output that I desire is to group an array based on is_sweet for its first element, is_bitter for its second element, and is_spicy for its third_element like the output below:

[ 
  [ 
    { name: 'serabi',
      is_sweet: true,
      is_spicy: false,
      is_bitter: false },
    { name: 'onde-onde',
      is_sweet: true,
      is_spicy: false,
      is_bitter: false } 
  ],
  [ 
    { name: 'pare',
      is_sweet: false,
      is_spicy: false,
      is_bitter: true } 
  ],
  [ 
    { name: 'mie aceh',
      is_sweet: false,
      is_spicy: true,
      is_bitter: false },
    { name: 'nasi padang',
      is_sweet: false,
      is_spicy: true,
      is_bitter: false } 
  ] 
]

I've tried to do this by writing the codes below

const isSweet = foodsData.filter(food => food.is_sweet);
const isBitter = foodsData.filter(food => food.is_bitter);
const isSpicy = foodsData.filter(food => food.is_spicy);
const newFood = [];
newFood.push(isSweet);
newFood.push(isBitter);
newFood.push(isSpicy);

Is there a way to make the same result but with cleaner code either using vanilla Javascript or Lodash?

4
  • Your question is ill-posed, imho. Your example shows entries for which only one of the three attributes is true, but you haven't specified that that's always the case. Because if that's not the case, then would would you do of an item that is bitter-sweet? Would you put it only in the sweet group? Or only in the bitter group? Or in both groups? Or in neither? Please, explain. Commented Jan 25, 2022 at 7:00
  • @Enlico hi enlico, thank you for your feedback. For your question, it's always the case indeed. I'm not really fluent in English so I try to simply my question as simple as possible and not going to detail because I'm afraid with my own terrible grammars and phrases, that said, I'll take what you said as a note for my future questions. Commented Jan 25, 2022 at 7:12
  • If that's always the case, I guess my answer is what you're looking for. Commented Jan 25, 2022 at 7:12
  • @Enlico yeah, thank you for taking your time to give me your feedback and answer Commented Jan 25, 2022 at 7:15

2 Answers 2

3

You've clarified that every item can only have one of the three properites is_sweet/is_spicy/is_bitter equal to true.

Given this assumption, one approach is that we can group the items based on the value of the quantity x.is_sweet*4 + x.is_spicy*2 + x.is_bitter*1 for the item x. This quantity is equal to 1 for those who have only is_bitter: true, 2 for item having only is_spicy: true, and 4 for item having only is_sweet: true.

Once you group according to that quantity, you can use _.values to get rid of the keys created by _.groupBy:

_.values(_.groupBy(foodsData, x => x.is_sweet*4 + x.is_spicy*2 + x.is_bitter*1))

Obviously you can give a descriptive name to the predicate:

const flavour = food => food.is_sweet*4 + food.is_spicy*2 + food.is_bitter*1;
let groupedFoods = _.values(_.groupBy(foodsData, flavour))

Notice that in case items are allowed to have more than one of those three keys true, the code above will still give sensible results, in the sense that you will have the eight groups relative to [is_sweet, is_spicy, is_bitter] equal to [0,0,0], [0,0,1], [0,1,0], [0,1,1], [1,0,0], [1,0,1], [1,1,0], and [1,1,1].

In hindsight, we don't need that the predicate flavour pulls out a number for each entry food; based on last paragraph, we are just happy if it pulls the three values food.is_sweet, food.is_spicy, and food.is_bitter, and puts them in an array. Lodash has _.over for this, so we could define

const flavour = _.over([is_sweet, is_spicy, is_bitter]);

A few words about the readability of the solution above.

  • _.groupBy is a higher level abstraction than reduce-like (member or free) functions; in other words the latter functions are much more powerful than you need for the usecase in the question, and therefore they are less expressive than the former. Indeed, _.groupBy could well be implemented in terms of reduce. Here's a brutal implementation to show that it is possible:

    const groupBy = (things, f) => {
      return things.reduce((acc, item) => {
        if (acc[f(item)] == undefined)
          acc[f(item)] = [item];
        else
          acc[f(item)].push(item);
        return acc;
      }, {});
    }
    // both these give the same result:
      groupBy([1,2,3,4,4,4], x => x % 2 == 0);  // mine
    _.groupBy([1,2,3,4,4,4], x => x % 2 == 0);  // lodash's
    
  • The solution is textually extremely short: it doesn't matter how "complicate" or "fancy" it looks; once you get used to this way of doing things (aka the right way), you end up reading the code. Look at it again:

    _.values(_.groupBy(foodsData, flavour))
    

    That's already fairly readable, but if you use lodash/fp module, you can even improve it:

    _.values(_.groupBy(flavour, foodsData))
    

    How far is that from the following English sentence?

    "get the values of the result of grouping by flavor the foods"

    Saying that's not readable just means denying the reality of things.

  • Longer solutions using lower lever constructs like for loops or (a bit better) reduce-like utilities force you to parse the code, instead. There's no way you understand what those solutions do without carefully inspecting their bodies. Even if you decided to pull the function (acc, item) => { … } out of the call to reduce and give it a name, ending up with input.reduce(fun, acc) how would you even name it? It is a function which pushes elements on top of the arrays in an intermediate result. Except in trivial use cases, you're not gonna find a good name for such a mouthful. You're just moving the problem somewhere else. People reading this code will still be puzzled.

  • _.over is maybe a bit scary too:

    const flavour = _.over([is_sweet, is_spicy, is_bitter]);
    

    But really, what does it cost tha you just learn and accept what _.over means? I mean, it is just like learning a new language, nothing more. And the explanation of _.over is fairly simple:

    _.over([iteratees=[_.identity]])

    Creates a function that invokes iteratees with the arguments it receives and returns their results.

    It is so simple: _.over(['a', 'b', 'd'])({a: 1, b: 2, c: 3}) == [1, 2, undefined]

Sign up to request clarification or add additional context in comments.

1 Comment

Wow, that's a nice explanation you write there, I understand better now why it's written the way it's written. Thank you.
1

While the other two answers are correct, I personally find this the most readable.

For your example input:

const input = [
  {
    name: "mie aceh",
    is_sweet: false,
    is_spicy: true,
    is_bitter: false,
  },
  ...
];

You can call reduce on the array. Reducing the array, iterates over the items in an array, reducing those values into an accumulator (acc). Your accumulator can be any value, and depends on what you want your output to be. In this case, I think it an object, mapping the name of your groups (isSweet, isSpicy, etc..) to an array works the best.

const { isSweet, isSpicy, isBitter } = input.reduce(
  (acc, item) => {
    if (item.is_sweet) {
      acc.isSweet.push(item);
    } else if (item.is_spicy) {
      acc.isSpicy.push(item);
    } else if (item.is_bitter) {
      acc.isBitter.push(item);
    }
    return acc;
  },
  {
    isSweet: [],
    isSpicy: [],
    isBitter: [],
  }
);

Then you can use the object spread operator to get the groups as variables.

2 Comments

More readable? You're essentially reinventing _.groupBy with the lower level reduce utitility, and _.values with the unpacking upon assingment.
@Enlico this solution doesn't require lodash

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.