Use Ramda.js to pull off items from object - ramda.js

This question is about how to perform a task using RamdaJS.
First, assume I have an object with this structure:
let myObj = {
allItems: [
{
name: 'firstthing',
args: [
{
name: 'arg0'
},
{
name: 'arg1'
}
],
type: {
name: 'type_name_1'
}
},
{
name: 'otherthing',
args: [
{
name: 'arg0'
}
]
}
]
}
I am trying to create an object that looks like:
{
arg0: 'arg0', // myObj.allItems[0].args[0].name
typeName: 'type_name_1' // myObj.allItems[0].type.name
}
(I know the names are stupid, arg0, typeName. It's not important)
So if we weren't using Ramda, this is how I'd do it imperatively:
// The thing I'm searching for in the array (allItems)
let myName = 'firstthing';
// Here's how I'd find it in the array
let myMatch = myObj.allItems.find(item => item.name === myName);
// Here is the desired result, by manually using dot
// notation to access properties on the object (non-functional)
let myResult = {
arg0: myMatch.args[0].name,
typeName: myMatch.type.name
};
// Yields: {"arg0":"arg0","typeName":"type_name_1"}
console.log(myResult)
Finally, just for good measure, this is as far as I've gotten so far. Note that, I'd really like to accomplish this in a single compose/pipe.
(An object goes in, and an object with the desired data comes out)
const ramdaResult = R.compose(
R.path(['type', 'name']),
R.find(
R.propEq('name', myName)
)
)(R.prop('allItems', myObj))
Thanks

A combination of applySpec and path should work:
const transform = applySpec ({
arg0: path (['allItems', 0, 'args', 0, 'name']),
typeName: path (['allItems', 0, 'type', 'name'])
})
const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}
console .log (
transform (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {applySpec, path} = R </script>
But depending upon your preferences, a helper function might be useful to make a slightly simpler API:
const splitPath = useWith (path, [split('.'), identity] )
// or splitPath = curry ( (str, obj) => path (split ('.') (str), obj))
const transform = applySpec({
arg0: splitPath('allItems.0.args.0.name'),
typeName: splitPath('allItems.0.type.name'),
})
const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}
console .log (
transform (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {applySpec, path, useWith, split, identity} = R </script>
splitPath is not appropriate for Ramda, but it's a useful function I often include, especially if the paths are coming from a source outside my control.
Update
Yes, I did miss that requirement. Serves me right for looking only at the input and the requested output. There's always multiple incompatible algorithms that give the same result for a specific input. So here's my mea culpa, an attempt to break this into several reusable functions.
Lenses are probably your best bet for this. Ramda has a generic lens function, and specific ones for an object property (lensProp), for an array index(lensIndex), and for a deeper path(lensPath), but it does not include one to find a matching value in an array by id. It's not hard to make our own, though.
A lens is made by passing two functions to lens: a getter which takes the object and returns the corresponding value, and a setter which takes the new value and the object and returns an updated version of the object.
An important fact about lenses is that they compose, although for technical reasons the order in which you supply them feels opposite to what you might expect.
Here we write lensMatch which find or sets the value in the array where the value at a given path matches the supplied value. And we write applyLensSpec, which acts like applySpec but takes lenses in place of vanilla functions.
Using any lens, we have the view, set, and over functions which, respectively, get, set, and update the value. Here we only need view, so we could theoretically make a simpler version of lensMatch, but this could be a useful reusable function, so I keep it complete.
const lensMatch = (path) => (key) =>
lens
( find ( pathEq (path, key) )
, ( val
, arr
, idx = findIndex (pathEq (path, key), arr)
) =>
update (idx > -1 ? idx : length (arr), val, arr)
)
const applyLensSpec = (spec) => (obj) =>
map (lens => view (lens, obj), spec)
const lensName = (name) => lensMatch (['name']) (name)
const transform = (
name,
nameLens = compose(lensProp('allItems'), lensName(name))
) => applyLensSpec({
arg0: compose (nameLens, lensPath (['args', 0, 'name']) ),
typeName: compose (nameLens, lensPath (['type', 'name']) )
})
const myObj = {allItems: [{name: 'firstthing', args: [{name: 'arg0'}, {name: 'arg1'}], type: {name: 'type_name_1'}}, {name: 'otherthing', args: [{name: 'arg0'}]}]}
console .log (
transform ('firstthing') (myObj)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {lens, find, pathEq, findIndex, update, length, map, view, compose, lensProp, lensPath} = R </script>
While this may feel like more work than some other solutions, the main function, transform is pretty simple, and it's obvious how to extend it with additional behavior. And lensMatch and applyLensSpec are genuinely useful.

Related

Ramda - how to get multiple properties from array

I am trying to get two properties from an array of objects which looks like this:
const nodes = [
{
"id": "0",
"outgoingNodeIds": ['1', '2'],
"outgoingVirtualNodes": [
{
"virtualNodeId": "5",
},
{
"virtualNodeId": "10",
}
],
}
]
I need to get outgoingNodeIds which is a simple array of strings and outgoingVirtualNodes which is an array of objects and I need to get virtualNodeId from it.
There is no problem with getting only one property at the time and then concat two arrays (example in ramda):
const prop1 = R.pipe(
R.pluck('outgoingNodeIds'),
R.flatten
)(nodes)
const prop2 = R.pipe(
R.pluck('outgoingVirtualNodes'),
R.map(R.pluck('virtualNodeId')),
R.flatten
)(nodes)
R.concat(prop1, prop2)
But I would like to combine this into one pipe if possible. I know how to get two properties at once but I don't know how to map the second one to get virtualNodeId.
Any help would be appreciated. Thank you
Use R.ap to apply a list of functions to the array - each function produces a new array of values from all object objects in the original array, and then flatten to a single array. The end result would be an array of all outgoingNodeIds from all objects, and then the virtualNodeId from all objects:
const { pipe, ap, prop, pluck, flatten } = R
const fn = pipe(
ap([
prop('outgoingNodeIds'),
pipe(prop('outgoingVirtualNodes'), pluck('virtualNodeId'))
]),
flatten,
)
// Note that are two objects - a and b
const nodes = [{"id":"0","outgoingNodeIds":["1a","2a"],"outgoingVirtualNodes":[{"virtualNodeId":"5a"},{"virtualNodeId":"10a"}]},{"id":"1","outgoingNodeIds":["3b","4b"],"outgoingVirtualNodes":[{"virtualNodeId":"15b"},{"virtualNodeId":"20b"}]}]
const result = fn(nodes)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
If you want the outgoingNodeIds and then virtualNodeId of each object together replace R.ap with R.map and R.juxt. The R.juxt apply a list of functions to a list of parameters (a single object in this case).
const { pipe, map, juxt, prop, pluck, flatten } = R
const fn = pipe(
map(juxt([
prop('outgoingNodeIds'),
pipe(prop('outgoingVirtualNodes'), pluck('virtualNodeId'))
])),
flatten,
)
// Note that are two objects - a and b
const nodes = [{"id":"0","outgoingNodeIds":["1a","2a"],"outgoingVirtualNodes":[{"virtualNodeId":"5a"},{"virtualNodeId":"10a"}]},{"id":"1","outgoingNodeIds":["3b","4b"],"outgoingVirtualNodes":[{"virtualNodeId":"15b"},{"virtualNodeId":"20b"}]}]
const result = fn(nodes)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Since we're dealing with a single object on each iteration of R.map, you can replace R.juxt with R.applySpec:
const { pipe, map, applySpec, prop, pluck, flatten } = R
const fn = pipe(
map(applySpec([
prop('outgoingNodeIds'),
pipe(prop('outgoingVirtualNodes'), pluck('virtualNodeId'))
])),
flatten,
)
// Note that are two objects - a and b
const nodes = [{"id":"0","outgoingNodeIds":["1a","2a"],"outgoingVirtualNodes":[{"virtualNodeId":"5a"},{"virtualNodeId":"10a"}]},{"id":"1","outgoingNodeIds":["3b","4b"],"outgoingVirtualNodes":[{"virtualNodeId":"15b"},{"virtualNodeId":"20b"}]}]
const result = fn(nodes)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js" integrity="sha512-t0vPcE8ynwIFovsylwUuLPIbdhDj6fav2prN9fEu/VYBupsmrmk9x43Hvnt+Mgn2h5YPSJOk7PMo9zIeGedD1A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Ori Drori has already given several great Ramda versions. But let's not forget how simple this can be in modern JS:
const extractNodeIds = (nodes) => nodes .flatMap ((node) => [
... (node .outgoingNodeIds),
... (node .outgoingVirtualNodes .map (vn => vn .virtualNodeId))
])
const nodes = [{id: '0', outgoingNodeIds: ['1', '2'], outgoingVirtualNodes: [{virtualNodeId: '5'}, {virtualNodeId: '10'}]}]
console .log (extractNodeIds (nodes))
Ramda was designed and mostly built in the days before ES6 was ubiquitous. But these days, tools like flatMap and the array ...spread syntax make many problems that Ramda solved so nicely almost as nice in plain JS. It's not that I don't love the tool -- I'm a Ramda founder and a big fan -- but it no longer seems as necessary as it once did.

Ramda - extract object from array

I am trying to filter an array of objects with Ramda and it is working almost as I planned but I have one small issue. My result is array with one filtered object which is great but I need only object itself not array around it.
My example data set:
const principlesArray = [
{
id: 1,
harvesterId: "1",
title: "Principle1"
},
{
id: 2,
harvesterId: "2",
title: "Principle2"
},
]
And that is my Ramda query:
R.filter(R.propEq('harvesterId', '1'))(principlesArray)
As a result I get array with one filtered element but I need object itself:
[{"id":1,"harvesterId":"1","title":"Principle1"}]
Any help will be appreciated
You can use R.find instead of R.filter, to get the first object found:
const principlesArray = [{"id":1,"harvesterId":"1","title":"Principle1"},{"id":2,"harvesterId":"2","title":"Principle2"}]
const result = R.find(R.propEq('harvesterId', '1'))(principlesArray)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
A more generic approach would be to create a function that takes a predicate used by R.where, pass the partially applied R.where to R.find, and then get the results by applying the function to the array:
const { pipe, where, find, equals } = R
const fn = pipe(where, find)
const principlesArray = [{"id":1,"harvesterId":"1","title":"Principle1"},{"id":2,"harvesterId":"2","title":"Principle2"}]
const result = fn({ harvesterId: equals('1') })(principlesArray)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>

Ramda.js - how to view many values from a nested array

I have this code:
import {compose, view, lensProp, lensIndex, over, map} from "rambda";
let order = {
lineItems:[
{name:"A", total:33},
{name:"B", total:123},
{name:"C", total:777},
]
};
let lineItems = lensProp("lineItems");
let firstLineItem = lensIndex(0);
let total = lensProp("total");
My goal is to get all the totals of all the lineItems (because I want to sum them). I approached the problem incrementally like this:
console.log(view(lineItems, order)); // -> the entire lineItems array
console.log(view(compose(lineItems, firstLineItem), order)); // -> { name: 'A', total: 33 }
console.log(view(compose(lineItems, firstLineItem, total), order)); // -> 33
But I can't figure out the right expression to get back the array of totals
console.log(view(?????, order)); // -> [33,123,777]
That is my question - what goes where the ????? is?
I coded around my ignorance by doing this:
let collector = [];
function collect(t) {
collector.push(t);
}
over(lineItems, map(over(total, collect)), order);
console.log(collector); // -> [33,123,777]
But I'm sure a ramda-native knows how to do this better.
It is possible to achieve this using lenses (traversals), though will likely not be worth the additional complexity.
The idea is that we can use R.traverse with the applicative instance of a Const type as something that is composable with a lens and combines zero or more targets together.
The Const type allows you to wrap up a value that does not change when mapped over (i.e. it remains constant). How do we combine two constant values together to support the applicative ap? We require that the constant values have a monoid instance, meaning they are values that can be combined together and have some value representing an empty instance (e.g. two lists can be concatenated with the empty list being the empty instance, two numbers can be added with zero being the empty instace, etc.)
const Const = x => ({
value: x,
map: function (_) { return this },
ap: other => Const(x.concat(other.value))
})
Next we can create a function that will let us combine the lens targets in different ways, depending on the provided function that wraps the target values in some monoid instance.
const foldMapOf = (theLens, toMonoid) => thing =>
theLens(compose(Const, toMonoid))(thing).value
This function will be used like R.view and R.over, accepting a lens as its first argument and then a function for wrapping the target in an instance of the monoid that will combine the values together. Finally it accepts the thing that you want to drill into with the lens.
Next we'll create a simple helper function that can be used to create our traversal, capturing the monoid type that will be used to aggregate the final target.
const aggregate = empty => traverse(_ => Const(empty))
This is an unfortunate leak where we need to know how the end result will aggregated when composing the traversal, rather than simply knowing that it is something that needs to be traversed. Other languages can make use of static types to infer this information, but no such luck with JS without changing how lenses are defined in Ramda.
Given you mentioned that you would like to sum the targets together, we can create a monoid instance that does exactly that.
const Sum = x => ({
value: x,
concat: other => Sum(x + other.value)
})
This just says that you can wrap two numbers together and when combined, they will produce a new Sum containing the value of adding them together.
We now have everything we need to combine it all together.
const sumItemTotals = order => foldMapOf(
compose(
lensProp('lineItems'),
aggregate(Sum(0)),
lensProp('total')
),
Sum
)(order).value
sumItemTotals({
lineItems: [
{ name: "A", total: 33 },
{ name: "B", total: 123 },
{ name: "C", total: 777 }
]
}) //=> 933
If you just wanted to extract a list instead of summing them directly, we could use the monoid instance for lists instead (e.g. [].concat).
const itemTotals = foldMapOf(
compose(
lensProp('lineItems'),
aggregate([]),
lensProp('total')
),
x => [x]
)
itemTotals({
lineItems: [
{ name: "A", total: 33 },
{ name: "B", total: 123 },
{ name: "C", total: 777 }
]
}) //=> [33, 123, 777]
Based on your comments on the answer from customcommander, I think you can write this fairly simply. I don't know how you receive your schema, but if you can turn the pathway to your lineItems node into an array of strings, then you can write a fairly simple function:
const lineItemTotal = compose (sum, pluck ('total'), path)
let order = {
path: {
to: {
lineItems: [
{name: "A", total: 33},
{name: "B", total: 123},
{name: "C", total: 777},
]
}
}
}
console .log (
lineItemTotal (['path', 'to', 'lineItems'], order)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>
<script> const {compose, sum, pluck, path} = R </script>
You can wrap curry around this and call the resulting function with lineItemTotal (['path', 'to', 'lineItems']) (order), potentially saving the intermediate function for reuse.
Is there a particular reason why you want to use lenses here? Don't get me wrong; lenses are nice but they don't seem to add much value in your case.
Ultimately this is what you try to accomplish (as far as I can tell):
map(prop('total'), order.lineItems)
you can refactor this a little bit with:
const get_total = compose(map(prop('total')), propOr([], 'lineItems'));
get_total(order);
You can use R.pluck to get an array of values from an array of objects:
const order = {"lineItems":[{"name":"A","total":33},{"name":"B","total":123},{"name":"C","total":777}]};
const result = R.pluck('total', order.lineItems);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script>

Only run map once, ramda js

const arr = [{
_id: 'z11231',
_typename: 'items'
id: '123',
comment: null,
title: 'hello'
}, {
_id: 'z11231',
_typename: 'items'
id: 'qqq',
comment: 'test',
title: 'abc'
}]
Wanted output:
[['123', null, 'hello'], ['qqq', 'test', 'abc']];
export const convertObjectsWithValues = R.map(R.values);
export const removeMongoIdAndGraphqlTypeName = R.map(R.omit(['_id', '__typename']));
export const getExcelRows = R.pipe(removeMongoIdAndGraphqlTypeName, convertObjectsWithValues);
Problem here is I'm running two separate maps. It's to slow. Can I combine this in a way where only one map is executed. And still keep it clean in three seperate functions?
I'd be curious to see whether you've actually tested that it's too slow. The Knuth quote always seems a propos: "premature optimization is the root of all evil".
But if you've tested, and if multiple iterations are an actual bottleneck in your application, then the composition law of Functors should help. In Ramda terms this law states that
compose ( map (f), map (g) ) ≍ map (compose (f, g) )
and of course similarly that
pipe ( map (g), map (f) ) ≍ map (pipe (g, f) )
That means that you can rewrite your function like this:
const getExcelRows = map (pipe (omit ( ['_id', '_typename'] ), values ))
const arr = [
{_id: 'z11231', _typename: 'items', id: '123', comment: null, title: 'hello'},
{_id: 'z11231', _typename: 'items', id: 'qqq', comment: 'test', title: 'abc'}
]
console .log (
getExcelRows (arr)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script> <script>
const {map, pipe, omit, values} = R </script>
Use R.map with R.props to state which properties you want in the order that you want them. This will always maintain the correct order, unlike. R.values, which is constrained by the way JS orders keys.
const arr = [{"_id":"z11231","_typename":"items","id":"123","comment":null,"title":"hello"},{"_id":"z11231","_typename":"items","id":"qqq","comment":"test","title":"abc"}]
const getExcelRows = keys => R.map(R.props(keys))
const result = getExcelRows(['id', 'comment', 'title'])(arr)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>

Delete and add attributes with array.map and the spread operator

I'm trying to mangle data returned from an api. I've got an array of objects returned. I want to delete the password field and then add a couple of additional fields. I'd like to use the spread operator but my process feels a bit clunky.
myArray.map( item => {
const newItem = { ...item };
delete newItem.password;
newItem.saved = true;
return newItem;
});
Is there a nicer way to do this?
Given an array of objects -
const myArrayOfObjects = [
{id: 1, keyToDelete: 'nonsense'},
{id: 2, keyToDelete: 'rubbish'}
];
Delete the attribute keyToDelete, and add a new key newKey with the value "someVar".
myArrayOfObjects.map(({ keyToDelete, ...item}) => { ...item, newKey:'someVar'});
Updating the array to
[
{id: 1, newKey:'someVar'},
{id: 2, newKey:'someVar'}
]
See this great post for more information on the deletion method.