Is there a simple way to map nested data with Lodash? - javascript

For my current project, I'm working with an API that returns data formatted like this:
{
groups: [
{
items: [
{
points: [
{ name: "name1", ... },
{ name: "name2", ... },
{ name: "name3", ... },
...
],
...
},
...
]
},
...
],
...
};
I'd like to create a pure function, mapValues, that takes in an object in the above format, as well as an object mapping each name to a value, and returns the same structure, but with each point containing the value that corresponds to its name.
For example, calling mapValues(data, { name1: "value1", name2: "value2", name3: "value3" }) should return this:
{
groups: [
{
items: [
{
points: [
{ name: "name1", value: "value1", ... },
{ name: "name2", value: "value2", ... },
{ name: "name3", value: "value3", ... },
...
],
...
},
...
]
},
...
],
...
};
Here's my first pass:
function mapValues(data, values) {
return _.extend({}, data, {
groups: _.map(ui.groups, (group) => {
return _.extend({}, group, {
items: _.map(group.items, (item) => {
return _.extend({}, item, {
points: _.map(item.points, (point) => {
return _.extend({ value: values[point.name] }, point);
})
});
})
});
})
});
}
That works, but there's quite a bit of nesting a duplicate code. For my second attempt, I reached for recursion.
function mapValues(data, values) {
return (function recursiveMap(object, attributes) {
if (attributes.length === 0) { return _.extend({ value: values[object.name] }, object); }
let key = attributes[0];
return _.extend({}, object, {
[key]: _.map(object[key], child => recursiveMap(child, attributes.slice(1)))
});
})(ui, ["groups", "items", "points"]);
}
That works too, but it's difficult to read and not very concise.
Is there a cleaner way to recursively map an object using Lodash? Ideally, I'm looking for a functional approach.

Here's a way you can do it using Object.assign and no fancy functions
var data = <your data here>;
var values = <your mapped values>;
data.groups.items.points.map(p=>
Object.assign(p, {value: values[p.name]})
);
This works because arrays and objects are pass by reference. So any modifications to the values will result in the original being changed.
If you don't want to mutate your original dataset, it requires you to use {} as the first argument (to assign to a new, empty object) and show clear read/write paths for each object.
var newData = Object.assign({}, data,
{groups:
{items:
{points: data.groups.items.points.map(p=>
Object.assign({}, p, {value: values[p.name]})
)}
}
}
);

I know you wanted Lodash, but I had same issue some time ago and I've came up with this JSFiddle I am using great tool created by nervgh. It is very simple yet usefull. You can adjust this function to be pure using Object.assign, accept key parameter and tweak it however you want. You get the idea.
function mapValues(data, values) {
var iterator = new RecursiveIterator(data);
for(let {node} of iterator) {
if(node.hasOwnProperty('name')) {
node.value = values[node.name];
}
}
return data;
}

Related

How can I skip to value to specific key in JSON object?

So I am dealing with an object that looks a bit like this:
{
examples: [
{ key: 'value1' },
{ key: 'value1' },
{ key: 'value1' },
{ key: 'value2' },
{ key: 'value2' },
{ key: 'value2' }
]
}
As you can see the values are ordered. I'm trying to get where the value is value2 and I'm sure there is a more efficient way than what I am doing currently, as the actual object is significantly larger and so it takes some time to reach value2.
This is the function I created:
function getValue2 (obj, num) {
if (obj.examples[num] = "value2"){
console.log(obj.examples[num]);
}
else {
getValue2(obj, num + 1);
};
};
var x = JSON.parse(obj);
getValue2(x, 0);
Thank you in advance! :)
You can use Array#find:
returns the value of the first element in the provided array that satisfies the provided testing function. If no values satisfy the testing function, undefined is returned.
const data = {
examples: [ { key: 'value1' }, { key: 'value1' }, { key: 'value1' }, { key: 'value2' }, { key: 'value2' }, { key: 'value2' } ]
};
const res = data.examples.find(({ key }) => key === 'value2');
console.log(res);
To get the index of the first occurrence, you can use Array#findIndex:
returns the index of the first element in the array that satisfies the provided testing function. Otherwise, it returns -1, indicating that no element passed the test.
const data = {
examples: [ { key: 'value1' }, { key: 'value1' }, { key: 'value1' }, { key: 'value2' }, { key: 'value2' }, { key: 'value2' } ]
};
const index = data.examples.findIndex(({ key }) => key === 'value2');
console.log(index);
How about using Object.values, like this:
function getValue2(obj){
return Object.values(obj.examples).find(e => e === 'value2')
}
The first problem I see is you are using a single equals = to check equality when it should be == or ===.
As for alternative ways to run a similar function, I suggest you look up and learn about the for and while loops, as they are essential for JS.

spread object and conditionaly modify one property ES6

I have object as below
const myObject = {
"Obj1" :[ {
name:"test",
down:"No"
Up: "Yes",
},
{ }, {}....
],
"Obj2" :[ {}, {}......
],
"Obj3" : [ {}, {}, {....
],
}
I want to clone above object and want to modified "Obj1" if name="test" then make it as Up to "Yes"
Basically I want to conditionally spread object property.
Well the question is a bit unclear. Anyway,
if you want to conditionally update 'up' if 'name' === 'test' from a cloned object:
import { cloneDeep } from lodash/fp;
// you can use also JSON stringify + JSON parse
// spread operator will only shallow copy your object
//clone it
const deepClonedObject = cloneDeep(myObject);
// update items accordingly to your needs
deepClonedObject.obj1 = deepClonedObject.obj1.map(item => (item.name === 'test'
? { ...item, up: 'Yes' }
: item)
)
You can do it combine reduce and map. reduce because it versatile and can return object instead array and map to update properties.
const myObject = {
Obj1: [
{
name:"test",
down:"No",
Up: "No",
},
],
Obj2: [
{
name:"test",
down:"No",
Up: "No",
},
],
Obj3: [
{
name:"test2",
down:"No",
Up: "No",
},
],
};
const updatedObject = Object.keys(myObject).reduce((obj, key) => {
return {
...obj,
[key]: myObject[key].map((newObject) => {
if (newObject.name === 'test') {
newObject.Up = 'Yes';
}
return newObject;
})
};
}, {});
console.log(updatedObject);

Searching JSON deeply nested Objects with dynamic keys using Lodash?

I have a nested JSON that I'd like to search through using lodash. How can I get the root object from data if a search term I'm looking for is within certain keys, and with one of the keys being dynamic?
For example, if I have:
"data": [
{
"name": "Bob's concourse"
"activities": [
{
"day": "Monday",
"routines":
{
"Biking":
{
"details": "won 3 trophies"
"type": "road"
},
"Kayaking":
{
"details": "participated in 3 races"
"type": "rhythm"
}
}
}
}
]
},
{..other_data_etc...},
]
activities can be []; it's not guaranteed that it contains any data.
routines keys are dynamic. ie, Biking, Kayaking are dynamic strings. It can be anything.
If I want to search for an races (case insensitive), I want to search specifically in:
data.name
data.activities.routines.* (the dynamic keys)
data.activities.routines.*.details
If any one of those matches, then it will return the root object: { "name": "Bob", ..... }
I was able to get the name to return:
function searchText(collection, searchterm) {
return _.filter(collection, function(o) {
return _.includes(o.name.toLowerCase(), searchterm)
} );
};
But I'm still new to lodash, and I was unable to get any of the nested searches to return correctly, especially with the dynamic keys part.
Could anyone help explain a solution?
Expanding on your existing attempt with lodash:
const obj = {
data: [{
name: 'Bob\'s concourse',
activities: [{
day: 'Monday',
routines: {
Biking: {
details: 'won 3 trophies',
type: 'road'
},
Kayaking: {
details: 'participated in 3 races',
type: 'rhythm'
}
}
}]
}]
};
function search(str, data) {
const searchStr = str.toLowerCase();
// Only return the entries that contain a matched value
return _.filter(data, (datum) => {
// Check if name matches
return _.includes(datum.name, searchStr)
|| _.some(datum.activities, (activity) => {
return _.entries(activity.routines).some(([routine, {details}]) => {
// Check if dynamic routine matches or details
return _.includes(routine, searchStr) || _.includes(details, searchStr);
});
});
});
}
console.log(search('foobar', obj.data));
console.log(search('races', obj.data));
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.10/lodash.min.js"></script>
You can also accomplish this with plain JavaScript. Using some newish syntax such as destructuring assignment and newish native methods such as Object.entries essentially follows the same pattern as using lodash:
const obj = {
data: [{
name: 'Bob\'s concourse',
activities: [{
day: 'Monday',
routines: {
Biking: {
details: 'won 3 trophies',
type: 'road'
},
Kayaking: {
details: 'participated in 3 races',
type: 'rhythm'
}
}
}]
}]
};
function search(str, data) {
const regex = RegExp(str, 'i');
// Only return the entries that contain a matched value
return data.filter((datum) => {
// Check if name matches
return regex.test(datum.name)
|| datum.activities.some((activity) => {
return Object.entries(activity.routines).some(([routine, {details}]) => {
// Check if dynamic routine matches or details
return regex.test(routine) || regex.test(details);
});
});
});
}
console.log(search('foobar', obj.data));
console.log(search('races', obj.data));

Return all property names that share the same value in JS Object

I have an array of objects and I want to return array containing only the names of the happy people and return all names when everybody is happy.
The thing I fail to get is to get all names when everybody is happy. Any ideas?
EDIT: This is the object.
[
{ name: 'Don', disposition: 'Happy' },
{ name: 'Trev', disposition: 'Happy' },
]
function findHappyPeople(people) {
var happyPeople = Object.keys(people).filter(function(key) {
if(people[key] === 'Happy') {
return people[name]
}
});
return happyPeople;
}
You have an array of objects, so Object.keys() wouldn't be needed here.
You can use a .map() operation after the filter to end up with an array of names.
Your people[name] code isn't going to work because you have no name variable, except the global one if you're in a browser, which isn't what you want. Your data has a .name property, so use that.
const data = [
{ name: 'Don', disposition: 'Happy' },
{ name: 'Trev', disposition: 'Happy' },
]
console.log(findHappyPeople(data));
function findHappyPeople(people) {
return people
.filter(function(p) { return p.disposition === "Happy" })
.map(function(p) { return p.name });
}
Or with arrow function syntax:
const data = [
{ name: 'Don', disposition: 'Happy' },
{ name: 'Trev', disposition: 'Happy' },
]
console.log(findHappyPeople(data));
function findHappyPeople(people) {
return people
.filter(p => p.disposition === "Happy")
.map(p => p.name);
}

Making a copy of JS object an changing the occurrence of a given key

I have few JS objects. They can have any structure:
{
name: "first",
_ref_id: 1234,
spec: {
_ref_id: 2345,
data: "lots of data"
}
}
{
name: 'second',
_ref_id: 5678,
container: {
_ref_id: 6789,
children: [
{_ref_id: 3214, name: 'Bob'}
{_ref_id: 1111, name: 'Mark'}
{_ref_id: 2222, name: 'Frank'}
]
}
}
Problem:
I need to make a copies of this object but with a different _ref_ids.
Creation of the 'first' object my look like this:
first = {
name: "first",
_ref_id: uuid.v4(),
spec: {
_ref_id: uuid.v4(),
data: "lots of data"
}
}
So I know the structure of the object when I am creating it but further down the chain in a place where I am trying to make a copy of this object I don't have access and I don't know what is the structure of this object all I have is the object it self. So after coping 'first' I would like to have:
{
name: "first",
_ref_id: 8888,
spec: {
_ref_id: 9999,
data: "lots of data"
}
}
I tried instead of defining the _ref_id a simple value a sort of memoized function during the object creation:
refId(memoized = true){
var memo = {}
return () => {
if(!memoized) memo = {}
if(memo['_ref_id'])
return memo._ref_id
else {
memo._ref_id = uuid.v4()
return memo._ref_id
}
}
}
So I can create it:
first = {
name: "first",
_ref_id: refId(),
spec: {
_ref_id: refId(),
data: "lots of data"
}
}
And change the first._ref_id to first._ref_id() whenever I am trying to access the value of it.
But I have no idea how to reset the memoized variable from inside the coping function or if this is even possible?
Have anyone had similar problem? Maybe there is a different way to solve it?
PS:
I am using lodash and immutable.js in this project but I haven't found any helper functions for this particular task.
Inspired from Most elegant way to clone a JS object, with a check for _ref_id fields :
function deepCopyWithNewRefId(obj) {
if (null == obj || "object" != typeof obj) return obj;
var copy = obj.constructor();
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) {
if (attr == "_ref_id") {
copy[attr] = uuid.v4();
} else {
copy[attr] = deepCopyWithNewRefId(obj[attr]);
}
}
}
return copy;
}
A log of its use :
var uuid = { v4 : function(){ return Math.random()} };
var first = {
name: "first",
_ref_id: uuid.v4(),
spec: {
_ref_id: uuid.v4(),
data: "lots of data"
}
};
console.log(first._ref_id);
console.log(first.spec._ref_id);
var second = deepCopyWithNewRefId(first);
console.log(second);
console.log(second._ref_id);
console.log(second.spec._ref_id);
// the printed values are not the same. The rest of the object is

Categories