JS: Delete Object Key if all nested Values equal null - javascript

From a given data structure (json file) I basically need to render a table. Empty rows and/or columns should not render. I'm fairly new to JavaScript and tried different approaches (converting to array and using .map(), reduce(), .filter(), lodash etc.) without success. I don't even know what the best way would be to tackle the problem. (Or what possible search terms would be.)
Neither "row keys" (In example: mo, tu, we, th, fr) nor "column keys" (john, hane, doe) are known and can vary.
Complete example: https://jsbin.com/rafeyasena/edit?js,output
"groupA": {
"mo": { "john": 8, "jane": 5, "doe": null },
"tu": { "john": 8, "jane": 5, "doe": null },
"we": { "john": 5, "jane": 9, "doe": null },
"th": { "john": 6, "jane": 3, "doe": null },
"fr": { "john": null, "jane": null, "doe": null }
}
Possible resulting data structure
const header = ["John", "Jane"];
const content = [
"mo": {[ 8, 5 ]},
"tu": {[ 8, 5 ]},
"we": {[ 5, 9 ]},
"th": {[ 6, 3 ]}
]
Expected result (Front-end, React):
| John | Jane |
---|------|--------
mo | 8 | 5 |
tu | 8 | 5 |
we | 5 | 9 |
th | 6 | 3 |
What I tried so far:
I was able to delete all values of null and the corresponding key, if it doesn't contain keys/values any more (Delete null values in nested javascript objects) - leading me with the challenge to find out all the leftover keys to build the table header. (In the example below this would be only John and Jane - So basically a way to iterate over all keys and log each key that exists at least one time). So my current data looks like this (but I'm not sure if it is the best way):
"groupA": {
"mo": { "john": 8, "jane": 5, },
"tu": { "john": 8, "jane": 5, },
"we": { "john": 5, "jane": 9, },
"th": { "john": 6, "jane": 3, }
}

I would just represent the data as a 2D array (that makes rendering easier):
const columnNames = [""];
const rows = [columnNames];
for(const [rowName, values] of Object.entries(groupA)) {
const row = [rowName];
for(const [columnName, value] of Object.entries(values)) {
let position = columnNames.indexOf(columnName);
if(value === null) continue;
if(position === -1)
position = columnNames.push(columnName) - 1;
row[position] = value;
}
rows.push(row);
}
// just some debugging:
console.log( rows.map(row => row.map(it => (it || "").padStart(10)).join("|")).join("\n") );

I think creating that latter format (with the nulls removed) is a very useful first step. From there you could write something like this to get it into a variant of your target format:
const uniqKeys = (obj) => [... new Set(Object.values(obj).flatMap(Object.keys))]
const transform = (group, headers = uniqKeys(group)) => ({
headers,
content: Object.entries(group).reduce(
(a, [k, v]) => ({...a, [k]: headers.map(h => v[h])}),
{}
)
})
const groupA = {mo: {john: 8, jane: 5}, tu: {john: 8, jane: 5}, we: {john: 5, jane: 9}, th: {john: 6, jane: 3}}
console.log(transform(groupA))
Note that the target is a little different than your request, as your example content isn't legal JS ({[ 8, 5 ]} doesn't make sense) but I think it captures the spirit of it, returning something like:
{
headers: ['john', 'jane'],
content: {
mo: [8, 5],
tu: [8, 5],
we: [5, 9],
th: [6, 3]
}
}
Note that this function is a little more general than the requirements, as you could supply it a list of headers and only extract those from the data.

Take a look at object-scan. It makes this sort of this relatively easy once you wrap your head around how it works. Here is how you'd answer your questions
// const objectScan = require('object-scan');
const isNullObject = (obj) => (
obj instanceof Object
&& !Array.isArray(obj)
&& Object.values(obj).every((e) => e === null)
);
const prune = (data) => objectScan(['**'], {
rtn: 'count',
filterFn: ({ value, parent, property }) => {
if (isNullObject(value)) {
delete parent[property];
return true;
}
return false;
}
})(data);
const stats = { groupA: { mo: { john: 8, jane: 5, doe: null }, tu: { john: 8, jane: 5, doe: null }, we: { john: 5, jane: 9, doe: null }, th: { john: 6, jane: 3, doe: null }, fr: { john: null, jane: null, doe: null } } };
console.log(prune(stats)); // return number of replaces
// => 1
console.log(stats);
/* =>
{ groupA:
{ mo: { john: 8, jane: 5, doe: null },
tu: { john: 8, jane: 5, doe: null },
we: { john: 5, jane: 9, doe: null },
th: { john: 6, jane: 3, doe: null } } }
*/
.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

Related

Find the highest object in array tree of nested children

I have an array of nested regions that look like this:
Egypt
Zone 1
Tagamo3
Giza
Helwan
Fayoum
Zone 2
Gesr ElSuis
test
Delta
Mohandeseen
Down Town
The array itself:
[
{
"key": 10,
"title": "Egypt",
"parent_key": null,
"children": [
{
"key": 1,
"title": "Zone 1",
"parent_key": 10,
"children": [
{
"key": 3,
"title": "Tagamo3",
"parent_key": 1,
"children": []
},
{
"key": 7,
"title": "Giza",
"parent_key": 1,
"children": []
},
{
"key": 8,
"title": "Helwan",
"parent_key": 1,
"children": []
},
{
"key": 11,
"title": "Fayoum",
"parent_key": 1,
"children": []
}
]
},
{
"key": 2,
"title": "Zone 2",
"parent_key": 10,
"children": [
{
"key": 4,
"title": "Gesr ElSuis",
"parent_key": 2,
"children": [
{
"key": 12,
"title": "test",
"parent_key": 4,
"children": []
}
]
},
{
"key": 5,
"title": "Delta",
"parent_key": 2,
"children": []
},
{
"key": 6,
"title": "Mohandeseen",
"parent_key": 2,
"children": []
},
{
"key": 9,
"title": "Down Town",
"parent_key": 2,
"children": []
}
]
}
]
}
]
I want to return to the highest region in a given input
Examples:
input [7, 1, 10] should return [10] since 10 is Egypt parent of 1 and 7
input [1, 2] should return both [1, 2] since they are on the same level both Zone 1 and zone 2 located under Egypt
input [2, 3, 1] should return [2, 1] since they are on the same level and 3 removed because it's a child of 1
input [1, 4] should return [1, 4] since they are on different levels and no one parent to the other
First it helps to turn your tree structure into a map of descendant ids, recursively:
const descendantsMap = new Map<number, Set<number>>();
function walk(tree: Tree) {
const s: Set<number> = new Set();
descendantsMap.set(tree.key, s);
for (const t of tree.children) {
walk(t);
s.add(t.key);
descendantsMap.get(t.key)?.forEach(v => s.add(v));
}
}
arr.forEach(walk);
We are building up a Map from each key in your tree structure to a Set of the keys of its descendants. The walk() function is recursive, and we merge the descendants for the children of each node into the descendants for the current node.
Let's make sure it looks right:
console.log(descendantsMap);
/* Map (12) {
10 => Set (11) {1, 3, 7, 8, 11, 2, 4, 12, 5, 6, 9},
1 => Set (4) {3, 7, 8, 11},
3 => Set (0) {},
7 => Set (0) {},
8 => Set (0) {},
11 => Set (0) {},
2 => Set (5) {4, 12, 5, 6, 9},
4 => Set (1) {12},
12 => Set (0) {},
5 => Set (0) {},
6 => Set (0) {},
9 => Set (0) {}
} */
Yes. You can see how now we have a quick mapping from each key to the set of keys in its descendant subtree.
Now to get the "highest" entries in an array (I would call these the "shallowest" since they are closest to the root), we find all the descendants of all the elements in the array and then filter these out of the array:
const shallowest = (x: number[]): number[] => {
const descendants = new Set<number>();
for (const v of x) {
descendantsMap.get(v)?.forEach(i => descendants.add(i));
}
console.log(descendants); // just to understand what's happening
return x.filter(v => !descendants.has(v));
}
Let's test it:
console.log(shallowest([7, 1, 10]));
// descendants are {3, 7, 8, 11, 1, 2, 4, 12, 5, 6, 9}
// output [10]
console.log(shallowest([1, 2]));
// descendants are {3, 7, 8, 11, 4, 12, 5, 6, 9};
// output [1, 2]
console.log(shallowest([2, 3, 1]));
// descendants are {4, 12, 5, 6, 9, 3, 7, 8, 11};
// output [2, 1]
console.log(shallowest([1, 4]));
// descendants are {3, 7, 8, 11, 12};
// output [1, 4]
Looks good. You can see that shallowest([7, 1, 10]) first finds all the descendants of 7, 1, and 10, which is {3, 7, 8, 11, 1, 2, 4, 12, 5, 6, 9}, or everything except 10. So when we filter those out of [7, 1, 10] we are left with just 10. Similarly, shallowest([1, 2]) and shallowest([1, 4]) produce sets of descendants that don't overlap at all with the input, so the output is identical to the input. And with shallowest([2, 3, 1]), the list of descendants contains 3 but not 2 or 1, so the output is [2, 1].
Playground link to code
This is my 2nd attempt, thanks to jcalz for pointing out the error and his solution is neater than mine.
The function buildArray builds an array of objects in to the variable keyArray, the key is the element in the array to be searched and another array that's the path to that element (so key 7 will have a path of [10, 1, 7]).
We then filter keyArray to remove any elements that have a parent in the original search array.
Anyway, reading jcalz's solution, I've learnt about maps so my time's not been entirely wasted. Hope this helps in some way though.
console.log(search2([7, 1, 10], obj)); //returns [10]
console.log(search2([1,2], obj)); //returns [1,2]
console.log(search2([2,3,1], obj)); //returns [1,2]
console.log(search2([1,4], obj)); //returns [1,4]
function search2(search, obj) {
keyArray=[];
buildArray(obj);
return keyArray.filter((element)=> !element.path.some(e => search.includes(e))).map((e)=> e.key);
function buildArray(obj, path=[]) {
obj.forEach((element) =>{
if(search.includes(element.key)) {
keyArray.push({"key":element.key,"path":path});
}
buildArray(element.children,[...path,element.key]);
});
}
}

Filter array and compare but skip null values

I am currently trying to filter available products based on their selected options.
const products = [{
id: 1,
name: 'Safari',
horsepowers: 30,
doors: 4,
gear_type: 'automatic',
wheels: 6
},
{
id: 2,
name: 'Jungle',
horsepowers: 50,
doors: 3,
gear_type: 'automatic',
wheels: 5
},
{
id: 3,
name: 'Moon',
horsepowers: 30,
doors: 4,
gear_type: 'manual',
wheels: 4
}
]
const selectedOptions =
{
horsepowers: 50,
doors: 3,
gear_type: null,
wheels: null
}
Typically I would do something like
const availableProducts = products.filter((product) =>
product.horsepowers === selectedOptions.horsepowers &&
product.doors === selectedOptions.doors .... etc
however, how do I skip null values, empty arrays, and undefined values if the user has not yet selected all possible options yet?
The next provided approach takes advantage of the 2nd thisArg argument of almost every available prototypal array method.
Thus one can write a generic filter function which compares any item's property values to the related ones configured by the selectedOptions object which will be passed alongside the filter function as filter's 2nd argument and as the filter function's this context ...
const selectedOptions = {
horsepowers: 50,
doors: 3,
gear_type: null,
wheels: null,
};
const products = [{
id: 1,
name: 'Safari',
horsepowers: 30,
doors: 4,
gear_type: 'automatic',
wheels: 6,
}, {
id: 2,
name: 'Jungle',
horsepowers: 50,
doors: 3,
gear_type: 'automatic',
wheels: 5,
}, {
id: 3,
name: 'Moon',
horsepowers: 30,
doors: 4,
gear_type: 'manual',
wheels: 4,
}];
function doItemPropertiesEqualEveryBoundSelectedOption(item) {
return Object
// create key value pairs from the `this` bound selected options.
.entries(this)
// skip/ignore selected option entries where `value` equals `null`.
.filter(([key, value]) => value !== null)
// execute item specific selected option validation via `every`.
.every(([key, value]) => item[key] === value);
}
console.log(
products
.filter(
doItemPropertiesEqualEveryBoundSelectedOption,
selectedOptions,
)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
In order to answer another of the OP's questions ...
"however, how do I skip null values, empty arrays, and undefined values if the user has not yet selected all possible options yet?"
... and also provide a generic solution to it, the above approach can be changed to a thisArg object which not only features the selected options but also the condition of not to be validated (invalid) selectedOption properties ...
const products = [{
id: 1,
name: 'Safari',
horsepowers: 30,
doors: 4,
gear_type: 'automatic',
wheels: 6,
}, {
id: 2,
name: 'Jungle',
horsepowers: 50,
doors: 3,
gear_type: 'automatic',
wheels: 5,
}, {
id: 3,
name: 'Moon',
horsepowers: 30,
doors: 4,
gear_type: 'manual',
wheels: 4,
}];
const selectedOptions = {
horsepowers: 50,
doors: 3,
gear_type: null,
wheels: null,
};
const isInvalidValue = (value) => {
return Array.isArray(value)
// empty array validation.
? (value.length === 0)
// undefined and null value validation.
: (value == null)
}
function doItemPropertiesEqualEveryBoundValidOption(item) {
const { options, isInvalidValue } = this;
return Object
.entries(options)
.filter(([key, value]) => !isInvalidValue(value))
.every(([key, value]) => item[key] === value);
}
console.log(
products
.filter(
doItemPropertiesEqualEveryBoundValidOption,
{ options: selectedOptions, isInvalidValue },
)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
selectedOptions.horsepowers == null ? true : product.horsepowers === selectedOptions.horsepowers &&
product.doors == null ? true : product.doors === selectedOptions.doors
If you want to keep it in line, you can use a ternary operator to check if it's null before comparing.
You could preprocess selectedOptions first and then comparing:
const applicableOptions = Object.entries(selectedOptions).filter(
([_, value]) => value !== null && value !== undefined
);
const availableProducts = products.filter((product) =>
applicableOptions.every(([optKey, optValue]) => product[optKey] === optValue)
);
to compare using array, you would need to update your example as there's no array property
Rather than typing in each option by hand, you could just iterate over selectedOptions. Then it's as simple as checking if the value of each option is null before comparing.
let filtered = products.filter(e => {
for(let [key, value] of Object.entries(selectedOptions))
{
if(value != null && e[key] != value)
return false;
}
return true;
});
const products = [{
id: 1,
name: 'Safari',
horsepowers: 30,
doors: 4,
gear_type: 'automatic',
wheels: 6
},
{
id: 2,
name: 'Jungle',
horsepowers: 50,
doors: 3,
gear_type: 'automatic',
wheels: 5
},
{
id: 3,
name: 'Moon',
horsepowers: 30,
doors: 4,
gear_type: 'manual',
wheels: 4
}
]
const selectedOptions =
{
horsepowers: 50,
doors: 3,
gear_type: null,
wheels: null
}
let filtered = products.filter(e => {
for(let [key, value] of Object.entries(selectedOptions))
{
if(value != null && e[key] != value)
return false;
}
return true;
});
console.log(filtered);
However, if you really want to write it out, I'd just check if the option is set with a simple boolean check. !(null) returns true, so this would work.
return (!selectedOptions.horsepowers || selectedOptions.horsepowers == product.horsepowers) && ...
You could generate a filter array from selectedOptions and filter the entries and use this for filtering the data, later.
const
products = [{ id: 1, name: 'Safari', horsepowers: 30, doors: 4, gear_type: 'automatic', wheels: 6 }, { id: 2, name: 'Jungle', horsepowers: 50, doors: 3, gear_type: 'automatic', wheels: 5 }, { id: 3, name: 'Moon', horsepowers: 30, doors: 4, gear_type: 'manual', wheels: 4 }],
selectedOptions = { horsepowers: 50, doors: 3, gear_type: null, wheels: null },
filter = Object
.entries(selectedOptions)
.filter(([, v]) => v !== null),
result = products.filter(o => filter.every(([k, v]) => o[k] === v));
console.log(result);
Using Object#entries and Array#filter, get the pairs with selected values from selectedOptions to use for filtering the products list
Using Array#filter and Array#every, filter the list to make sure that resulting products match the above pairs
const
products = [ { id: 1, name: 'Safari', horsepowers: 30, doors: 4, gear_type: 'automatic', wheels: 6 }, { id: 2, name: 'Jungle', horsepowers: 50, doors: 3, gear_type: 'automatic', wheels: 5 }, { id: 3, name: 'Moon', horsepowers: 30, doors: 4, gear_type: 'manual', wheels: 4 } ],
selectedOptions = { horsepowers: 50, doors: 3, gear_type: null, wheels: null };
const filterOptions =
Object.entries(selectedOptions).filter(([_, value]) => value !== null);
const selectedProducts =
products.filter(product =>
filterOptions.every(([key, value]) => product[key] === value)
);
console.log(selectedProducts);

Combine results from array of objects based on key in nested array of objects [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
I have array of objects where I want to filter and combine results based on specific id. This is example:
[
{
id: 1,
items: [
{
id: 10,
values: [11],
},
{
id: 20,
values: [13, 14, 15],
},
],
},
{
id: 2,
items: [
{
id: 10,
values: [12],
},
{
id: 20,
values: [13, 15],
},
],
},
];
And this is expected result:
[
{
id: 10,
values: [11, 12],
},
{
id: 20,
values: [13, 14, 15],
},
];
I also need to filter duplicates. Thanks
Note: What if I want this result?
[
{
// here I want value for id 10 (it will be always one number)
value: 11,
// here I want values for id 20 (array of numbers) => remove possible duplicates
values: [13, 14, 15],
},
{
// here I want value for id 10 (it will be always one number)
value: 12,
// here I want values for id 20 (array of numbers) => remove possible duplicates
values: [13, 15],
},
];
I tried the same approach with Map, but without success. Basically I want to combine values based on ids.
You could do with Array.flatMap to filter all items in single array.
Then recreate the array with Array.reduce and push the value based on id into new value object
And use Array.filter ignore the duplicate values on array
Object.values return only the value of object in array format
Older
const arr = [ { id: 1, items: [ { id: 10, values: [11], }, { id: 20, values: [13, 14, 15], }, ], }, { id: 2, items: [ { id: 10, values: [12], }, { id: 20, values: [13, 15], }, ], }, ];
const res = Object.values(arr.flatMap(({items})=> items)
.reduce((acc,{id,values})=>{
acc[id] = acc[id] ?? {id,values:[]};
//check the object exist or not
let newArr = acc[id]['values'].concat(values);
let valArr = newArr.filter((v,i)=>newArr.indexOf(v) === i)
//remove the duplicates
acc[id]['values'] = valArr
return acc
},{}))
console.log(res)
Updated
const arr = [ { id: 1, items: [ { id: 10, values: [11], }, { id: 20, values: [13, 14, 15], }, ], }, { id: 2, items: [ { id: 10, values: [12], }, { id: 20, values: [13, 15], }, ], }, ];
function filterMethod(arr,value,values){
return arr.map(({items})=> ({
value:detector(items,value)[0],
values:detector(items,values)
}))
}
function detector(items,idVal){
let ind = items.findIndex(({id})=> id === idVal);
return ind > -1 ? items[ind]['values'] : ['']
}
console.log(filterMethod(arr,10,20))

Find the same elements of an array from other arrays?

I have a movie, I want to show films of the same genres, what am I doing wrong?
My film (find):
{
"id": 1,
"title": "Kill Bill",
"genre_ids": [1, 10, 15]
}
// All films (movies)
{
"id": 2,
"title": "Leon",
"genre_ids": [1, 12, 15]
},
{
"id": 3,
"title": "Spider-man",
"genre_ids": [12, 32, 15]
},
{
"id": 3,
"title": "Marvel cap",
"genre_ids": [20, 38, 1]
},
// My code
return find.map(i => { // My film
return i.genre_ids.map(ids => {
return movies.data.results.filter(movie => { // All films
return movie.genre_ids.filter(idMov => idMov === ids)
})
})
});
Your movie (find) is an object, not an array, it doesn't have a map function to call.
A Solution:
Create a function that can from a single genre and array of movies return an array of matching movies by genre
const matchByGenre = movies => genre =>
movies.filter(movie => movie.genre_ids.includes(genre));
Iterate over the film's genre_ids array for matches. This yields an array of array matches, flatten them with .flat to a single array. The set is used to remove duplicates and have the result returned back to you as array.
const movieSet = Array.from(
new Set(film.genre_ids.map(matchByGenre(movies)).flat())
);
const film = {
id: 1,
title: "Kill Bill",
genre_ids: [1, 10, 15]
};
const movies = [
{
id: 2,
title: "Leon",
genre_ids: [1, 12, 15]
},
{
id: 3,
title: "Spider-man",
genre_ids: [12, 32, 15]
},
{
id: 4,
title: "Marvel cap",
genre_ids: [20, 38, 1]
},
{
id: 5,
title: "The Big Lebowski",
genre_ids: [20, 38, 2]
}
];
const matchByGenre = movies => genre =>
movies.filter(movie => movie.genre_ids.includes(genre));
const movieSet = Array.from(
new Set(film.genre_ids.map(matchByGenre(movies)).flat())
);
console.log(movieSet);
Note: If the syntax for matchByGenre is confusing, it is a curried function taking a movies array and returns a callback function to be used by array::map

Organizing an object to create keys when needed and/or push different attribute to value pair array

The data structure that I am trying to achieve would look as so :
I would like the list_id to become a key in a object, and hold all the id's of the items that have the matching list id.
var lists = { (list_id)1 : [1, 2, 3]
(list_id)2 : [4, 5, 6]
(list_id)3 : [7, 8, 9]
(list_id)4 : [10, 11, 12] };
this object is created from a json data structure that looks like this:
let json = [{ id: 1, list_id: 1 }, { id: 2, list_id: 1 },
{id: 3, list_id: 1 }, {id: 4, list_id: 2 },
{id: 5, list_id: 2 }, {id: 6, list_id: 2 },
{id: 7, list_id: 3 }, {id: 8, list_id: 3 },
{id: 9, list_id: 3 }, {id: 10, list_id: 4 },
{id: 11, list_id: 4 }, {id: 12, list_id: 4 }]
I can make an object that holds all the list_id's as keys but am getting stumped on pushing the actions_id into the value pair array with the matching list id.
let listAll = {};
json.forEach(function(lista, index, listb) {
listAll[lista.list_id] = [];
if ( listAll[lista.list_id] === lista.list_id){
listAll[lista.list_id].push(lista.id)
} else {
listAll[lista.list_id] = [lista.id];
}
});
My goal is to have and object that contains a key for every list_id currently avaliable from the actions.
Then add every action that contains the matching list_id into a value pair array.
the current output of this code is
{ '1': [ 3 ], '2': [ 6 ], '3': [ 9 ], '4': [ 12 ] }
which does not contain all numbers, each array should contain 3 numbers.
An alternative is using the function reduce to group the objects by a specific key = ['list_id', list_id].join('').
let json = [{ id: 1, list_id: 1 }, { id: 2, list_id: 1 }, {id: 3, list_id: 1 }, {id: 4, list_id: 2 }, {id: 5, list_id: 2 }, {id: 6, list_id: 2 }, {id: 7, list_id: 3 }, {id: 8, list_id: 3 }, {id: 9, list_id: 3 }, {id: 10, list_id: 4 }, {id: 11, list_id: 4 }, {id: 12, list_id: 4 }],
result = json.reduce((a, {id, list_id}) => {
let key = ['list_id', list_id].join(''); // For example: this is creating ['list_id', 1] to list_id1
(a[key] || (a[key] = [])).push(id);
return a;
}, Object.create(null)/*This is only to create an object without prototype -> {}*/);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Why don't you try hasOwnProperty instead?
var listAll = {};
json.forEach(function(list, index) {
if (listAll.hasOwnProperty(list.list_id)) {
listAll[list.list_id].push(list.id);
}else {
listAll[list.list_id] = [list.id];
}
});
console.log(listAll);

Categories

Resources