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);
Related
My Problem:
I'm having a website where I can compare products stored inside an array (with objects). I want to add different filters from array inside of an object that get applied together.
For two filters I can easily do it (see my code below). I just compare two objects and use a filter depending on their content.
But what would be a good approach to use the filter if there are more than two objects. Can I loop through the object and compare if the arrays are empty?
With my current approach I would have to extend my code for every new filter and it would balloon.
What I'm trying to do:
I want to check which filter objects have any data in their "feature" array (that array gets filled after the user clicks a filter on the site) and if they have I want to use these arrays to filter the main filteredArray array.
My current Object:
features_collection: {
aspect_ratio_object: {
features: [],
value: "Aspect Ratio",
},
performance_rating_object: {
features: [],
value: "Performance Rating",
},
},
My Filter Function:
if (
features_collection.aspect_ratio_object.features.length &&
features_collection.performance_rating_object.features.length
) {
return filteredArray.filter(
(obj) =>
features_collection.aspect_ratio_object.features.includes(
obj[features_collection.aspect_ratio_object.value]
) &&
features_collection.performance_rating_object.features.includes(
obj[features_collection.performance_rating_object.value]
)
);
} else if (
features_collection.aspect_ratio_object.features.length ||
features_collection.performance_rating_object.features.length
) {
return filteredArray.filter(
(obj) =>
features_collection.aspect_ratio_object.features.includes(
obj[features_collection.aspect_ratio_object.value]
) ||
features_collection.performance_rating_object.features.includes(
obj[features_collection.performance_rating_object.value]
)
);
}
},
Further Notes:
I can also change my object. I could change it into an array of objects if that would make things easier?
Making your filters an array seems more practical. Here's an example on how to
filter a set of objects against your feature_collection.
function filter_by_features(targets, feature_collection) {
// Start right of to filter the `filteredArray`
return targets.filter(obj => {
// go through every feature and test it against the current object.
// every() returns either true or false and the targets array is filtered
// by that condition supplied within the callback of `every()`
return feature_collection.every(filter => {
// If for a given feature no filter is available, return true
// so the test for this filter passes.
if(filter.features.length === 0) {
return true
}
// there are features, check if any applies.
return filter.features.includes(obj[filter.value])
})
})
}
Usage
// feature collection (as array)
const feature_collection = [
{
features: [],
value: "Aspect Ratio",
},
{
features: [],
value: "Performance Rating",
}
]
// the objects you want to filter.
const objects_to_filter = [/* ... */]
const filtered = filter_by_features(objects_to_filter, feature_collection)
docs
every()
You obviously have too loop through your object.
Here is your loop code for features_collection:
features_collection.forEach(function (item, index) {
console.log(item, index);
});
I have a function that is using eval to convert a string with an expression to an object based on the parameter.
let indexType = ["Mac", "User", "Line", "Mask", "Ip", "Location"]
const filterIndex = (item) => {
filteredIndexSearch = []
eval(`search${item}`).forEach((e) => filteredIndexSearch.push(searchData[e.key]))
}
filterIndex(indexType[searchTotal.indexOf(Math.max(...searchTotal))])
searchData is an array that returns values based on the user input.
searchTotal is an array with the length of each search{item} array.
The filterIndex function takes the highest value from the searchData array and corresponds it to the indexType array, then use eval to convert the string to an object to pass the value to the filteredIndexSearch array.
What would be a better alternative to eval?
EDIT
To add more information on what this does:
searchData = [
[
{
key: 1,
data: "0123456789101"
},
{
key: 1,
data: "John Smith"
}
],
[
{
key: 2,
data: "0123456789102"
},
{
key: 2,
data: "Jane Smith"
},
]
]
const search = (data, key, container) => {
if (!data) data = "";
if (data.toLowerCase().includes(string)) {
container = container[container.length] = {
key: key,
data: data
}
}
}
const returnSearch = () => {
for (let i = 0; i < searchData.length; i++) {
search(searchData[i][0].data, searchData[i][0].key, searchMac)
search(searchData[i][1].data, searchData[i][1].key, searchUser)
}
}
returnSearch()
The data is incomplete, but hopefully conveys what I'm trying to do.
search will take the user input, and store the information in the corresponding array. If I input "Jo", it will return the searchUser array with only the "John Smith" value and all the other values with the same key. Inputting "102" returns the searchMac with the "0123456789102" value and all other values with the same key.
At the end of the day. I just want to convert search${parameter} to an object without using eval.
Move your global arrays into an object.
Somewhere it appears that you're defining the arrays, something like:
searchMac = [...];
searchUser = [...];
...
Instead of defining them as individual arrays, I'd define them as properties in an object:
searchIndices.Mac = [...];
searchIndices.User = [...];
...
Then, instead of using eval, your can replace your eval().forEach with searchIndices[item].forEach.
If the order of your search isn't important, your can instead loop through the keys of searchIndices:
Object.keys(searchIndices).forEach(item => {
searchIndices[item].forEach(...);
});
This ensures that if you ever add or drop an entry in searchIndices, you won't miss it or accidentally error out on an undefined search index.
Any time you have a situation with variables named x0, x1 etc, that should be a red flag to tell you you should be using an array instead. Variable names should never be semantically meaningful - that is code should never rely on the name of a variable to determine how the code behaves. Convert search0 etc into an array of search terms. Then use:
const filterIndex = (item) => search[item].map(i => searchData[i.key]);
filteredIndexSearch = filterIndex(indexType[searchTotal.indexOf(Math.max(...searchTotal))]);
(simplifying your code). Note that in your code, filteredIndexSearch is modified inside the arrow function. Better to have it return the result as above.
I am having a hard time filtering through an array of objects based on a value in a nested array of objects. I have a chat application where a component renders a list of chats that a user has. I want to be able to filter through the chats by name when a user types into an input element.
Here is an example of the array or initial state :
const chats= [
{
id: "1",
isGroupChat: true,
users: [
{
id: "123",
name: "Billy Bob",
verified: false
},
{
id: "456",
name: "Superman",
verified: true
}
]
},
{
id: "2",
isGroupChat: true,
users: [
{
id: "193",
name: "Johhny Dang",
verified: false
},
{
id: "496",
name: "Batman",
verified: true
}
]
}
];
I want to be able to search by the Users names, and if the name exists in one of the objects (chats) have the whole object returned.
Here is what I have tried with no results
const handleSearch = (e) => {
const filtered = chats.map((chat) =>
chat.users.filter((user) => user.name.includes(e.target.value))
);
console.log(filtered);
// prints an empty array on every key press
};
const handleSearch = (e) => {
const filtered = chats.filter((chat) =>
chat.users.filter((user) => user.name.includes(e.target.value))
);
console.log(filtered);
// prints both objects (chats) on every keypress
};
Expected Results
If the input value is "bat" I would expect the chat with Id of 2 to be returned
[{
id: "2",
isGroupChat: true,
users: [
{
id: "193",
name: "Johhny Dang",
verified: false
},
{
id: "496",
name: "Batman",
verified: true
}
]
}]
The second approach seems a little closer to what you're trying to accomplish. There's two problems you may still need to tackle:
Is the search within the name case insensitive? If not, you're not handling that.
The function being used by a filter call needs to return a boolean value. Your outer filter is returning all results due to the inner filter returning the array itself and not a boolean expression. Javascript is converting it to a "truthy" result.
The following code should correct both of those issues:
const filtered = chats.filter((chat) => {
const searchValue = e.target.value.toLowerCase();
return chat.users.filter((user) => user.name.toLowerCase().includes(searchValue)).length > 0;
});
The toLowerCase() calls can be removed if you want case sensitivity. The .length > 0 verifies that the inner filter found at least one user with the substring and therefore returns the entire chat objects in the outer filter call.
If you want to get object id 2 when entering bat you should transform to lowercase
const handleSearch = (e) =>
chats.filter(chat =>
chat.users.filter(user => user.name.toLowerCase().includes(e.target.value)).length
);
try this it should work
const handleSearch2 = (e) => {
const filtered = chats.filter((chat) =>
chat.users.some((user) => user.name.includes(e))
);
console.log(filtered);
};
filter needs a predicate as argument, or, in other words, a function that returns a boolean; here some returns a boolean.
Using map as first iteration is wrong because map creates an array with the same number of elements of the array that's been applied to.
Going the easy route, you can do this.
It will loop first over all the chats and then in every chat it will check to see if the one of the users' username contains the username passed to the function. If so, the chat will be added to the filtered list.
Note, I am using toLowerCase() in order to make the search non case sensitive, you can remove it to make it case sensitive.
const handleSearch = (username) => {
var filtered = [];
chats.forEach((chat) => {
chat.users.forEach((user) => {
if (user.name.toLowerCase().includes(username.toLowerCase())) {
filtered.push(chat);
}
});
});
console.log(filtered);
return filtered;
}
handleSearch('bat');
I have an array of objects with the following structure
arr = [ { name: "abc" , items: ["itemA","itemB","itemC"], days :138} ,
{ name: "def" , items: ["itemA1","itemB2","itemC1"], days :157} ,
{ name: "hfg" , items: ["itemAN","itemB7","itemC7"], days :189} ]
This array needs to be filtered based on the search input passed. I was able to achieve the same for the name , where days is not getting filtered.
Also can someone help how to search across items array too so it filters the rows based on input passed
This is what I have tried
handleSearch = (arr, searchInput) => {
let filteredData= arr.filter(value => {
return (
value.name.toLowerCase().includes(searchInput.toLowerCase()) ||
value.days.toString().includes(searchInput.toString())
);
});
console.log(filteredData);
//this.setState({ list: filteredData });
}
You can use Array#some and then perform the same kind of match that you've already done :
The some() method tests whether at least one element in the array passes the test implemented by the provided function. It returns a Boolean value.
handleSearch = (arr, searchInput) => {
const filteredData = arr.filter(value => {
const searchStr = searchInput.toLowerCase();
const nameMatches = value.name.toLowerCase().includes(searchStr);
const daysMatches = value.days.toString().includes(searchStr);
const oneItemMatches = value.items.some(item => item.toLowerCase().includes(searchStr));
return nameMatches || daysMatches || oneItemMatches;
});
console.log(filteredData);
//this.setState({ list: filteredData });
}
As your search value can apply to all fields in your data array, you can combine the values together in one array (row by row) and perform the search in one place.
To do that, I've provided a snippet below that will filter the original array checking each object's values after the transformations. These involve using Object.values() to get the values of the object in an array, since this array is nested, we can make use of Array.flat() to flatten it into just the strings and numbers, finally call Array.some() to check if one of the values partially includes the search value (after they've both been lowercase-d).
const arr = [
{ name: "abc" , items: ["itemA","itemB","itemC"], days: 138 },
{ name: "def" , items: ["itemA1","itemB2","itemC1"], days: 157 },
{ name: "hfg" , items: ["itemAN","itemB7","itemC7"], days: 189 }
];
const handleSearch = (arr, searchInput) => (
arr.filter((obj) => (
Object.values(obj)
.flat()
.some((v) => (
`${v}`.toLowerCase().includes(`${searchInput}`.toLowerCase())
))
))
);
console.log('"A1" =>', JSON.stringify(handleSearch(arr, 'A1')));
console.log('189 =>', JSON.stringify(handleSearch(arr, 189)));
console.log('"nope" =>', JSON.stringify(handleSearch(arr, 'nope')));
NOTE: This approach has one obvious flaw, it will seach through numbers as strings, meaning that providing 89 as the search value will still return the second element.
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"));