Search code examples
arraysfunctional-programmingjavascript-objectsramda.js

Create groups from an unstructured data array using Ramda or similar


Given an array with objects of this form:

[{data: {set: 'set1', option: 'option1'}},
 {data: {set: 'set1', option: 'option2'}},
 {data: {set: 'set1', option: 'option3'}},
 {data: {set: 'set2', option: 'optionA'}},
 {data: {set: 'set2', option: 'optionB'}}]

How can I get an array that looks like this:

[{set: 'set1', options: ['option1', 'option2', 'option3']},
 {set: 'set2', options: ['optionA', 'optionB']}]

I would like to use functional programming, Ramda or native JS methods. Thanks.


Solution

  • I like to think about questions like this in terms of a few steps that keep moving me toward my desired output. Sometimes this means that I miss a more elegant solution, but it usually makes it easier to come up with something that works.

    So let's look at your problem this way using Ramda.


    Step 1: Remove unnecessary data

    You don't want that outer data property. Since your data is a list of objects, and each one has a single data property holding the data we want, we can simply call prop('data') on each one.

    To call it on each one, we can use map, giving us map(prop('data')).

    Because this is such a common use, Ramda also supplies a function which combines map and prop this way: pluck('data'). Applying that to your input we get:

    [
      {option: "option1", set: "set1"},
      {option: "option2", set: "set1"},
      {option: "option3", set: "set1"},
      {option: "optionA", set: "set2"}
      {option: "optionB", set: "set2"}
    ]
    

    This is a good start. Now we need to think about combining them into like groups.

    Step 2: Grouping the data

    We want all the elements that share their set property to be grouped together. Ramda offers groupBy, which accepts a function that turns an item into a grouping key. We want to group by that set property, so we can use prop again, and call groupBy(prop('set')) against the previous results.

    This yields:

    {
      set1: [
        {option: "option1", set: "set1"},
        {option: "option2", set: "set1"},
        {option: "option3", set: "set1"},
      ],
      set2: [
        {option: "optionA", set: "set2"}
        {option: "optionB", set: "set2"}
      ]
    }
    

    There is redundant information in there. Somewhere we're going to need to figure that out. But I'll save that for a little bit while I try to pull together other parts.

    Step 3: Combine the options

    We've already seen pluck. It looks like we could use it on set1 and set2. Well map also works on objects, so if we simply call map(pluck('option')) on that last we get this:

    {
      set1: ["option1", "option2", "option3"], 
      set2: ["optionA", "optionB"]
    }
    

    Oh look, that also got rid of the redundancy. This is looking pretty close to the desired output.

    Step 4: Rebuilding Objects

    But now I don't see a built-in Ramda function that will get me all the way there. I could write a custom one. Or I could look to convert this in two steps. Knowing that I would like to use Ramda's zipObj function, I can first convert the above to arrays via toPairs, generating this:

    [
      ["set1", ["option1", "option2", "option3"]], 
      ["set2", ["optionA", "optionB"]]
    ]
    

    and then I can map zipObj over the results with the keys I want each property to have. That means I can call map(zipObj(['set', 'options'])) to get the final desired results:

    [
      {
        set: "set1",
        options: ["option1", "option2", "option3"]
      },
      {
        set: "set2",
        options: ["optionA", "optionB"]
      }
    ]
    

    Step 5: Putting it all together

    All right, now we have to put these together. Ramda has pipe and compose. I usually choose compose only when it fits on one line. So merging these with pipe, we can write this:

    const transform = pipe(
      pluck('data'),
      groupBy(prop('set')),
      map(pluck('option')),
      toPairs,
      map(zipObj(['set', 'options']))
    )
    

    And then just call it as

    transform(myObj)
    

    You can see this in action on the Ramda REPL. On there you can comment out later lines inside the pipe to see what the earlier ones do.

    I built the code there, adding one line at a time to the pipe until I had transformed the data. I think this is a nice way to work.