I need to filter some data inside an array of objects which is contained in another array of objects. Here is the sample structure of my data. I need to filter on categories.
[
{
id: 540,
name:'Makeup kit'
slug:'makeup-kit',
status:'publish',
categories: [
{
id: 42, name:'Fashion',slug:'fashion'
},
{
id: 43, name:'Beauty',slug:'beauty'
}
]
},
{
id: 541,
name:'Silicon gloves'
slug:'silicon-gloves',
status:'publish',
categories: [
{
id: 44, name:'Health',slug:'health'
}
]
},
{
id: 650,
name:'Julep Mask'
slug:'julep-mask',
status:'publish',
categories: [
{
id: 43, name:'Beauty',slug:'beauty'
}
]
}
]
Here is how I'm trying
beautyProducts=temp1.filter(product=>product.categories.filter(cat=>cat.id===43))
but my solution doesn't seem to work.
Array#filter() expects the function you give it to return a truthy or falsy value. Elements for which the function returns a truthy value are kept in the new array, and those that give a falsy value are removed.
You want to keep only elements for which one of the categories has an id of 43. Using a second filter, then, makes no sense here: it returns an array, and arrays are always truthy; therefore the first filter will always receive an array for each element and all elements are kept in the new array.
Instead of a second filter, you should use Array#some() - you want to know if any of the categories have id===43, and if none of them do, then you want a falsy value so that the product gets excluded from the results.
Simple change:
beautyProducts = temp1.filter(product => product.categories.some(cat => cat.id === 43))
Here is a working sample:
let temp1 = [{id:540,name:'Makeup kit',slug:'makeup-kit',status:'publish',categories:[{id:42,name:'Fashion',slug:'fashion'},{id:43,name:'Beauty',slug:'beauty'}]},{id:541,name:'Silicon gloves',slug:'silicon-gloves',status:'publish',categories:[{id:44,name:'Health',slug:'health'}]},{id:650,name:'Julep Mask',slug:'julep-mask',status:'publish',categories:[{id:43,name:'Beauty',slug:'beauty'}]}];
let beautyProducts = temp1.filter(product => product.categories.some(cat => cat.id === 43));
console.log(beautyProducts);
Try like this.
beautyProducts = temp1.map(({categories, ...others}) => {
const filteredCategories = categories.filter(cat => cat.id === 43);
return {
filteredCategories,
...others
};
}).filter(product => product.categories.length > 0)
So first, you should do the inner filter first and map the inner filtered data to the current one and do the main filter after that like above.
let data = [
{
id: 540,
name: 'Makeup kit',
slug: 'makeup-kit',
status: 'publish',
categories: [
{
id: 42, name: 'Fashion', slug: 'fashion'
},
{
id: 43, name: 'Beauty', slug: 'beauty'
}
]
},
{
id: 541,
name: 'Silicon gloves',
slug: 'silicon-gloves',
status: 'publish',
categories: [
{
id: 44, name: 'Health', slug: 'health'
}
]
},
{
id: 650,
name: 'Julep Mask',
slug: 'julep-mask',
status: 'publish',
categories: [
{
id: 43, name: 'Beauty', slug: 'beauty'
}
]
}
];
let beautyProducts = data.map(product => {
const categories = product.categories.filter(cat => cat.id === 43);
if (categories.length) {
return { ...product, categories };
}
return null;
}).filter(p => p);
console.log("Prod:", beautyProducts);
console.log(">>>>>>>>>>>>>>>>>>>>>>>>>");
let beautyProductsTwo = data.filter(product => product.categories.some(cat => cat.id === 43));
console.log("Prod ans two:", beautyProductsTwo);
Related
Given the following structure and data:
interface GrandChild {
id: number,
values: Array<string>,
}
interface Child {
id: number,
subItems: Array<GrandChild>
}
interface Foo {
items: Array<Child>
}
const data: Foo = {
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 2, subItems: [ { id: 20, values: ['REMOVE', 'REMOVE'] }, { id: 21, values: ['REMOVE'] } ] },
{ id: 3, subItems: [ { id: 30, values: ['REMOVE'] }, { id: 31, values: ['REMOVE'] }, { id: 32, values: ['REMOVE', '32'] } ] },
]
};
How can I use the Array's methods (filter, map, some, etc.) to achieve the following result?
const expected: Foo = {
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 3, subItems: [ { id: 32, values: ['32'] } ] },
]
}
So far, I filtered the resulting data, removing the undesired elements, as following:
const filteredData: Foo = {
...data,
items: data.items.map(item => ({
...item,
subItems: item.subItems.map(subItem => ({
...subItem,
values: subItem.values.filter(value => value !== 'REMOVE')
}))
}))
}
Resulting:
{
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 2, subItems: [ { id: 20, values: [] }, { id: 21, values: [] } ] },
{ id: 3, subItems: [ { id: 30, values: [] }, { id: 31, values: [] }, { id: 32, values: ['32'] } ] },
]
};
But, I cannot figure a way out to remove the empty subItems elements without looping through the result.
You can check online the above code here.
If you really want to do it just with filter and map, add a filter after each of your maps to remove subItems that have an empty values array and to remove items that have an empty subItems array:
const filteredData = {
...data,
items: data.items
.map((item) => ({
...item,
subItems: item.subItems
.map((subItem) => ({
...subItem,
values: subItem.values.filter((value) => value !== "REMOVE"),
}))
.filter(({ values }) => values.length > 0), // ***
}))
.filter(({subItems}) => subItems.length > 0), // ***
};
But:
When I have map followed by filter, I always ask myself if the data is large enough that I should avoid making multiple passes through it.
When I'm doing lots of nesting of map calls and such, I always ask myself if it would be clearer when reading the code later to use simpler, smaller loops.
Here's what you might do if answering "yes" to either or both of those questions:
const filteredData: Foo = {
...data,
items: [],
};
for (const item of data.items) {
const subItems: Array<GrandChild> = [];
for (const subItem of item.subItems) {
const values = subItem.values.filter((value) => value !== "REMOVE");
if (values.length) {
subItems.push({
...subItem,
values,
});
}
}
if (subItems.length > 0) {
filteredData.items.push({
...item,
subItems,
});
}
}
I am trying to improve the time complexity and quality of the code snippet below.
I am iterating through one array to check if the element this array exists in the object, should this be true it should return the name matching the element id in the object.
how can I do this without having a nested loop?
Can someone tell me what I can do to make this algo better, please?
Thank you all in advance.
let genres = [28, 12, 878];
data = {
genres: [
{
id: 28,
name: 'Action',
},
{
id: 12,
name: 'Adventure',
},
{
id: 16,
name: 'Animation',
},
{
id: 35,
name: 'Comedy',
},
{
id: 80,
name: 'Crime',
},
{
id: 99,
name: 'Documentary',
},
{
id: 18,
name: 'Drama',
},
{
id: 10751,
name: 'Family',
},
{
id: 14,
name: 'Fantasy',
},
{
id: 36,
name: 'History',
},
{
id: 27,
name: 'Horror',
},
{
id: 10402,
name: 'Music',
},
{
id: 9648,
name: 'Mystery',
},
{
id: 10749,
name: 'Romance',
},
{
id: 878,
name: 'Science Fiction',
},
{
id: 10770,
name: 'TV Movie',
},
{
id: 53,
name: 'Thriller',
},
{
id: 10752,
name: 'War',
},
{
id: 37,
name: 'Western',
},
],
};
const getGenreName = () => {
let result = [];
for (let genre of data.genres) {
//console.log("genre", genre.name)
for (let id of genres) {
//console.log('id',genres[i])
if (id === genre.id) result.push(genre.name);
}
}
console.log(result);
};
getGenreName();
You can use reduce and includes as others have already shown. This will make the code a bit cleaner, but not change the overall runtime complexity. To improve runtime complexity you may need to use a different data structure.
For instance instead of
let genres = [1,2,3,4];
as a simple array, you could use a Set, which has a better lookup performance.
let genres = new Set([1,2,3,4]);
Then you can use this as follows
let result = data.genres
.filter(g => genres.has(g.id))
.map(g => g.name);
and won't need any explict for loops
The simplest improvement would probably be converting genres to a Set https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
and use the has method to check if each id in the data is a member of the set of chosen genres.
You can also convert the data to a map with the ids as the keys in order to look up by id quickly instead of looping, but that is only faster if the data is reused many times.
JavaScript #reduce in the example outlined below would have O(n) time complexity. This only loops through the array once. We could use filter, and map but it would result in us having to loop through the array twice.
const getGenreName = () => {
const genreSet = new Set(genres);
return data.genres.reduce((accumulator, { id, name }) => {
if (genreSet.has(id)) accumulator.push(name);
return accumulator;
}, []);
};
console.log(getGenreName()); // [ 'Action', 'Adventure', 'Science Fiction' ]
We are initializing the reducer to start with the array [], or an empty array, and then checking to see if the genre property of the object is included in the genres array, if it isn't, return the accumulator, if it is, append it to the end of the accumulator and return it.
You wanted this in one loop, so here it is:
let result = [];
data.genres.forEach(function (e) {
if (genres.includes(e.id)) result.push(e.name);
});
console.log(result);
In case you were wondering about forEach, here's a very good reference: https://www.w3schools.com/jsref/jsref_foreach.asp
The current time complexity is O(MN) where M is the length of data.genres and N is the length of genres.
Time complexity in JavaScript depends on which engine you use, but in most cases you can use a Map to reduce this time complexity to O(max{N,M}):
const getGenreName = () => {
const dataGenresMap = new Map( // O(M)
data.genres.map(({id,...params}) => [id,params]) // O(M)
)
let result = []
for (let id of genres) { // O(N)
if (dataGenresMap.has(id)) result.push(dataGenresMap.get(id).name) // O(1)
}
console.log(result)
}
If you might be doing this more than once then I'd recommend using a Map. By creating a hash map, retrieving genre names per id is much more performant.
let genres = [28, 12, 878];
data = {
genres: [
{
id: 28,
name: 'Action',
},
{
id: 12,
name: 'Adventure',
},
{
id: 16,
name: 'Animation',
},
{
id: 35,
name: 'Comedy',
},
{
id: 80,
name: 'Crime',
},
{
id: 99,
name: 'Documentary',
},
{
id: 18,
name: 'Drama',
},
{
id: 10751,
name: 'Family',
},
{
id: 14,
name: 'Fantasy',
},
{
id: 36,
name: 'History',
},
{
id: 27,
name: 'Horror',
},
{
id: 10402,
name: 'Music',
},
{
id: 9648,
name: 'Mystery',
},
{
id: 10749,
name: 'Romance',
},
{
id: 878,
name: 'Science Fiction',
},
{
id: 10770,
name: 'TV Movie',
},
{
id: 53,
name: 'Thriller',
},
{
id: 10752,
name: 'War',
},
{
id: 37,
name: 'Western',
},
],
};
const genreById = new Map ();
data.genres.forEach(({id, name}) => genreById.set(id, name));
const pushMapValueIfTruthy = map => array => key => {
const val = map.get(key);
if (val) {
array.push(val);
}
};
/** function that takes an array, then id, and pushes corresponding name (if exists) into the array. */
const pushGenreNaneIfExists = pushMapValueIfTruthy(genreById);
const getGenreNames = (ids) => {
result = [];
ids.forEach(pushGenreNaneIfExists(result));
return result;
};
console.log(getGenreNames(genres));
I'm trying to filter object with nested arrays by several criteria. Filtering options are generated dynamically and stored in array. This options values are theme id's in nested objects. If filtering options contain for example 2 id's values I need to show all objects that have that theme id's.
let data = {
'17 may': [
{
id: 31,
name: 'Test Name',
theme: {
id: 2,
name: 'Theme Test Name',
},
},
],
'18 may': [
{
id: 41,
name: 'Test Name',
theme: {
id: 2,
name: 'Theme Test Name',
},
},
{
id: 43,
name: 'Test Name',
theme: {
id: 3,
name: 'Theme Test Name',
},
},
],
'19 may': [
[
{
id: 51,
name: 'Test Name',
theme: {
id: 1,
name: 'Theme Test Name',
},
},
{
id: 52,
name: 'Test Name',
theme: {
id: 2,
name: 'Theme Test Name',
},
},
],
],
};
filteringOptions = [1,2]; // theme id's
I use filtering function for nested objects. It's working fine, but I dont' know how to pass more than one filtering option.
filterArray(array, filters) {
const filterKeys = Object.keys(filters);
return array.filter((item) => {
return filterKeys.every((key) => {
if (typeof filters[key] !== 'function') return true;
return filters[key](item[key]);
});
});
}
Filtering algorithm
const filteredByThemeId = {};
for (const day in data) {
filteredByTheme[day] = [];
this.data[day].map((item, index) => {
filteredByThemeId[day][index] = [
...filterArray(item, {
theme:
(theme) => {
if (!theme) return;
return theme.id === 2; // works fine, but I need to pass all values from filtering options array (options can contain 2, 5, 10 etc. values)
},
}),
];
});
}
Suppose you want to filter for all values in list called filterList = [2,5,10]. Instead of return theme.id === 2, you can try return filterList.includes(theme.id)
I need to delete an object or nested object in an array, from a given object ID.
The object needed to be deleted can both be a root object in the array or a nested object (a variant in this example) in one of the root objects.
Here's the array structure (both root objects and variant objects has unique IDs):
[
{ id: 1, title: 'object without variants', variants: [] },
{ id: 2, title: 'object with variants', variants: [{ id: 21, title: 'variant 1' }, { id: 22, title: 'variant 2' }]
]
So for example if the object ID passed from the click event that triggers the delete function is 1, I want to delete the whole root object with the ID of 1 and if the object passed from the click event is 21, I only want to delete the variant with the ID of 21 under the root object with the ID of 2 and not the whole root object.
How can this be done?
UPDATE
I got it working by using this code (passedObjectId is the ID of the object to be removed):
array = array.filter(object => object.id !== passedObjectId);
for (let object of array) {
object.variants = object.variants.filter(variant => variant.id !== passedObjectId);
}
I also need to remove the root object from the array if the last variant is removed from the object.
The code below works, but can I make this any prettier without having to use 3 filter() methods?
array = array.filter(object => object.id !== passedObjectId);
for (let object of array) {
// Remove the variant from the root object
object.variants = object.variants.filter(variant => variant.id !== passedObjectId);
// Remove the root object, if there's no variants left in it
if (!object.variants.length) {
array = array.filter(object => object.id !== passedObjectId);
}
}
ANOTHER UPDATE
I ended up using this code, that also removes a root object, if the last variant is removed:
array = array.filter(object => {
const hasRemovedVariant = object.variants.some(variant => variant.id === passedObjectId);
if (hasRemovedVariant) {
object.variants = object.variants.filter(variant => variant.id !== passedObjectId);
return object.variants.length;
}
return object.id !== passedObjectId;
});
Here is an example on how you can delete them separately, I let you put that together. If you have any question or trouble in the way to do it, feel free to ask.
const original = [{
id: 1,
title: 'object without variants',
variants: [],
},
{
id: 2,
title: 'object with variants',
variants: [{
id: 21,
title: 'variant 1'
}, {
id: 22,
title: 'variant 2'
}],
},
{
id: 3,
title: 'object with one variant',
variants: [{
id: 21,
title: 'variant 1'
}],
}
];
// Remove the root id
const rootIdToDelete = 1;
const modifiedRoot = original.filter(x => x.id !== rootIdToDelete);
// Remove the variant id
const variantToDelete = 21;
const modifiedRootAndVariant = modifiedRoot.filter((x) => {
x.variants = x.variants.filter(x => x.id !== variantToDelete);
// Keep only the roots that have at least 1 variant
return x.variants.length;
});
console.log(modifiedRootAndVariant);
You need to loop through your array and check if each object id is a match if it is the delete the object, else loop through your variants within the object and check for a match and delete the object.
Make sure you loop in reverse as you are modifying the array that you are looping through, hence the indexes change and you might get index out range exception
var myArray = getYourArray();
var idToCheck = 1; // get the id
for(int i=myArray.length;i--){
if(myArray[i].id == idToCheck){
myArray.splice(i);
}
else{
if(myArray[i].variants.length>0){
for(int j=myArray[i].variants.length;j--){
if(myArray[i].variants[j].id == idToCheck){
myArray[i].variants.splice(j);
}
}
}
}
}
This snippet will remove any child with an specified ID, no matter how big the hierarchy is.
var myArray = [{
id: 1,
title: 'object without variants',
variants: []
},
{
id: 2,
title: 'object with variants',
variants: [{
id: 21,
title: 'variant 1',
variants: [{
id: 23,
title: 'variant 1'
}, {
id: 24,
title: 'variant 2'
}]
}, {
id: 22,
title: 'variant 2'
}]
}
]
console.log(deleteByID(myArray, 21));
function deleteByID(array, id) {
for (var i = 0; i < array.length; i++) {
var item = array[i];
deleteItemByID(myArray, item, id, i);
}
return array;
}
function deleteItemByID(array, item, id, count) {
if (item.id == id) {
array.splice(count, 1);
return;
} else {
if (item.variants) {
if (typeof item.variants === "object") {
for (var i = 0; i < item.variants.length; i++) {
var varItem = item.variants[i];
deleteItemByID(item.variants, varItem, id, i);
}
}
}
}
}
You need to have a loop inside a loop.
You could use forEach for example.
var arr = [
{ id: 1, title: 'object without variants', variants: [] },
{ id: 2, title: 'object with variants', variants: [{ id: 21, title: 'variant 1' }, { id: 22, title: 'variant 2' }]}
];
var idToDelete = 21;
arr.forEach(function(obj,i){
if (obj.id == idToDelete){
arr.splice(i,1);
}
obj.variants.forEach(function(variants,i){
if (variants.id == idToDelete){
obj.variants.splice(i,1);
}
})
})
console.log(arr)
People have already answered but how about some functional, recursive and immutable code:
let array = [
{ id: 1, title: 'object without variants', variants: [] },
{ id: 2, title: 'object with variants', variants: [{ id: 21, title: 'variant 1' }, { id: 22, title: 'variant 2' }] }
];
const deleteFromArray = (arr, id) => {
if (!arr) return
let res = [];
arr.forEach((obj) => {
if (obj.id !== id) {
res.push({ ...obj, variants: deleteFromArray(obj.variants, id) })
}
})
return res;
}
console.log(JSON.stringify(deleteFromArray(array, 1)));
That way if you have some reference to the deleted object, it is deleted in ALL the objects in your array, including any nested variants.
You could create recursive function with some method so that you can exit the loop on first match and you can use splice to remove the element.
const data = [{ id: 1, title: 'object without variants', variants: [] },{ id: 2, title: 'object with variants', variants: [{ id: 21, title: 'variant 1' }, { id: 22, title: 'variant 2' }]}]
function remove(data, oid) {
data.some((e, i) => {
if(oid == e.id) return data.splice(i, 1);
if(e.variants) remove(e.variants, oid)
})
}
remove(data, 21)
console.log(data)
I want to filter out a nested array of objects but stuck at the filter part.
How to remove one of the mark?
this.state = {
data: [
{
id: 1,
name: "Main",
subs: [
{
id: "jay",
name: "Jay",
mark: [
{
id: "5a5d84b94a074c49ef2d4553",
name: 100
},
{
id: "5a5d84b94a074119ef2d4553",
name: 70
}
]
}
]
}
]
};
https://codesandbox.io/s/p39momxzp7
I try to use es6 as it's more readable.
expected output
data: [
{
id: 1,
name: "Main",
subs: [
{
id: "jay",
name: "Jay",
mark: [
{
id: "5a5d84b94a074119ef2d4553",
name: 70
}
]
}
]
}
]
Since there are multiple nested arrays in your data structure, you need to use forEach those many times
data.forEach( s => //iterate data
s.subs.forEach( t => //iterate subs
( t.mark = t.mark.slice( 1, 2 ) ) ) ); //slice the second value out
Demo
var data = [{
id: 1,
name: "Main",
subs: [{
id: "jay",
name: "Jay",
mark: [{
id: "5a5d84b94a074c49ef2d4553",
name: 100
},
{
id: "5a5d84b94a074119ef2d4553",
name: 70
}
]
}]
}];
data.forEach(s => s.subs.forEach(t => (t.mark = t.mark.slice(1,2))));
console.log(JSON.stringify(data, 0, 4))
In case the last value should be picked?
data.forEach( s => //iterate data
s.subs.forEach( t => //iterate subs
( t.mark = t.mark.slice( -1 ) ) ) ); //slice the last value out
If you are trying to filter a relevant mark by a given id,
you can combine Array#map and Array#filter to achieve it:
Note that i'm also using the Object Rest/Spread Properties proposal (stage 4)
Running example
const state = {
data: [{
id: 1,
name: "Main",
subs: [{
id: "jay",
name: "Jay",
mark: [{
id: "5a5d84b94a074c49ef2d4553",
name: 100
}, {
id: "5a5d84b94a074119ef2d4553",
name: 70
}]
}]
}]
};
const mark_id = '5a5d84b94a074119ef2d4553';
const nextState = {
...state,
data: state.data.map(obj => {
const filteredSubs = obj.subs.map(sub => {
const markById = sub.mark.filter(m => m.id === mark_id);
return {
...sub,
mark: markById
}
});
return {
...obj,
subs: filteredSubs
}
})
};
console.log(nextState);
You can even use lodash which contains many methods that can be handled easily.
Check if this is what you are looking for. (there is a good scope to refactor it but before that would like to understand if thats what you are looking for)
Below is the code that has been used there.
let inputId = "5a5d84b94a074c49ef2d4553";
let filteredData =_.each(_.cloneDeep(data), function(value, key1) {
_.each(value.subs, function(valueSubs, key2) {
var finalSubMark = _.find(valueSubs.mark, function(eachMark) {
return eachMark.id == inputId;
});
_.set(valueSubs, "mark", finalSubMark);
});
});
https://codesandbox.io/s/v065w05rly