Jason Joseph Nathan
Jason Joseph Nathan

Reputation: 7601

How to append to an array after cherry picking keys of another using Ramda

I have an existing array of objects

const oldData = [
  {'one': 1, 'two': 2},
  {'one': 3, 'two': 4}
];

I have a new array of objects:

const newData = [
  {'three': 5, 'two': 6, 'one': 7},
  {'five': 8, 'one': 9, 'two': 10}, 
];

I have an array containing object props I want to extract (this is a variable)

const columnMeta = ['one', 'two'];

I'd like to create a composable, reusable function using Ramda. This function should extract only the chosen columnMeta properties of newArray and append that with whatever that existed in oldArray

The following works but seems a tad bit too verbose (and hardly reusable..)

// simple enough, join two arrays using the spread operator
const appendData = curry((curr, prev) => [...prev, ...curr]);

// get a subset of an object, given an array of keys
const extractByColumnMeta = curry((k, obj) => zipObj(k, props(k)(obj)));

// create a composable function that takes an array of keys
const mapDataByColumns = compose(map, extractByColumnMeta)(columnMeta);

// compose a new function that would take a the newData & oldData
const mergeDataAfterMap = compose(appendData, mapDataByColumns);

// works, but I'm sure I can do better :)
mergeDataAfterMap(newData)(oldData);

// => [{one: 1, two: 2}, {one: 3, two: 4}, {one: 7, two: 6}, {one: 9, two: 10}]

Non ramda way

const oldData = [
  {'one': 1, 'two': 2},
  {'one': 3, 'two': 4}
];

const newData = [
  {'three': 5, 'two': 6, 'one': 7},
  {'five': 8, 'one': 9, 'two': 10}, 
];

const columnMeta = ['one', 'two'];


const mapDataToColumns = (fields, oldData, newData) => {
    // a new array. anything in oldData is overwritten
    return [...oldData, ...newData.map(row => {
        // reduce to return only wanted keys
        return fields.reduce((acc, field) => {
          // add to accumulator or return
          return row.hasOwnProperty(field) 
            ? {...acc, [field]:row[field]}
            : acc;
        }, {});
    })];
}

console.log(mapDataToColumns(columnMeta, oldData, newData));

Upvotes: 0

Views: 734

Answers (4)

Jason Joseph Nathan
Jason Joseph Nathan

Reputation: 7601

Building upon @ScottSauyet's answer and comments to @naomik's answer, I came up with this:

const pickAndMerge = compose(chain, project);

And then it's simply,

const oldData = [
  {'one': 1, 'two': 2},
  {'one': 3, 'two': 4}
];

const newData = [
  {'three': 5, 'two': 6, 'one': 7},
  {'five': 8, 'one': 9, 'two': 10}, 
];

const columnMeta = ['one', 'two'];

const pickAndMerge = R.compose(R.chain, R.project);

console.log(pickAndMerge(columnMeta)([oldData, newData]));
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.20.0/ramda.min.js"></script>

Upvotes: 1

jgr0
jgr0

Reputation: 727

How about:

const mergeSelectAttr = R.curry((oldData, columnMeta, newData) => 
  R.compose(
    R.concat(oldData), 
    R.map(R.pick(columnMeta))
  )(newData) 
)

Ramda fiddle

I would say this yields to a more flexible api than the current answers, you may call it as per your convenience:

  • mergeSelectAttr(oldData, columnMeta, newData)
  • mergeSelectAttr(oldData)(columnMeta, newData)
  • mergeSelectAttr(oldData, columnMeta)(newData)

One major advantage you get it with this solution would work even if you get your newData at a later stage, say from an api call / DB result, it would still work great due to currying. You can of course also change the order of oldData and columnMeta depending on what you are getting first

Eg:

// You have olData and coulmnMeta now, but not newData:
const merger = mergeSelectAttr(oldData, columnMeta)
    .
    .
    .
    .
// A little while later, when you have access to newData:

const finalValue = merger(newData)

Upvotes: 2

Mulan
Mulan

Reputation: 135416

You just need to compose R.pick with R.concat, but in a somewhat tricky way. You're trying to compose a unary function, R.pick(columnMenta), with a binary function, R.concat.

const compose2 = R.compose(R.compose, R.compose)

const foo = compose2(R.map(R.pick(['one', 'two'])), R.concat)

console.log(foo(oldData, newData))
// => [{"one":1,"two":2},{"one":3,"two":4},{"one":7,"two":6}, {"one":9,"two":10}]

The arbitrary limitation of two inputs makes this a little weak tho, and most people don't like compose2 trickery. What if you had several lists of input data and you wanted to combine them in a sensible way? Enter R.chain

const foo = R.chain(R.map(R.pick(['one', 'two'])))

console.log(foo([oldData, newData, oldData]))
// => [{"one":1,"two":2},{"one":3,"two":4},{"one":7,"two":6},{"one":9,"two":10},{"one":1,"two":2},{"one":3,"two":4}]

Note foo now expects an array of inputs which effectively allows you to combine as many data sets as you need


OK, it seems you also want the columnMeta to be an input to the function. It will be easiest if we adapt the latter answer

const foo = R.curry((columns, xxs) =>
  R.chain(R.map(R.pick(columns)), xxs))

console.log(foo(['one'], [oldData, newData, oldData]))
// => [{"one":1},{"one":3},{"one":7},{"one":9},{"one":1},{"one":3}]

console.log(foo(['two'], [oldData, newData, oldData]))
// => [{"two":2},{"two":4},{"two":6},{"two":10},{"two":2},{"two":4}]

console.log(foo(['one', 'two'], [oldData, newData, oldData]))
// => [{"one":1,"two":2},{"one":3,"two":4},{"one":7,"two":6},{"one":9,"two":10},{"one":1,"two":2},{"one":3,"two":4}]

Upvotes: 1

Scott Sauyet
Scott Sauyet

Reputation: 50807

If you don't mind this API:

mergeAndPick(columnMeta, [oldData, newData]);
//=> [{"one": 1, "two": 2}, {"one": 3, "two": 4}, 
//    {"one": 7, "two": 6}, {"one": 9, "two": 10}]

Then you can write it simply like this:

const mergeAndPick = R.useWith(R.project, [R.identity, R.unnest]);

If you really want this:

mergeDataAfterMap(columnMeta, oldData, newData);
//=> [{"one": 1, "two": 2}, {"one": 3, "two": 4}, 
//    {"one": 7, "two": 6}, {"one": 9, "two": 10}]

Then you could build it on top of the above.

const mergeDataAfterMap = (columnMeta, oldData, newData) => 
    mergeAndPick(columnMeta, [oldData, newData]);

But the first one is more flexible, allowing you to specify any number of lists to combine.

This is built on top of a fairly unusual Ramda function, useWith, which makes it easier to combine functions in a points-free manner. In the days of ES6, it's less of an obvious advantage, and that could be rewritten as

const mergeAndPick = R.curry((columnMeta, lists) => 
    R.project(columnMeta, R.unnest(lists)));

You can see all this in action on the Ramda REPL.

Upvotes: 1

Related Questions