Related
I have input data like this:
[{
"name": "outField2",
"value": "something"
}, {
"name": "outField3[index].outField4",
"value": "something"
}, {
"name": "outField3[index].outField5",
"value": "something"
}, {
"name": "outField3[index].outField6.outField7",
"value": "something"
}]
I am trying to achieve an output like this based on substring '[index]' (i.e. if that substring is not present then that element should be an object instead of an array):
{
"outField2": "something",
"outField3[index]": [{
"outField4": "something",
"outField5": "something",
"outField6": {
"outField7": "something"
}
}]
}
My current code (below) is able to produce the outField3 as an object if there is no substring '[index]' but I'm unable to find a good solution to generate it as an array in the presence of the substring. Can someone help out? I've tried a few options but none gives me the desired result.
function doThis(item, index) {
let path = map[index].name.split(".");
if (path.length > 1) {
createNestedObject(mapOutput, path, map[index].value);
} else {
mapOutput[map[index].name] = map[index].value;
};
};
function createNestedObject(element, path, value) {
var lastElement = arguments.length === 3 ? path.pop() : false;
for (var i = 0; i < path.length; i++) {
if (path[i].includes('[index]')) {
/*some logic here to push the child elements
that do not contain [index] as an array into
the ones that contain [index]*/
} else {
element = element[path[i]] = element[path[i]] || {};
};
}
if (lastElement) element = element[lastElement] = value;
return element;
};
const map = [{
"name": "outField2",
"value": "something"
}, {
"name": "outField3[index].outField4",
"value": "something"
}, {
"name": "outField3[index].outField5",
"value": "something"
}, {
"name": "outField3[index].outField6.outField7",
"value": "something"
}];
let mapOutput = {};
map.forEach(doThis);
let mapOutputJSON = JSON.stringify(mapOutput, null, 2);
console.log(mapOutputJSON);
.as-console-wrapper { min-height: 100%!important; top: 0; }
you can do something like this
const data = [{
"name": "outField2",
"value": "something"
},
{
"name": "outField3[index].outField4",
"value": "something"
},
{
"name": "outField3[index].outField5",
"value": "something"
},
{
"name": "outField3[index].outField6.outField7",
"value": "something"
}
]
const buildObject = (paths, value, obj) => {
if (paths.length === 0) {
return value
}
const [path, ...rest] = paths
if(path.includes('[index]')) {
return {
...obj,
[path]: [buildObject(rest, value, (obj[path] || [])[0] || {})]
}
}
return {
...obj,
[path]: buildObject(rest, value, obj[path] || {})
}
}
const result = data.reduce((res, {
name,
value
}) => buildObject(name.split('.'), value, res), {})
console.log(result)
A possible generic approach which in my opinion also assigns the correct type of the OP's "outField3[index]" property (object type instead of an Array instance) is based on reduce where ...
the outer loop iterates the array of { name, value } items
by executing a single function accumulateObjectTypeFromPathAndValue where ...
this function does split each name-value into an array of object-path keys which then gets iterated by the inner reduce method where the passed object programmatically accumulates nested key-value pairs.
function accumulateObjectTypeFromPathAndValue(root, path, value) {
path
.split('.')
.reduce((obj, key, idx, arr) => {
if (!obj.hasOwnProperty(key)) {
Object.assign(obj, {
[ key ]: (idx === arr.length - 1)
? value
: {},
});
}
return obj[key];
}, root);
return root;
}
console.log(
[{
"name": "outField2",
"value": "something"
}, {
"name": "outField3[index].outField4",
"value": "something"
}, {
"name": "outField3[index].outField5",
"value": "something"
}, {
"name": "outField3[index].outField6.outField7",
"value": "something"
}].reduce((result, { name: path, value }) => {
return accumulateObjectTypeFromPathAndValue(result, path, value);
}, {})
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
The above implementation of the 2nd reducer function then could be changed according to the OP's custom array-type requirements ...
function accumulateCustomObjectTypeFromPathAndValue(root, path, value) {
path
.split('.')
.reduce((obj, key, idx, arr) => {
if (!obj.hasOwnProperty(key)) {
Object.assign(obj, {
[ key ]: (idx === arr.length - 1)
? value
: {},
});
if (key.endsWith('[index]')) {
obj[ key ] = [obj[ key ]];
}
}
return Array.isArray(obj[ key ])
//? obj[ key ].at(-1) // last item.
? obj[ key ][obj[ key ].length - 1] // last item.
: obj[ key ];
}, root);
return root;
}
console.log(
[{
"name": "outField2",
"value": "something"
}, {
"name": "outField3[index].outField4",
"value": "something"
}, {
"name": "outField3[index].outField5",
"value": "something"
}, {
"name": "outField3[index].outField6.outField7",
"value": "something"
}].reduce((result, { name: path, value }) => {
return accumulateCustomObjectTypeFromPathAndValue(result, path, value);
}, {})
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
I'm trying to create two (2) recursive functions to "loop" over an array of objects like below. I think the two functions are "similar" but they do two different things.
Function 1 should update the object - which could be every field in the "found" object and return the "new" array of objects, so the function needs to identify the appropriate object by .id
Function 2 needs to identify the appropriate object by .id BUT to delete that object and again return the "new" array of objects.
I've tried a number of ways (below the array of objects) - but to no avail, I cannot get the new object to return.
To note even if each object has varying/different keys, there will always be an .id key -
[
{
"type":"section",
"name":"Section 1",
"hassection":[
{
"type":"section",
"id":1,
"name":"Section 1 child section 1",
"hasMenuItem":
[
{
"type":"item",
"id":2,
"name":"Item 1",
"prices":
{
"type":"price",
"price":"15.95"
},
"description":"Blah Blah..."
},{
"type":"item",
"id":3,"name":
"Item 2",
"prices":[
{
"type":"price",
"price":"24.95"
},{
"type":"price",
"price":"13.95"
}
],
"description":"Blah Blah..."
}
]
},{
"type":"section",
"id":4,
"name":"Section 1 child section 2",
"hasitem":[
{
"type":"item",
"name":"Item 3",
"prices":
{
"type":"price","price":"14.50"
},
"description":"Blah Blah..."
},{
"type":"item",
"id":5,
"name":"Item 4",
"prices":
{
"type":"price",
"price":"14.50"
},
"description":"Blah Blah..."
}
]
},
]},{
"type":"section",
"name":"Section 2",
"hassection":[
{
"type":"section",
"id":6,
"name":"Section 2 child section 1",
"hasitem":[
{
"type":"item",
"id":7,
"name":"Item 5",
"prices":
{
"type":"price",
"price":"15.95"
},
"description":"Blah Blah..."
},
{
"type":"item",
"id":8,
"name":"Item 6",
"prices":
{
"type":"price",
"price":"13.95"
},
"description":"Blah Blah..."
}
]
}
]}
]
My update function
function updateNestedObj(obj,updates) {
const updateToApply = updates.find(upd => upd.id === obj.id);
if (updateToApply) {
// UPDATE THE OBJ
}
for(let k in obj) {
if (typeof(obj[k]) === 'object') {
// LOOP THROUGH THE OBJECT
updateNestedObj(obj[k], updates);
}
}
return updateToApply
}
My Delete function
function deleteNestedObj(obj, updates) {
const updateToApply = updates.find(upd => upd.id === obj.id);
if (updateToApply) {
delete upd;
}
for(let k in obj) {
if (typeof(obj[k]) === 'object') {
deleteNestedObj(obj[k], updates);
}
}
}
I just cannot fathom out how to "work them" - thanks in advance, any help much appreciated.
generics
Let's start with a immutable update(t, func) that takes a value of any type, t, and a callable updater function, func. func transforms t per the caller's specified return value. If no value is returned, ie undefined, then update will remove that value from the tree -
function update(t, func) {
switch (t?.constructor) {
case Object:
return Object.entries(t).reduce((r, [k, v]) => {
const newValue = update(func(v), func)
if (newValue !== undefined) r[k] = newValue
return r
}, {})
case Array:
return t.flatMap(v => {
const newValue = update(func(v), func)
return newValue === undefined ? [] : [newValue]
})
default:
return t
}
}
Immutable remove(t, func) can be defined as a specialization of update -
function remove(t, func) {
return update(t, v => Boolean(func(v)) ? undefined : v)
}
special forms
The functions can be further specialized to match your particular needs. updateWithObj(t, obj) will recursively update t where a node's id matches obj.id -
function updateWithObj(t, obj) {
return update(t, v => v.id == obj.id ? {...v, ...obj} : v)
}
Likewise removeWithObj(t, obj) recursively removes from t where a node's id matches obj.id -
function removeWithObj(t, obj) {
return remove(t, v => v.id == obj.id)
}
examples
Let's create some sample data. For what it's worth, update doesn't care whether it is an array of elements, [...] or a single object, {...} -
const data = [
{id: 1, data: 50 },
{id: 2, data: {id: 3, data: "foo"}},
{id: 4, data: [{id: 5, data: 3.141}, {id: 6, data: {id: 7, data: "bar"}}]}
]
We'll start with a simple update on obj.id == 1. Note the existing data attribute remains in tact and a new ok attribute is added. All other nodes remain unchanged -
console.log(updateWithObj(data, {id: 1, ok: "✅"}))
[
{
"id": 1,
"data": 50, // 👍🏽 remains unchanged
"ok": "✅" // 👍🏽 updated
},
{
"id": 2,
"data": {
"id": 3,
"data": "foo"
}
},
{
"id": 4,
"data": [
{
"id": 5,
"data": 3.141
},
{
"id": 6,
"data": {
"id": 7,
"data": "bar"
}
}
]
}
]
Here we see a deeply nested update with obj.id == 7. Note the data attribute for this node is updated and a new ok attribute is added -
console.log(updateWithObj(data, {id: 7, data: 0.123, ok: "✅"}))
[
{
"id": 1,
"data": 50
},
{
"id": 2,
"data": {
"id": 3,
"data": "foo"
}
},
{
"id": 4,
"data": [
{
"id": 5,
"data": 3.141
},
{
"id": 6,
"data": {
"id": 7,
"data": 0.123, // 👍🏽 updated
"ok": "✅" // 👍🏽 updated
}
}
]
}
]
Now let's see removal using removeWithObj. Notice obj.id == 6 is removed along with its descendants -
console.log(removeWithObj(data, {id: 6}))
[
{
"id": 1,
"data": 50
},
{
"id": 2,
"data": {
"id": 3,
"data": "foo"
}
},
{
"id": 4,
"data": [
{
"id": 5,
"data": 3.141
}
// 👍🏽 node removed
]
}
]
live demo
Here's a demo you can run in your own browser -
function update(t, func) {
switch (t?.constructor) {
case Object:
return Object.entries(t).reduce((r, [k, v]) => {
const newValue = update(func(v), func)
if (newValue !== undefined) r[k] = newValue
return r
}, {})
case Array:
return t.flatMap(v => {
const newValue = update(func(v), func)
return newValue === undefined ? [] : [newValue]
})
default:
return t
}
}
function remove(t, func) {
return update(t, v => Boolean(func(v)) ? undefined : v)
}
function updateWithObj(t, obj) {
return update(t, v => v.id == obj.id ? {...v, ...obj} : v)
}
function removeWithObj(t, obj) {
return remove(t, v => v.id == obj.id)
}
const data = [
{id: 1, data: 50 },
{id: 2, data: {id: 3, data: "foo"}},
{id: 4, data: [{id: 5, data: 3.141}, {id: 6, data: {id: 7, data: "bar"}}]}
]
console.log(updateWithObj(data, {id: 1, ok: "✅"}))
console.log(updateWithObj(data, {id: 7, data: 0.123, ok: "✅"}))
console.log(removeWithObj(data, {id: 6}))
.as-console-wrapper { min-height: 100%; top: 0; }
why undefined?
#Scott's comment draws attention to use of undefined as the mechanism for removal. I have always advocated for the programmer to reserve the right to use undefined for her/his particular needs. If a user gives undefined to a program, they can expect undefined behavior. For update, the undefined value is explicitly used to make a value not defined, or not there, ie remove it.
Other reasons support this choice. In most cases, an object with an explicitly undefined key behaves the same as one without the key. If the user really wants a var/key to be present but not yet set to a value, this is the perfect use of null -
const a = { foo: undefined } // foo is defined, but also not defined ??
const b = {} // does not have foo
console.log(a.foo, b.foo) // same behavior
// undefined undefined
JSON considers an undefined as "not defined" and so removes it when serializing an object -
const o = { a: undefined, b: null, c: false, d: 0, e: "" }
const j = JSON.stringify(o)
// "a" is not defined, so it's not serialized
console.log(j)
// looking for "a"? it's not defined :D
console.log(JSON.parse(j).a)
// undefined input gives undefined output. it's not defined :D
console.log(JSON.stringify(undefined))
ReScript plainly encodes None (ie "no value") as undefined in its Option module. Any Some(value) is represented as value -
// rescript
let foo = Some(1)
switch foo {
| Some(z) => Js.log(z)
| None => Js.log("no value")
}
// compiled javascript
var foo = 1;
if (foo !== undefined) {
console.log(foo);
} else {
console.log("no value");
}
explicit symbol
Maybe none of that convinces you and your fragile program still depends on having that undefined appear in the output. An explicit none sentinel can be used to signal to update that a particular value should be removed -
const none = Symbol() // ✅ removal sentinel
function update(t, func) {
switch (t?.constructor) {
case Object:
return Object.entries(t).reduce((r, [k, v]) => {
const newValue = update(func(v), func)
if (newValue !== none) r[k] = newValue // ✅
return r
}, {})
case Array:
return t.flatMap(v => {
const newValue = update(func(v), func)
return newValue === none ? [] : [newValue] // ✅
})
default:
return t
}
}
function remove(t, func) {
return update(t, v => Boolean(func(v)) ? none : v) // ✅
}
I have two array, One of them is like this:
const array1 = [
Object {
"ItemId": 1,
},
Object {
"ItemId": 2,
},
]
other array is :
const array2 = [
Object {
"obj1": Object {
"Id": 4736,
},
"obj2": Object {
"ItemId": 1,
},
}
]
I want to get items in array1, which are not equals in obj2 of array2 .
I tried this but doesnt work
array1.filter(function (item) {
return array2.map((x) => {
return x.obj2 != item;
});
instead of array2.map, you're looking for Array.protype.some or Array.prototype.every. Why?
array1.filter is expecting a truthy value for every item.
array2.map is returning an array (!) with the results of its callback. In your case it's the result of each comparisson x.obj2 != item. It could look something like this: [true, false]. But this array will always evaluate to true.
array2.some() or array2.every() also iterate over the array but will return a boolean for the condition you're using. You can try for yourself and check the code sample afterwards.
const array1 = [
{
"ItemId": 1,
},
{
"ItemId": 2,
},
];
const array2 = [
{
"obj1": {
"ItemId": 4736,
},
"obj2": {
"ItemId": 1,
},
},
];
// errornous filter usage
const test = array1.filter(item => {
const arr = array2.map(x => x.obj2 !== item);
return arr;
});
// suggestion
const filteredResult = array1.filter(item => {
// both ways work
// return array2.every(x => x.obj2.ItemId !== item.ItemId);
return !array2.some(x => x.obj2.ItemId === item.ItemId);
});
console.log('initial result', test);
console.log('correct result', filteredResult);
Try this
array1=array1.filter((item) => {
return array2[0].obj2.ItemId !== item.ItemId;
})
I have a following JsonArray:
arr = [
{
"name": "test",
"alias": "alias1",
"type": 1
},
{
"name": "test",
"type": 0
},
{
"name": "abc",
"alias": "alias2",
"type": 1
}
]
And I want to find using a variable value (which may contain alias / key). So basically the first preference of find should be alias, and if alias with the same value is not found then it should search in "name" and where "alias" is not present.
Normally it would have gone like:
_.find(arr, {
alias: value
})
But I want the code to return me the obj where name = value , if alias=value is not found
1) Ex: value = "alias1"
Expected==>
{
"name": "test",
"alias": "alias1",
"type": 1
}
2) Ex: value = "test"
Expected==>
{
"name": "test",
"type": 0
}
You'll need to use find (_.find() or Array.find()) to look for the alias match, and if none found use find again to look for a name match:
const findAliasOrName = (value, arr) =>
_.find(arr, { alias: value }) || _.find(arr, { name: value });
const arr = [{"name":"test","type":0},{"name":"test","alias":"alias1","type":1},{"name":"abc","alias":"alias2","type":1}]
console.log(findAliasOrName('alias1', arr));
console.log(findAliasOrName('test', arr));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
you can do this as follow:
if the alias key is available in obj then match the value with alias else match the value with name key using ternary operator
var arr = [
{
"name": "test",
"alias": "alias1",
"type": 1
},
{
"name": "test",
"type": 0
},
{
"name": "abc",
"alias": "alias2",
"type": 1
}
];
const findValue = (arr, value) => {
return _.find(arr, (elem) => {
return elem.alias ? elem.alias === value : elem.name === value;
});
}
console.log(findValue(arr, 'alias1'));
console.log(findValue(arr, 'test'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
You need to take a custom callback which takes the two possible properties for checking the value.
function find(array, value) {
return _.find(array, o => o.alias === value || o.name === value)
}
var array = [{ name: "abc", alias: "abc_alias", type: 1 }, { name: "tofind", type: 2 }, { name: "def", alias: "al1", type: 3 }];
console.log(find(array, 'abc_alias'));
console.log(find(array, 'tofind'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js">
I wish to filter a nested javascript object by the value of the "step" key:
var data = {
"name": "Root",
"step": 1,
"id": "0.0",
"children": [
{
"name": "first level child 1",
"id": "0.1",
"step":2,
"children": [
{
"name": "second level child 1",
"id": "0.1.1",
"step": 3,
"children": [
{
"name": "third level child 1",
"id": "0.1.1.1",
"step": 4,
"children": []},
{
"name": "third level child 2",
"id": "0.1.1.2",
"step": 5,
"children": []}
]},
]}
]
};
var subdata = data.children.filter(function (d) {
return (d.step <= 2)});
This just returns the unmodified nested object, even if I put value of filter to 1.
does .filter work on nested objects or do I need to roll my own function here, advise and correct code appreciated.
cjm
Recursive filter functions are fairly easy to create. This is an example, which strips a JS object of all items defined ["depth","x","x0","y","y0","parent","size"]:
function filter(data) {
for(var i in data){
if(["depth","x","x0","y","y0","parent","size"].indexOf(i) != -1){
delete data[i];
} else if (i === "children") {
for (var j in data.children) {
data.children[j] = filter(data.children[j])
}
}
}
return data;
}
If you would like to filter by something else, just updated the 2nd line with your filter function of choice.
Here's the function to filter nested arrays:
const filter = arr => condition => {
const res = [];
for (const item of arr) {
if (condition(item)) {
if (!item.children) {
res.push({ ...item });
} else {
const children = filter(item.children)(condition);
res.push({ ...item, children })
}
}
}
return res;
}
The only thing you have to do is to wrap your root object into an array to reach self-similarity. In common, your input array should look like this:
data = [
{ <...>, children: [
{ <...>, children: [...] },
...
] },
...
]
where <...> stands for some properties (in your case those are "name", "step" and "id"), and "children" is an optional service property.
Now you can pass your wrapped object into the filter function alongside a condition callback:
filter(data)(item => item.step <= 2)
and you'll get your structure filtered.
Here are a few more functions to deal with such structures I've just coded for fun:
const map = arr => f => {
const res = [];
for (const item of arr) {
if (!item.children) {
res.push({ ...f({ ...item }) });
} else {
res.push({ ...f({ ...item }), children: map(item.children)(f) });
}
}
return res;
}
const reduce = arr => g => init => {
if (!arr) return undefined;
let res = init;
for (const item of arr) {
if (!item.children) {
res = g(res)({ ...item });
} else {
res = g(res)({ ...item });
res = reduce(item.children)(g)(res);
}
}
return res;
}
Usage examples:
map(data)(item => ({ step: item.step }))
reduce(data)($ => item => $ + item.step)(0)
Likely, the code samples aren't ideal but probably could push someone to the right direction.
Yes, filter works on one array (list), like the children of one node. You have got a tree, if you want to search the whole tree you will need to use a tree traversal algorithm or you first put all nodes into an array which you can filter. I'm sure you can write the code yourself.