Rename nested key in array of objects JS - javascript

Using JS i am trying to rename canBook -> quantity, variationsEN -> variations and nested keys valueEN -> value
var prod = [{
price: 10,
canBook: 1
}, {
price: 11,
canBook: 2,
variationsEN: [{
valueEN: 1
}, {
valueEN: 2
}]
}]
I was able to rename keys, but i dont have a clue how to rename the nested ones: valueEN
prod.map(p => ({
quantity: p.canBook, variations:p.variationsEN
}))

Just apply the same trick again. Replace:
variations:p.variationsEN
with:
variations:(p.variationsEN || []).map(q => ({ value: q.valueEN }))
The additional || [] is to deal with cases where the property does not exist in your source object. In that case an empty array is produced for it.

You could take a recursiv approach with an object for the renamed properties and build new object or arrays.
function rename(value) {
if (!value || typeof value !== 'object') return value;
if (Array.isArray(value)) return value.map(rename);
return Object.fromEntries(Object
.entries(value)
.map(([k, v]) => [keys[k] || k, rename(v)])
);
}
var keys = { canBook: 'quantity', variationsEN: 'variations', valueEN: 'value' },
prod = [{ price: 10, canBook: 1 }, { price: 11, canBook: 2, variationsEN: [{ valueEN: 1 }, { valueEN: 2 }] }],
result = rename(prod);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

If it's indeed a JSON (as your tag seems to indicate), e.g. you get it as string from a server, you can use a reviver function, such as:
var prod = JSON.parse(prodJSON, function (k, v) {
if (k === "canBook") {
this["quantity"] = v
} else {
return v
}
});
(Of course you could always stringify in case you start from a JS Object and not from a JSON string, but in that case it would be an overkill)

variations:p.variationsEN.map(q => { value: q.valueEN })

Related

How to rename object key based on a string of both properties and indexes generated by JSONPath

Sorry for the potentially confusing question. What I mean is this:
I've been tasked to rename a specific set of object keys on a massively nested JSON object I've converted from XML. These keys are scattered throughout the nested object, buried under both arrays and objects. Let me show you what I mean:
const obj = {
"Zone": [
{
ExtWall: [
{ Win: "10" }
],
HVACDist: {
HVACCool: [
{ Win: "10" }
]
}
}
]
};
In this case, I need to rename the key Win to ResWin. However, this tag Win is scattered across a nested object far deeper than this.
I'm currently using JSONPath to find the path of these keys. For example:
var WinPath = jp.paths(object, '$..Win');
// Returns - [
// [ '$', 'Zone', 0, 'ExtWall', 0, 'Win' ],
// [ '$', 'Zone', 0, 'HVACDist', 'HVACCool', 0, 'Win' ]
// ]
JSONPath also has a stringify function which generates a path based on the paths array it produces. Let's focus on one element for now:
const pathStringified = jp.stringify(WinPath[0])
// Returns - $['Zone'][0]['ExtWall][0]['Win']
Somehow, I need to rename this Win key to ResWin.
Right now my best guess is to replace the $ with an empty string so I can access the objec for key replacement.
My goal:
obj['Zone'][0]['ExtWall][0]['ResWin'] = obj['Zone'][0]['ExtWall][0]['Win'];
delete obj['Zone'][0]['ExtWall][0]['Win'];
Any feedback or suggestions would be much appreciated.
Also, let me know how to restructure my question because I can understand how it would be difficult to understand.
Sounds like you're ok using a library, so here is a solution using object-scan.
.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/object-scan#18.3.11/lib/index.min.js';
const obj = { Zone: [{ ExtWall: [{ Win: '10' }], HVACDist: { HVACCool: [{ Win: '10' }] } }] };
const modify = objectScan(['**.Win'], {
filterFn: ({ parent, property, value }) => {
delete parent[property];
parent.ResWin = value;
},
rtn: 'count'
});
const r = modify(obj);
console.log(r); // number of matches
// => 2
console.log(obj);
// => { Zone: [ { ExtWall: [ { ResWin: '10' } ], HVACDist: { HVACCool: [ { ResWin: '10' } ] } } ] }
</script>
Disclaimer: I'm the author of object-scan
The syntax is very similar to the JSONPath sytax, but it provides callbacks. The code could easily be generified to allow renaming of multiple keys in a single iteration.
Note that obj is modified in place. You could do a copy before modification, if that is not desired (e.g. using lodash cloneDeep)
If you want to replace all keys with same name you can iterate the entire tree.
If you want to rename by path, basically loop the path drilling down until you are at the requested key.
const obj = {
"Zone": [{
ExtWall: [{
Win: "10"
}],
HVACDist: {
HVACCool: [{
Win: "10"
}]
}
}]
};
var WinPath = [
['$', 'Zone', 0, 'ExtWall', 0, 'Win'],
['$', 'Zone', 0, 'HVACDist', 'HVACCool', 0, 'Win']
];
function rename_by_path(obj, arr_path, new_name) {
var pointer = obj;
var dollar = arr_path.shift();
var key = arr_path.shift();
while (arr_path.length) {
pointer = pointer[key]
key = arr_path.shift();
}
pointer[new_name] = pointer[key]
delete pointer[key]
}
function rename_all(obj, key_name, new_name) {
const iterate = (obj) => {
if (!obj) {
return;
}
Object.keys(obj).forEach(key => {
var value = obj[key]
if (typeof value === "object" && value !== null) {
iterate(value)
}
if (key == key_name) {
obj[new_name] = value;
delete obj[key];
}
})
}
iterate(obj)
}
function rename_by_paths(obj, arr_paths, new_name) {
arr_paths.forEach(arr_path => rename_by_path(obj, arr_path, new_name))
}
rename_by_paths(obj, WinPath, "ResWin")
console.log(obj)
rename_all(obj, "ResWin", "ResWin2");
console.log(obj)
.as-console-wrapper {
max-height: 100% !important;
}

Transform dot notation to a tree data form

I have object oriented data in the form:
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
...
]
which I would like to transform into a tree structure, to be able to serve them with a tree component (https://github.com/vinz3872/vuejs-tree in particular). The require form is the following:
var ok = [
{
text: "foo",
state: { expanded: false },
nodes: [
{
id: 1,
path: "foo.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
id: 2,
path: "foo.lol2",
text: "lol2",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
]
},
{
text: "bar",
state: { expanded: false },
nodes: [
{
id: 3,
path: "bar.lol1",
text: "lol1",
checkable: true,
state: { checked: false },
},
{
text: "barbar",
state: { expanded: false },
nodes: [
{
id: 4,
path: "bar.barbar.kk",
text: "kk",
checkable: true,
state: { checked: false },
},
]
},
]
}
]
I am aware that I should use recursion and I have tried all relevan posts in stackoverflow, i.e. How to build a JSON tree structure using object dot notation.
My main problem is that I have to somehow preserve the information of the full path to the leaves of the tree. As a newbie in js I lost myself in counters and callback for days without any luck.
I would appreciate your help.
Thank you in advance
Basically you could use forEach then split each string into array and then use reduce on that. Then you build nested object where the keys are current paths and also ad to result array.
var alist = [
'foo',
'foo.lol1',
'foo.lol2',
'bar.lol1',
'bar.barbar.kk',
]
const result = []
const levels = {
result
}
let prev = ''
let id = 1
alist.forEach(str => {
str.split('.').reduce((r, text, i, arr) => {
const path = prev += (prev.length ? '.' : '') + text
if (!r[path]) {
r[path] = {result: []}
const obj = {
id: id++,
text,
}
if (i === 0) {
obj.state = {expanded: false}
} else {
obj.state = {checked: false}
obj.checkable = true
obj.path = path
}
obj.nodes = r[path].result
r.result.push(obj)
}
if (i === arr.length - 1) {
prev = ''
}
return r[path]
}, levels)
})
console.log(result)
I found that it was easiest to do this transformation in two steps. The first converts your input into this format:
{
foo: {
lol1: {},
lol2: {}
},
bar: {
barbar: {
kk: {}
},
lol1: {}
},
}
The second uses just this format to create your desired structure. This has two advantages. First, I have tools lying around that make it easy to create this structure from your input. Second, this structure embeds enough information to create your output, with only one branching construct: whether the value at a path is an empty object or has properties. This makes the generation code relatively simple:
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number .isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const reformat = (o, path = [], nextId = ((id) => () => String (++ id)) (0)) =>
Object .entries (o) .map (([k, v]) => Object .entries (v) .length > 0
? {text: k, state: {exapanded: false}, nodes: reformat (v, [...path, k], nextId)}
: {id: nextId (), path: [...path, k] .join('.'), text: k, checkable: false, state: {checked: false}}
)
const transform = (pathTokens) =>
reformat (pathTokens
.map (s => s .split ('.'))
.reduce ((a, path) => setPath (path) ({}) (a), {})
)
const alist = ['foo', 'foo.lol1', 'foo.lol2', 'bar.lol1', 'bar.barbar.kk']
console .log (transform (alist))
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with setPath, which takes a path, in a format such as ['bar', 'barbar', 'kk'], the value to set at that path, and an object to shallow clone with this new property along that path. Thus setPath (['foo', 'bar', 'baz']) (42) ({foo: {qux: 7}, corge: 6}) yields {foo: {qux: 7, bar: {baz: 42}}, corge: 6}. (There's a little more in this reusable function to also handle array indices instead of string object paths, but we can't reach that from this input format.)
Then we have reformat, which does the format conversion. It simply builds a different input object based upon whether the input value is an empty object.
Finally, transform maps a splitting function over your input array to get the path structure needed for setPath, folds the results into an initially empty object by setting every path value to an empty object, yielding our intermediate format, which we then pas to reformat.
There is one thing I really don't like here, and that is the nextId function, which is a stateful function. We could just have easily used a generator function, but whatever we do here, we're using state to build this output and that bothers me. If someone has a cleaner suggestion for this, I'd love to hear it.

Merging array objects with same properies

i am doing one of my front end project and i have a situation where i have to merge/add objects present in the array based on some conditions. Conditions would be
Only those Objects having same label should be merged.
For objects which has same label, if object 'a' has properties which are present in b object as well, the value has to be added, else simply copy the property.
So my input would be
[
{
label: 'label-1',
published: 1,
draft: 2,
id: 'some1'
},
{
label: 'label-1',
published: 2,
status: 0,
draft: 1,
id: 'some4'
},
{
label: 'label-2',
published: 1,
draft: 14,
id: 'some2'
},
{
label: 'label-2',
published: 12,
status: 0,
draft: 14,
id: 'some3'
}
]
and the expect
[
{
label: 'label-1',
published: 3,
draft: 4,
status: 0
},
{
label: 'label-2',
published: 13,
draft: 28,
status: 0
}
]
Currently i am using the following code for achieving the same , but find it not tidy . Is there any way this could be achieved easily.
function mapData(data) {
let groupData = _.groupBy(data, 'label');
let stackBarData = [];
Object.keys(groupData).forEach((key) => {
if (groupData[key] && groupData[key].length > 0) {
let temp = Array.from(groupData[key]).reduce((a, b) => {
for (let property in b) {
if (b.hasOwnProperty(property)) {
if (property !== 'label' && property !== 'id' && property !== 'Others') {
a[property] = (a[property] || 0) + b[property];
} else {
a[property] = b[property];
}
}
}
return a;
}, {});
stackBarData.push(temp);
}
});
return stackBarData;
}
Please help.
Here is a pure ES6 function that collects the object values that are numeric, adding them up (which is what you seem to do), per unique label:
function mapData(data) {
const grouped = new Map(data.map( ({label}) => [label, { label }] ));
for (let obj of data) {
let target = grouped.get(obj.label);
for (let [key, val] of Object.entries(obj)) {
if (typeof val === 'number') {
target[key] = (target[key] || 0) + val;
}
}
}
return [...grouped.values()];
}
// Sample data
const data = [{label: 'label-1',published: 1,draft: 2,id: 'some1'},{label: 'label-1',published: 2,status: 0,draft: 1,id: 'some4'},{label: 'label-2',published: 1,draft: 14,id: 'some2'},{label: 'label-2',published: 12,status: 0,draft: 14,id: 'some3'}];
console.log(mapData(data));
.as-console-wrapper { max-height: 100% !important; top: 0; }
If you have numeric properties that you wanted to exclude, then it might be better to have an explicit set of properties you are interested in:
const props = new Set(['status', 'published', 'draft']);
// ... etc
//
if (props.has(key)) {
target[key] = (target[key] || 0) + val;
}
// ...
Lodash
_.groupBy() by the label, _.map() the groups, and merge each group using _.mergeWith(), and _.omit() the id. When merging the groups, if the current value is a number, sum the current and new values, if not return undefined - If customizer returns undefined, merging is handled by the method instead.
const arr = [{"label":"label-1","published":1,"draft":2,"id":"some1"},{"label":"label-1","published":2,"status":0,"draft":1,"id":"some4"},{"label":"label-2","published":1,"draft":14,"id":"some2"},{"label":"label-2","published":12,"status":0,"draft":14,"id":"some3"}]
const result = _(arr)
.groupBy('label')
.map((g) => _.omit(_.mergeWith({}, ...g, (objValue, srcValue) => _.isNumber(objValue) ? objValue + srcValue : undefined), 'id'))
.value()
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
ES6
Iterate the array with Array.reduce(). On each iteration check if the accumulator (the Map) has the label, and if not add an empty object with the label as the key. Iterate the current object keys with Array.forEach(), ignore id, and sum the numeric values. To get an array spread the Map.values():
const arr = [{"label":"label-1","published":1,"draft":2,"id":"some1"},{"label":"label-1","published":2,"status":0,"draft":1,"id":"some4"},{"label":"label-2","published":1,"draft":14,"id":"some2"},{"label":"label-2","published":12,"status":0,"draft":14,"id":"some3"}]
const result = [...arr.reduce((m, o) => {
m.has(o.label) || m.set(o.label, {})
const obj = m.get(o.label)
Object.keys(o).forEach((k) => {
if(k === 'id') return
obj[k] = typeof o[k] === 'number' ? (obj[k] || 0) + o[k] : o[k]
})
return m
}, new Map()).values()]
console.log(result)

How do I recursively use Array.prototype.find() while returning a single object?

The bigger problem I am trying to solve is, given this data:
var data = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4, children: [
{ id: 6 },
{ id: 7, children: [
{id: 8 },
{id: 9 }
]}
]},
{ id: 5 }
]
I want to make a function findById(data, id) that returns { id: id }. For example, findById(data, 8) should return { id: 8 }, and findById(data, 4) should return { id: 4, children: [...] }.
To implement this, I used Array.prototype.find recursively, but ran into trouble when the return keeps mashing the objects together. My implementation returns the path to the specific object.
For example, when I used findById(data, 8), it returns the path to { id: 8 }:
{ id: 4, children: [ { id: 6 }, { id: 7, children: [ { id: 8}, { id: 9] } ] }
Instead I would like it to simply return
{ id: 8 }
Implementation (Node.js v4.0.0)
jsfiddle
var data = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{ id: 4, children: [
{ id: 6 },
{ id: 7, children: [
{id: 8 },
{id: 9 }
]}
]},
{ id: 5 }
]
function findById(arr, id) {
return arr.find(a => {
if (a.children && a.children.length > 0) {
return a.id === id ? true : findById(a.children, id)
} else {
return a.id === id
}
})
return a
}
console.log(findById(data, 8)) // Should return { id: 8 }
// Instead it returns the "path" block: (to reach 8, you go 4->7->8)
//
// { id: 4,
// children: [ { id: 6 }, { id: 7, children: [ {id: 8}, {id: 9] } ] }
The problem what you have, is the bubbling of the find. If the id is found inside the nested structure, the callback tries to returns the element, which is interpreted as true, the value for the find.
The find method executes the callback function once for each element present in the array until it finds one where callback returns a true value. [MDN]
Instead of find, I would suggest to use a recursive style for the search with a short circuit if found.
var data = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4, children: [{ id: 6 }, { id: 7, children: [{ id: 8 }, { id: 9 }] }] }, { id: 5 }];
function findById(data, id) {
function iter(a) {
if (a.id === id) {
result = a;
return true;
}
return Array.isArray(a.children) && a.children.some(iter);
}
var result;
data.some(iter);
return result
}
console.log(findById(data, 8));
Let's consider the implementation based on recursive calls:
function findById(tree, nodeId) {
for (let node of tree) {
if (node.id === nodeId) return node
if (node.children) {
let desiredNode = findById(node.children, nodeId)
if (desiredNode) return desiredNode
}
}
return false
}
Usage
var data = [
{ id: 1 }, { id: 2 }, { id: 3 },
{ id: 4, children: [
{ id: 6 },
{ id: 7,
children: [
{ id: 8 },
{ id: 9 }
]}]},
{ id: 5 }
]
findById(data, 7 ) // {id: 7, children: [{id: 8}, {id: 9}]}
findById(data, 5 ) // {id: 5}
findById(data, 9 ) // {id: 9}
findById(data, 11) // false
To simplify the picture, imagine that:
you are the monkey sitting on the top of a palm tree;
and searching for a ripe banana, going down the tree
you are in the end and searches aren't satisfied you;
come back to the top of the tree and start again from the next branch;
if you tried all bananas on the tree and no one is satisfied you, you just assert that ripe bananas don't grow on this this palm;
but if the banana was found you come back to the top and get pleasure of eating it.
Now let's try apply it to our recursive algorithm:
Start iteration from the top nodes (from the top of the tree);
Return the node if it was found in the iteration (if a banana is ripe);
Go deep until item is found or there will be nothing to deep. Hold the result of searches to the variable (hold the result of searches whether it is banana or just nothing and come back to the top);
Return the searches result variable if it contains the desired node (eat the banana if it is your find, otherwise just remember not to come back down by this branch);
Keep iteration if node wasn't found (if banana wasn't found keep testing other branches);
Return false if after all iterations the desired node wasn't found (assert that ripe bananas doesn't grow on this tree).
Keep learning recursion it seems not easy at the first time, but this technique allows you to solve daily issues in elegant way.
I would just use a regular loop and recursive style search:
function findById(data, id) {
for(var i = 0; i < data.length; i++) {
if (data[i].id === id) {
return data[i];
} else if (data[i].children && data[i].children.length && typeof data[i].children === "object") {
findById(data[i].children, id);
}
}
}
//findById(data, 4) => Object {id: 4, children: Array[2]}
//findById(data, 8) => Object {id: 8}
I know this is an old question, but as another answer recently revived it, I'll another version into the mix.
I would separate out the tree traversal and testing from the actual predicate that we want to test with. I believe that this makes for much cleaner code.
A reduce-based solution could look like this:
const nestedFind = (pred) => (xs) =>
xs .reduce (
(res, x) => res ? res : pred(x) ? x : nestedFind (pred) (x.children || []),
undefined
)
const findById = (testId) =>
nestedFind (({id}) => id == testId)
const data = [{id: 1}, {id: 2}, {id: 3}, {id: 4, children: [{id: 6}, {id: 7, children: [{id: 8}, {id: 9}]}]}, {id: 5}]
console .log (findById (8) (data))
console .log (findById (4) (data))
console .log (findById (42) (data))
.as-console-wrapper {min-height: 100% !important; top: 0}
There are ways we could replace that reduce with an iteration on our main list. Something like this would do the same:
const nestedFind = (pred) => ([x = undefined, ...xs]) =>
x == undefined
? undefined
: pred (x)
? x
: nestedFind (pred) (x.children || []) || nestedFind (pred) (xs)
And we could make that tail-recursive without much effort.
While we could fold the two functions into one in either of these, and achieve shorter code, I think the flexibility offered by nestedFind will make other similar problems easier. However, if you're interested, the first one might look like this:
const findById = (id) => (xs) =>
xs .reduce (
(res, x) => res ? res : x.id === id ? x : findById (id) (x.children || []),
undefined
)
const data = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
{
id: 4,
children: [{ id: 6 }, { id: 7, children: [{ id: 8 }, { id: 9 }] }]
},
{ id: 5 }
];
// use Array.flatMap() and Optional chaining to find children
// then Filter undefined results
const findById = (id) => (arr) => {
if (!arr.length) return null;
return (
arr.find((obj) => obj.id === id) ||
findById(id)(arr.flatMap((el) => el?.children).filter(Boolean))
);
};
const findId = (id) => findById(id)(data);
console.log(findId(12)); /* null */
console.log(findId(8)); /* { id: 8 } */
Based on Purkhalo Alex solution,
I have made a modification to his function to be able to find the ID recursively based on a given dynamic property and returning whether the value you want to find or an array of indexes to recursively reach to the object or property afterwards.
This is like find and findIndex together through arrays of objects with nested arrays of objects in a given property.
findByIdRecursive(tree, nodeId, prop = '', byIndex = false, arr = []) {
for (let [index, node] of tree.entries()) {
if (node.id === nodeId) return byIndex ? [...arr, index] : node;
if (prop.length && node[prop].length) {
let found = this.findByIdRecursive(node[prop], nodeId, prop, byIndex, [
...arr,
index
]);
if (found) return found;
}
}
return false;
}
Now you can control the property and the type of finding and get the proper result.
This can be solved with reduce.
const foundItem = data.reduce(findById(8), null)
function findById (id) {
const searchFunc = (found, item) => {
const children = item.children || []
return found || (item.id === id ? item : children.reduce(searchFunc, null))
}
return searchFunc
}
You can recursively use Array.prototype.find() in combination with Array.prototype.flatMap()
const findById = (a, id, p = "children", u) =>
a.length ? a.find(o => o.id === id) || findById(a.flatMap(o => o[p] || []), id) : u;
const tree = [{id:1}, {id:2}, {id:3}, {id:4, children:[{id: 6}, {id:7, children:[{id:8}, {id:9}]}]}, {id:5}];
console.log(findById(tree, 9)); // {id:9}
console.log(findById(tree, 10)); // undefined
If one wanted to use Array.prototype.find this is the option I chose:
findById( my_big_array, id ) {
var result;
function recursiveFind( haystack_array, needle_id ) {
return haystack_array.find( element => {
if ( !Array.isArray( element ) ) {
if( element.id === needle_id ) {
result = element;
return true;
}
} else {
return recursiveFind( element, needle_id );
}
} );
}
recursiveFind( my_big_array, id );
return result;
}
You need the result variable, because without it, the function would return the top level element in the array that contains the result, instead of a reference to the deeply nested object containing the matching id, meaning you would need to then filter it out further.
Upon looking through the other answers, my approach seems very similar to Nina Scholz's but instead uses find() instead of some().
Here is a solution that is not the shortest, but divides the problem into recursive iteration and finding an item in an iterable (not necessarily an array).
You could define two generic functions:
deepIterator: a generator that traverses a forest in pre-order fashion
iFind: a finder, like Array#find, but that works on an iterable
function * deepIterator(iterable, children="children") {
if (!iterable?.[Symbol.iterator]) return;
for (let item of iterable) {
yield item;
yield * deepIterator(item?.[children], children);
}
}
function iFind(iterator, callback, thisArg) {
for (let item of iterator) if (callback.call(thisArg, item)) return item;
}
// Demo
var data = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4, children: [{ id: 6 }, { id: 7, children: [{ id: 8 }, { id: 9 }] }] }, { id: 5 }];
console.log(iFind(deepIterator(data), ({id}) => id === 8));
In my opinion, if you want to search recursively by id, it is better to use an algorithm like this one:
function findById(data, id, prop = 'children', defaultValue = null) {
for (const item of data) {
if (item.id === id) {
return item;
}
if (Array.isArray(item[prop]) && item[prop].length) {
const element = this.findById(item[prop], id, prop, defaultValue);
if (element) {
return element;
}
}
}
return defaultValue;
}
findById(data, 2);
But I strongly suggest using a more flexible function, which can search by any key-value pair/pairs:
function findRecursive(data, keyvalues, prop = 'children', defaultValue = null, _keys = null) {
const keys = _keys || Object.keys(keyvalues);
for (const item of data) {
if (keys.every(key => item[key] === keyvalues[key])) {
return item;
}
if (Array.isArray(item[prop]) && item[prop].length) {
const element = this.findRecursive(item[prop], keyvalues, prop, defaultValue, keys);
if (element) {
return element;
}
}
}
return defaultValue;
}
findRecursive(data, {id: 2});
you can use this function:
If it finds the item so the item returns. But if it doesn't find the item, tries to find the item in sublist.
list: the main/root list
keyName: the key that you need to find the result up to it for example 'id'
keyValue: the value that must be searched
subListName: the name of 'child' array
callback: your callback function which you want to execute when item is found
function recursiveSearch(
list,
keyName = 'id',
keyValue,
subListName = 'children',
callback
) {
for (let i = 0; i < list.length; i++) {
const x = list[i]
if (x[keyName] === keyValue) {
if (callback) {
callback(list, keyName, keyValue, subListName, i)
}
return x
}
if (x[subListName] && x[subListName].length > 0) {
const item = this.recursiveSearch(
x[subListName],
keyName,
keyValue,
subListName,
callback
)
if (!item) continue
return item
}
}
},
Roko C. Buljan's solution, but more readable one:
function findById(data, id, prop = 'children', defaultValue = null) {
if (!data.length) {
return defaultValue;
}
return (
data.find(el => el.id === id) ||
findById(
data.flatMap(el => el[prop] || []),
id
)
);
}

How to remove a property from nested javascript objects any level deep?

Let's say I have nested objects, like:
var obj = {
"items":[
{
"name":"Item 1",
"value": "500",
"options": [{...},{...}]
},
{
"name":"Item 2",
"value": "300",
"options": [{...},{...}]
}
],
"name": "Category",
"options": [{...},{...}]
};
I want to remove the options property from any level deep from all the objects. Objects can be nested within objects, and arrays as well.
We're currently using Lodash in the project, but I'm curious about any solutions.
There is no straight forward way to achieve this, however you can use this below function to remove a key from JSON.
function filterObject(obj, key) {
for (var i in obj) {
if (!obj.hasOwnProperty(i)) continue;
if (typeof obj[i] == 'object') {
filterObject(obj[i], key);
} else if (i == key) {
delete obj[key];
}
}
return obj;
}
and use it like
var newObject = filterObject(old_json, "option");
Modifying the above solution, To delete "dataID" which appears multiple times in my JSON . mentioned below code works fine.
var candidate = {
"__dataID__": "Y2FuZGlkYXRlOjkuOTI3NDE5MDExMDU0Mjc2",
"identity": {
"__dataID__": "aWRlbnRpdHk6NjRmcDR2cnhneGE3NGNoZA==",
"name": "Sumanth Suvarnas"
},
};
candidate = removeProp(candidate, "__dataID__")
console.log(JSON.stringify(candidate, undefined, 2));
function removeProp(obj, propToDelete) {
for (var property in obj) {
if (typeof obj[property] == "object") {
delete obj.property
let newJsonData= this.removeProp(obj[property], propToDelete);
obj[property]= newJsonData
} else {
if (property === propToDelete) {
delete obj[property];
}
}
}
return obj
}
A little modification of void's answer that allows for deletion of propertise which are also objects
function filterObject(obj, key) {
for (var i in obj) {
if (!obj.hasOwnProperty(i)) continue;
if (i == key) {
delete obj[key];
} else if (typeof obj[i] == 'object') {
filterObject(obj[i], key);
}
}
return obj;
}
We now use object-scan for data processing tasks like this. It's very powerful once you wrap your head around it. Here is how you'd answer your questions
// const objectScan = require('object-scan');
const prune = (input) => objectScan(['**.options'], {
rtn: 'count',
filterFn: ({ parent, property }) => {
delete parent[property];
}
})(input);
const obj = { items: [{ name: 'Item 1', value: '500', options: [{}, {}] }, { name: 'Item 2', value: '300', options: [{}, {}] }], name: 'Category', options: [{}, {}] };
console.log(prune(obj));
// => 3
console.log(obj);
// => { items: [ { name: 'Item 1', value: '500' }, { name: 'Item 2', value: '300' } ], name: 'Category' }
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan
I had similar issues. So, I developed the following library. Please see the source code of the library on GitHub, and you can download it using npm.
You can use the function removePropertiesDeeply together with isInitialized as below to remove all non-initialized properties (such as an empty object, empty array, empty string, or non-finite number).
const {removePropertiesDeeply, isInitialized} = require("#thedolphinos/utility4js");
const object = {
a: null,
b: "",
x: {
a: null,
b: ""
},
y: [],
z: [
null,
"",
{a: null, b: ""},
[[{a: {b: null, c: ""}}]],
"abc"
]
};
removePropertiesDeeply(object, (x) => !isInitialized(x));
console.log(JSON.stringify(object)); // See that the object becomes {"z":["abc"]}.
I had a similar issue and I got it resolved. I hope my solution might be helpful to someone.
I use Es6 ... spread operator to do a shallow copy of an object and made null to property I was not interested.
const newObject = {
...obj.items,
...obj.name,
options: null // option property will be null.
}
function omit(source) {
return isArray(source)
? source.map(omit)
: isObject(source)
? (({ options, ...rst }) => mapValues(rst, omit))(source)
: source;
}
as with lodash, that's an easy thing, also you can specify the key via an param like this
function omit(source, omitKey) {
return isArray(source)
? source.map(partialRight(omit,omitKey)))
: isObject(source)
? (({[omitKey]: _, ...rst }) => mapValues(rst, partialRight(omit,omitKey)))(source)
: source;
}
You can remove properties given a condition using following function:
// Warning: this function mutates original object
const removeProperties = (obj, condition = (key, value) => false) => {
for (var key in obj) {
const value = obj[key]
if (!obj.hasOwnProperty(key)) continue
if (typeof obj[key] === "object") {
removeProperties(obj[key], condition)
} else if (condition(key, value)) {
delete obj[key]
}
}
return obj
}
Examples:
// Remove all properties where key is equal to 'options'
removeProperties(someObject, (key, value) => key === 'options'))
// Remove all properties where key starts with 'ignore_'
removeProperties(someObject, (key, value) => key.startsWith('ignore_'))
// Remove all properties where value is null
removeProperties(someObject, (key, value) => value === null))

Categories

Resources