Fastest way to filter object by multiple properties - javascript

I have an array of objects that I want to filter for a string. So I want to check multiple properties if they contain the filter string (case insensitive).
Here's the array:
[{
id: "01234",
name: "My Object 01234",
short_name: "MO01234"
}, ...]
So all of the following filter strings should match that object: 0123, obj, mO01 etc.
Here's what I have right now:
const filterString = this.filterString.toLowerCase();
return myObjects.filter(
entry => {
return
entry.id.toLowerCase().indexOf(filterString) >= 0 ||
entry.name.toLowerCase().indexOf(filterString) >= 0 ||
entry.short_name.toLowerCase().indexOf(filterString) >= 0;
}
);
Can you think of a faster/cleaner way to do that?

I don't think that you can do it faster, but cleaner may be something like that
const filterString = this.filterString.toLowerCase();
return myObjects.filter((entry) => {
return Object.values(entry).some((value) => {
return value.toLowerCase().includes(filterString)
})
});

If you are allowed to put additional properties in your object, perhaps you could concatenate id, name and short_name (already in lowercase) into a single string and store it in the object as e.g. search_key; then you'd only have to check that.
{
id: "01234",
name: "My Object 01234",
short_name: "MO01234",
search_key: "01234:my object 01234:mo01234"
}
return myObjects.filter(
entry => entry.search_key.indexOf(filterString) >= 0
);
One thing you have to be mindful of in this case is to prevent unintended matches that may arise because e.g. the last few characters of id and the first few characters of name together produce a match. This is why I used a : delimiter here, assuming that's a character that can't appear in an ID or a short name.

let objects = [{
id: "01234",
name: "My Object 01234",
short_name: "MO01234"
},
{
id: "test",
name: "test",
short_name: "test"
}];
const filter = (collection, searchFor) => {
return collection.filter(obj => Object.values(obj).reduce((a,b) => a || String(b).toLowerCase().indexOf(searchFor.toLowerCase()) > -1, false))
}
console.log(filter(objects, "0123"));
console.log(filter(objects, "obj"));
console.log(filter(objects, "mO01"));
You could also extend this function to take a set of columns as parameter to filter on.
Another version using Regex:
const filterRegex = (collection, searchFor) => {
return collection.filter(obj => Object.values(obj).reduce((a,b) => a || String(b).match(new RegExp(searchFor, 'gi')), false))
}
console.log(filterRegex(objects, "0123"));
console.log(filterRegex(objects, "obj"));
console.log(filterRegex(objects, "mO01"));

Related

chain logical AND without fixed length given an array in Node.js and Typescript

Scenario:
I am making a generic function that returns a boolean depending on logical AND statements, however, the function being generic accept multiple type of objects and arrays, and the statements can vary depending on the objects.
at the moment I have something like this
private async myFunction(
myArray: myArrObj[],
myObj : myObj,
): Promise<boolean> {
return (
myArr.some(
(a) =>
a.status1=== "*" ||
a.status1 === myObj.status1.status1Id
) &&
myArr.some(
(a) =>
a.status2=== "*" ||
a.status2 === myObj.status2.status2Id
) &&
myArr.some(
(a) =>
a.status3=== "*" ||
a.status3 === myObj.status3.status3Id
) &&
myArr.some(
(a) =>
a.status4=== "*" ||
a.status4 === myObj.status4.status4Id
)
)
}
the issue is not being able to know what kind of array is passed and how many checks are needed, how can I make a return? My idea was storing each array.some method in an array and join them with " && ", this approach would require to execute something from a string, which I'm not sure is the most secure thing to do, since eval is not secure at all.
to get the myObj statuses I could just use a for loop and store the the the property in a string.
I can't come up with a good solution, so feel free to propose something new if my idea is not good enough
As noted by others in the comments, it would help if you had a reproducible example with sample data. That being said, from your comment:
but the statuses and id's have different names, some id's are .nameId, and some are just .id , but the statuses themselves have the same name, so instead of status1 and obStatus1 it really should be status1 and status1
Breaking this down:
but the statuses and id's have different names, some id's are .nameId, and some are just .id
You could try to see if nameId exists and fall back to id.
but the statuses themselves have the same name, so instead of status1 and obStatus1 it really should be status1 and status1
When myArr entries share keys with myObj, then you could simply loop through myObj's keys.
async function myFunction(myArr, myObj) {
// Fallback value for if .nameId and .id both don't exist.
// Falling back to `undefined` would cause a bug / false positives.
const notFound = Symbol();
// Loop through every key:value pair in the input object.
return Object.entries(myObj).every(([myObjKey, myObjValue]) => {
// Handle both `.nameId` and `.id`
const id = myObjValue[`${myObjKey}Id`] ?? myObjValue.id ?? notFound;
// If `myArrObj`'s children only ever contain exactly
// a single key { status2: { someRandomKey: 123 } }, then you
// could use myObjValue[Object.keys(myObjValue)[0]];
// For this key--for example "status1"--is there *any* array entry
// in `myArrObj` that has the same key and value or "*"?
return myArr.some((a) => {
return a[myObjKey] === '*' || a[myObjKey] === id;
});
});
}
With the following sample data:
const sampleArr = [
{ status3: "*" },
{ status2: 234 },
{ status1: 123, thisIsAnUnusedKey: true },
{ status4: 456 },
{ name: "Foobar" },
{ thisIsAnUnusedArrayEntry: true },
];
const sampleObj = {
status1: {
status1Id: 123,
},
status2: {
status2Id: 234,
},
status3: {
status3Id: 345,
},
status4: {
// Different key
id: 456,
},
name: {
// Different dataType
nameId: "Foobar"
}
};
myFunction(sampleArr, sampleObj).then(console.log); // Logs `true`

Process multiple request queries

I have an API that serves JSON data. Currently if you do api/weapons for example it gives you all the weapons available, api/weapons/weaponName gives information about that specific weapon. What I want to do is be able to api/weapons?type=sword&rarity=5 for example. I managed to pull of api/weapons?type=sword and api/weapons?rarity=5 on their own but not together.
Here's what I'm currently doing:
let filtered = [];
if (query.type) {
filtered = filtered.concat((await weapons).filter(w => formatName(w.weaponType) === formatName(query.type)));
}
if (query.rarity) {
filtered = filtered.concat((await weapons).filter(w => w.rarity == query.rarity));
}
if (!filtered.length) filtered = [await weapons]
res.status(HttpStatusCodes.ACCEPTED).send(filtered);
formatName is just a function that makes the string all lowercase and trims it and removes all spaces.
If we take api/weapons?type=sword&rarity=5
I think what's happening right now is:
It is getting all the weapons with the type "sword"
It is getting all the weapons with the rarity "5"
It is joining all the results together, so all the weapons with the type sword (regardless of rarity) and al the weapons with the rarity 5 (regardless of type).
I want it to filter weapons with ONLY that rarity AND ONLY that type. So only 5 rarity swords for example. What is the most beneficial way of handling this
I'd suggest retrieving "weapons" once and then running any filters on them without concatenating the results:
let filtered = [ ...(await weapons) ];
if (query.type) {
filtered = filtered.filter(w => w => formatName(w.weaponType) === formatName(query.type));
}
if (query.rarity) {
filtered = filtered.filter(w => w.rarity == query.rarity);
}
res.status(HttpStatusCodes.ACCEPTED).send(filtered);
Your current logic is testing whether one constraint OR another matches, what you actually need to do is to do an AND, which means you must perform the test in a single pass of filter.
I would slightly modify your code so that you compare all constraints that you're sending...you could further modify the logic below to accept a logical operator to test whether the rarity is >= or <= to a certain number for example.
const weapons = [{
type: 'sword',
name: 'swift blade of zek',
rarity: 5
},
{
type: 'mace',
name: 'hammer of kromzek kings',
rarity: 1
},
{
type: 'sword',
name: 'split blade of thunder',
rarity: 2
},
{
type: 'sword',
name: 'blade of carnage',
rarity: 5
},
]
const getWeapons = (query = {}) => {
let filtered = [];
let constraints = [];
// We could build this object dynamically but I just wanted
// to demonstrate it using your current approach
if (query.hasOwnProperty('type')) {
constraints.push({
name: 'type',
value: query.type
})
}
if (query.hasOwnProperty('rarity')) {
constraints.push({
name: 'rarity',
value: query.rarity
})
}
// Compare all of the conditions and only return weapons
// that match all of the conditions passed.
filtered = weapons.filter(w => {
let matches = 0
constraints.forEach(c => {
if (w[c.name] === c.value) {
matches += 1
}
})
// ensures we only return complete matches
return matches === constraints.length
});
return filtered
}
console.log(getWeapons({
type: 'sword',
rarity: 5
}))
Create an object which has the same property keys as the filters you want to use. Assign a function to each property where the evaluation for that specific filter is specified.
const filters = {
type: (weapon, type) => formatName(weapon.weaponType) === formatName(type),
rarity: (weapon, rarity) => weapon.rarity === rarity,
};
Then loop over the weapons with filter. Inside the filter loop, loop over the keys of the query variable with the every method. This method will return true or false based on if every evaluation is true or not.
In the every loop, use the keys of the query to select the filter from the filters list. Pass the weapon and the values of the query object to these filter functions and return result.
By doing this you can use one, two or no filters at all. And any new filters can be added in the filters object.
const filteredWeapons = weapons.filter((weapon) =>
Object.keys(query).every((filterKey) => {
if (!(filterKey in filters)) {
return false;
}
const filter = filters[filterKey]
const value = query[filterKey]
return filter(weapon, value);
})
);
res.status(HttpStatusCodes.ACCEPTED).send(filteredWeapons);

Group values keys based for each other value key

Sorry for bad title, I don't really know how to phrase this and this might be trivial problem ...
The data that comes from the array looks like this, each name can have an indefinite amount of sequence, what I want to do is group them by name and put each sequence in an array
[
{
name: 'Mike',
sequence: 'AAAA',
},
{
name: 'Bob',
sequence: 'ABAB',
},
{
name: 'Bob',
sequence: 'AAAB',
},
{
name: 'Marvin',
sequence: 'AAAA',
},
{
name: 'Marvin',
sequence: 'AABA',
},
{
name: 'Marvin',
sequence: 'BBBB',
},
]
What I am looking to return for each name by using console.log(name, array) for example would be something like this
Mike ["AAAA"]
Bob ["ABAB","AAAB"]
Marvin ["AAAA","AABA","BBBB"]
Thank you very much!
As mentioned in the comments, it seems you have tried some ways to solve the problem.
You can try following solution
Use Array.reduce to convert your array into an object with keys as name and value as array of sequences
In the reduce function, check whether the name exist in the resultant object. If it exists, concat the sequence to it (using spread syntax) else add a new entry with an array with sequence.
let input = [{name:'Mike',sequence:'AAAA',},{name:'Bob',sequence:'ABAB',},{name:'Bob',sequence:'AAAB',},{name:'Marvin',sequence:'AAAA',},{name:'Marvin',sequence:'AABA',},{name:'Marvin',sequence:'BBBB',}];
let result = input.reduce((a, {name, sequence}) => Object.assign(a, {[name] : a[name] ? [...a[name], sequence]: [sequence]}), {});
console.log(result);
inputArray.reduce((acc,{name,sequence}) => {
let obj = acc.find(a => a.name === name);
obj ? obj.sequence.push(sequence)
: acc.push({name,sequence:[sequence]});
return acc;
}, [])

How to search nested object by following JSLint

I have my object structured as below and I want to find the product with provided ID.
0 :{
id: 0,
title: 'xxxx',
url: "www.test.com"
quantity: 100
},
1 :{
id: 10,
title: 'xxxx',
url: "www.test.com"
quantity: 100
},
// and so on...
In order to search nested attribute within the object, I have written the below function:
export const selectProductById = (state, productId) => {
const obj_index = Object.keys(state.products).find(function(idx) {
if (state.products[idx].id == productId) {
return idx;
}
}
return state.products[obj_index]
}
This works but I will always get a warning during compilation of my react app.
Expected '===' and instead saw '=='
But if I change this into === the code will not work anymore, does anyone knows how to change this so that it follows JSLint rules ?
It sounds like the productId is not a number. Cast it to a number first:
if (state.products[idx].id === Number(productId)) {
But you should return a truthy or falsey value from the .find callback, not something that you're iterating over (since you may not be sure whether it's truthy or falsey, and it's potentially confusing). Return the result of the === comparison instead:
const { products } = state;
const obj_index = Object.keys(products).find(
key => products[key].id === Number(productId)
);

How can I filter by all the properties in an array of objects? [duplicate]

This question already has answers here:
Filter array of objects on all properties value
(3 answers)
Closed 5 years ago.
Suppose I have an array like this:
const people = [
{
"name":"pete22",
"age":56
},
{
"name":"sonya56",
"age":22
}
]
I can filter with lodash like this by name:
let result = people.filter(person =>_.includes(person.name,'56')
//{'name':'sonya56'}
What if I want to return all people with '56' in any property? So in the above example it would return both people? I am looking for a compact solution, maybe lodash?
You don't need Lodash to do this as JavaScript natively has support to do these kind of things.
What you need to do is filter the people but also filter each value inside an entry, e.g.:
const people = [{
"name": "pete22",
"age": 56
}, {
"name": "sonya56",
"age": 22
}]
// Filter your 'people' JSON
const filteredPeople = people.filter(person => {
// Filter each 'value' (property) inside each entry in 'people'
return Object.values(person).filter(value => {
// Turn a value into a string and check if it includes the value '56'
return value.toString().includes('56')
})
})
console.log(filteredPeople)
You could just use Array#filter with Object.values, Array#map with strings and check with Array#some and Array#includes.
const
people = [{ name: "pete22", age: 56 }, { name: "sonya56", age: 22 }],
filteredPeople = people.filter(person => Object
.values(person)
.map(String)
.some(v => v.includes('56'))
)
console.log(filteredPeople);
What if I want to return all people with '56' in any property?
You need an array with all such properties with which you want to filter the input array.
var prop = [ "age", "size" ];
Now simply applies the filter in a loop
var valueToMatch = "56";
result = people;
prop.forEach( function( key ){
if ( result.length ) { return true; } //if the result is already empty
result = result.filter( function( peopleObj ){
peopleObj[ key ] == valueToMatch;
});
});
result is the output array filtered with all the properties given in prop array.
Stop using lodash for everything.
JavaScript:
let filtered = people.filter((e) => e.name === ‘foo’ && e.age === 23);
Keep in mind that && forces the two conditions and || says that only one of them must be true.
Extended solution for any number of properties:
var people = [
{ "name":"pete22", "age":56 }, {"name":"sonya56", "age":22 },
{ "name":"john33", "age":33 }, {name: "mike", "login":"56mike", "code": 10 }
],
result = people.filter(function(o){
return Object.keys(o).some(function(k){ return String(o[k]).indexOf("56") !== -1; });
});
console.log(result);
We can filter again with values of Object
people.filter(
hm=>Object.values(hm).filter(
vl=>(vl+'').indexOf(56)>-1).length>0)

Categories

Resources