search array of objects.shortName and also nested array of objects - javascript

I'm trying to filter a large array of objects that also have nested values.
I need to match shortName OR description OR isoCode. Some of the items may have 20+ countries but most have 1 to 5.
{
countries: Array(1)
0:
description: "United Kingdom"
isoCode: "GB"
1:
description: "Italy"
isoCode: "IT"
shortName: "AIB (NI)"
},
// * 2000
I've tried building on this with limited success.
methods: {
filterInstitutions: function (items: any, event: any): void {
console.log(items, event.target.value);
if (event === '') {
newFunction(items);
} else {
this.listedInstitutions = items.filter((item: any) => {
return item.shortName.toLowerCase().includes(event.target.value.toLowerCase());
})
}
},
},
I am building this in Vue (typescript) but understand its as much of a JS question than a Vue one.
Any suggestions welcome.

You will need to add in tests for the description and isoCode of your countries property to your filter function.
One way would be to use the Array's some() method which will test if any of the array element's match any test you setup in the callback. It will then return true if any do match and false if not.
let testValue = event.target.value.toLowerCase();
this.listedInstitutions = items.filter((item: any) => {
//test the shortName
let matchesShortName = item.shortName.toLowerCase().includes(testValue);
//loop through countries and see if any description or isoCode match
let matchesDescIsoCode = item.countries.some((item:any) => {
let desc = item.description.toLowerCase();
let code = item.isoCode.toLowerCase();
return desc.includes(testValue) || code.includes(testValue);
});
return matchesShortName || matchesDescIsoCode;
})
Example
function doFilter(event, items) {
let testValue = event.target.value.toLowerCase();
let listedInstitutions = items.filter((item) => {
let matchesShortName = item.shortName.toLowerCase().includes(testValue);
let matchesDescIsoCode = item.countries.some((item) => {
let desc = item.description.toLowerCase();
let code = item.isoCode.toLowerCase();
return desc.includes(testValue) || code.includes(testValue);
});
return matchesShortName || matchesDescIsoCode;
});
console.clear();
console.log("Filtered List");
console.log(listedInstitutions);
}
let someItems = [{
countries: [{
description: "United Kingdom",
isoCode: "GB"
},
{
description: "Italy",
isoCode: "IT"
}
],
shortName: "AIB (NI)"
}, {
countries: [{
description: "United States",
isoCode: "US"
},
{
description: "Italy",
isoCode: "IT"
}
],
shortName: "ABC (DE)"
}]
<input type="text" oninput="doFilter(event,someItems)">

You could create an array of properties to search on within your object(s) and determine if any matches exist.
this.listedInstitutions = items.filter((item: any) => {
return item.countries.filter(
country => ['description', 'isoCode', 'shortName'].filter(prop => item.prop && item.prop.toLowerCase().includes(event.target.value.toLowerCase()).length > 0
).length > 0
})

Using filter() and some():
function filterInstitutions (items, {target: {value}}) {
const isMatch = text => text.toLowerCase().includes(value.toLowerCase());
return items.filter(({shortName, countries}) => (
isMatch(shortName) || countries.some(({description, isoCode}) => (
isMatch(description) || isMatch(isoCode)
))
));
};

Related

Assure all ids are unique with RxJS Observable pipe

I have an observable that I'd like to modify before it resolves, either using a map pipe or something similar to ensure that all ids within the groups array are unique. If cats is encountered twice, the second occurrence should become cats-1, cats-2 etc. These fields are being used to populate a HTML id attribute so I need to ensure they are always unique.
{
title: 'MyTitle',
description: 'MyDescription',
groups: [
{
id: 'cats',
title: 'SomeTitle'
},
{
id: 'dogs',
title: 'SomeTitle'
},
{
id: 'octupus',
title: 'SomeTitle'
},
{
id: 'cats',
title: 'SomeTitle'
},
]
}
Using an RxJs observable my code looks like the following:
getGroups() {
return this.http.get(ENDPOINT_URL)
}
I was able to achieve this using a map operator with a set but part of me feels like this isn't the correct pipe for this as the array is nested.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => {
const groupIds = new Map();
data.groups.map(group => {
if (!groupIds.get(group.id)) {
groupIds.set(group.id, 1)
} else {
const updatedId = (groupIds.get(group.id) || 0) + 1;
groupIds.set(group.id, updatedId);
group.id = `${group.id}-${updatedId}`
}
return group
}
return data;
}
)
}
Is there a more efficient way to make this operation using a more appropriate pipe? I am worried this can become quite inefficient and significantly delay rendering of content while the observable resolves the conflicts. As of today I am unable to modify the actual content returned from the API so that is not an option unfortunately.
You could try something like this:
import { of, map } from 'rxjs';
import { findLastIndex } from 'lodash';
of({
title: 'MyTitle',
description: 'MyDescription',
groups: [
{
id: 'cats',
title: 'SomeTitle',
},
{
id: 'dogs',
title: 'SomeTitle',
},
{
id: 'cats',
title: 'SomeTitle',
},
{
id: 'octupus',
title: 'SomeTitle',
},
{
id: 'cats',
title: 'SomeTitle',
},
],
})
.pipe(
map((data) => ({
...data,
groups: data.groups.reduce((acc, group) => {
const lastElementIndex = findLastIndex(acc, (accGroup) => accGroup.id.startsWith(group.id));
if (lastElementIndex === -1) {
return [...acc, group];
}
const lastElement = acc[lastElementIndex];
const lastNameNumerator = lastElement.id.split('-')[1];
return [
...acc,
{
...group,
id: `${group.id}-${lastNameNumerator ? +lastNameNumerator + 1 : 1}`,
},
];
}, []),
}))
)
.subscribe(console.log);
Stackblitz: https://stackblitz.com/edit/rxjs-kcxdcw?file=index.ts
If the only requirement is to have the ids be unique, you could ensure uniqueness by appending the array index to each element's id.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => {
const groups = data.groups.map(
(g, i) => ({...g, id: `${g.id}-${i}`})
);
return { ...data, groups };
})
);
}
Output of groups:
// groups: Array[5]
// 0: Object
// id : "cats-0"
// title : "SomeTitle"
//
// 1: Object
// id : "dogs-1"
// title : "SomeTitle"
//
// 2: Object
// id : "cats-2"
// title : "SomeTitle"
//
// 3: Object
// id : "octupus-3"
// title : "SomeTitle"
//
// 4: Object
// id : "cats-4"
// title : "SomeTitle"
Here's a little StackBlitz.
Honestly what you have is probably fine. Here's another method that's slightly simpler. It first uses reduce to create an object literal of groups. If you were open to external dependencies you could use Ramda's groupWith function to produce the same result. Then it uses flatMap to flatten the groups. If there is only one item in the array then it is returned as is, otherwise the elements are mutated with the new ids.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => Object.values(
data.groups.reduce((acc, cur) => {
(acc[cur.id] || (acc[cur.id] = [])).push(cur);
return acc;
},
{} as Record<string | number, [] as GroupType[])
).flatMap(grp => (grp.length === 1)
? grp
: grp.map((x, i) => ({ ...x, id: `${x.id}-${i + 1}`)))
)
}
Another one
map((data:any) => {
//create an array in the way [{id:"cats",data:[0,3]}{id:"dogs",data:[1]..]
const keys=data.groups.reduce((a:any,b:any,i:number)=>{
const el=a.find(x=>x.id==b.id)
if (el)
el.data=[...el.data,i]
else
a=[...a,({id:b.id,data:[i]})]
return a
},[])
//loop over groups, if keys.data.length>1 ...
data.groups.forEach((x,i)=>{
const el=keys.find(key=>key.id==x.id)
if (el.data.length>1)
x.id=x.id+'-'+(el.data.findIndex(l=>l==i)+1)
})
return data;
})
Or
map((data:any) => {
//create an object keys {cats:[0,3],dogs:[1]....
const keys=data.groups.reduce((a:any,b:any,i:number)=>{
if (a[b.id])
a[b.id]=[...a[b.id],i]
else
a[b.id]=[i]
return a
},{})
//loop over groups, if keys[id].length>0 ...
data.groups.forEach((x,i)=>{
if (keys[x.id].length>1)
x.id=x.id+'-'+(keys[x.id].findIndex(l=>l==i)+1)
})
return data;
})

Filtering an array by multiple property values

I have an array of the form
var cars = [
{name: "BMW X5", topsales: ["USA", "China", "Russia"], maxspeed: 250, users: ["teenage", "ladies", "mens"]}
{name: "Volkswagen Touareg", topsales: ["USA", "Germany"], maxspeed: 240, users: ["teenage", "mens", "old mens"]}
etc....
]
I am trying to filter, let's say like this:
var query = {
topsales: ["USA", "China"],
users: "teenage"
}
function nestedFilter(targetArray, filters) {
var filterKeys = Object.keys(filters);
return targetArray.filter(function (eachObj) {
return filterKeys.every(function (eachKey) {
if (!filters[eachKey].length) {
return true;
}
return filters[eachKey].includes(eachObj[eachKey]);
});
});
};
goodresult = nestedFilter(cars, query);
But the function doesn't work as it should. If the object has one value in the property, then it filters, but if there are several of them, and I need at least one of them to satisfy the search, then it does not filter. Help who can please
You could check if query is an array and/or the value is an array and check accordingly.
function nestedFilter(data, query) {
const
filters = Object.entries(query);
return data.filter(o => filters.every(([k, v]) => Array.isArray(v)
? Array.isArray(o[k])
? v.some(s => o[k].includes(s))
: v.includes(o[k])
: Array.isArray(o[k])
? o[k].includes(v)
: o[k] === v
));
}
const
cars = [{ name: "BMW X5", topsales: ["USA", "China", "Russia"], maxspeed: 250, users: ["teenage", "ladies", "mens"] }, { name: "Volkswagen Touareg", topsales: ["USA", "Germany"], maxspeed: 240, users: ["teenage", "mens", "old mens"] }],
query = { topsales: ["USA", "China"], users: "teenage" };
console.log(nestedFilter(cars, query));
I am assuming that you intend to implement an OR functionality because you said at least one of them. Therefore the working code is below.
But before going on reading, please beware of below remarks:
I used some instead of every, because some works as or and
every works as and. It means that that line will return true if
the current car item matches at least one of the filters.
You should use item.includes(filter) instead
of filter.includes(item).
You need to check if the current filter item is
an array or not, and act accordingly.
In this code I didn't handle that and assumed that currentCandidate is a string or a primitive. If there are other cases where the candidate item (i.e. a field of the car) itself is also an array, then you have to update the code to also handle that.
var cars = [
{name: "BMW X5", topsales: "USA, China, Russia", maxspeed: 250, users: "teenage, ladies, men"},
{name: "Volkswagen Touareg", topsales: "USA, Germany", maxspeed: 240, users: "teenage, men, old men"}
]
var query = {
topsales: ["USA", "China"],
maxspeed: 240
}
function nestedFilter(targetArray, filters) {
const filterKeys = Object.keys(filters);
return targetArray.filter(function (eachObj) {
//using some instead of every to make sure that it works as OR
const result = filterKeys.some(function (eachKey) {
//the current item that we are trying to use in the filter
const currentCandidate = eachObj[eachKey];
//the current item that we are using as a filter
const currentFilterItem = filters[eachKey]
if (Array.isArray(currentFilterItem)) {
if (currentFilterItem.length === 0) {
//no filter, return true
return true
}
//loop on each item in the currentFilterItem
//if any of them matches simply return true (OR)
for (let filterKey in currentFilterItem) {
if (currentCandidate.includes(currentFilterItem[filterKey])) {
return true
}
}
//for loop ended, no match
return false
} else {
//the current filter item is not an array, use it as one item
//return eachObj[eachKey].includes(currentFilterItem)
return currentCandidate === currentFilterItem
}
});
return result;
});
}
goodresult = nestedFilter(cars, query);
console.debug(goodresult)
You can check if the value of the "filterKey" is not an array, make it an array, and check if an array has a subArray
function hasSubArray(master, sub) {
return sub.every((i => v => i = master.indexOf(v, i) + 1)(0));
}
function nestedFilter(targetArray, filters) {
var filterKeys = Object.keys(filters);
return targetArray.filter(function (eachObj) {
return filterKeys.every(function (eachKey) {
var subArray = filters[eachKey];
if (!Array.isArray(filters[eachKey])) {
subArray = [filters[eachKey]];
}
return hasSubArray(eachObj[eachKey], subArray);
});
});
}

The best way to filter down a dataSet with javascript

The idea was to query a dataset with querystring params. I only want the "records" to match only what was queried.
Dataset
{
1111:
{
Category: "Education"
Role: "Analyst"
}
2222:
{
Category: "Communications and Media"
Role: "Analyst"
}
3333:
{
Category: "Public Sector"
Role: "Something else"
}
4444:
{
Category: "Public Sector"
Role: "Something else"
}
...
}
[[Prototype]]: Object
I'm sending in qString
Category: (2) ['Communications and Media', 'Education']
Role: ['Analyst']
length: 0
[[Prototype]]: Array(0)
I'd like to loop over that and filter/reduce so I only have records that match. Sort of an and instead of an or.
dataSet is an Object of objects. Thoughts? Thanks in advance.
export const Filtered = (qStrings, dataSet) => {
const filtered = [];
Object.entries(qStrings).forEach(([field]) => {
qStrings[field].forEach((value) => {
filtered.push(
..._.filter(dataSet, (sess) => {
if (sess[field] && sess[field].toString() === value.toString()) {
return sess;
}
})
);
});
});
return _.uniq(filtered);
};
geez, I figured it out with a colleague who's way smarter than me wink Jess!
export const Filtered = (qStrings, dataSet) => {
let filtered = [];
Object.entries(qStrings).forEach(([field], idx) => {
let source = filtered;
if (idx === 0) {
source = dataSet;
}
filtered = _.filter(source, (sess) => {
return sess[field] && sess[field].includes(qStrings[field]);
});
});
return _.uniq(filtered);
};
Now to clean this up.
Not sure if this solves your problem exactly, but you can apply this logic without mutation for a much cleaner function.
export const matches = (qStrings, dataSet) =>
Object.entries(dataSet).reduce((acc, [key, value]) =>
Object.entries(value).every(([rKey, rValue]) => qStrings[rKey]?.includes(rValue))
? { ...acc, [key]: value }
: acc,
{});
This will return records 1111 and 2222 because they match one of the categories and the role in qStrings.

Filter from key value objects in reactjs

I am creating searchbar to filter key value data objects. I am getting filter is not a function error while doing search. Is there any other function to filter in key value pair objects ?
Data :-
{
meta_city : {
label: "City"
values: (5) ["DL", "KA", "GJ", "MH", "UP"]
},
meta_country : {
label: "Country"
values: (5) ["IN", "US", "CA"]
}
}
Handle search (filterData data is local state) :-
const handleSearchFilter = (event) => {
const searchWord = event.target.value;
const newFilter = Object.keys(filterData).map((key) => {
filterData[key].filter((value) => {
return value.includes(searchWord);
});
});
setFilterData(newFilter);
};
<div className="mp-input-field-container">
<input
className="mp-input"
placeholder="Search"
onChange={handleSearchFilter}
/>
</div>
You should use reduce like this:
const handleSearchFilter = (event) => {
const searchWord = event.target.value;
const newFilter = Object.keys(filterData).reduce((result, key) => {
if (filterData[key].values.includes(searchWord)) {
result.push(filterData[key]);
};
return result;
}, []);
setFilterData(newFilter);
};
In this example I'm returning an array result. you can return an object if you want.
Filter does not exist on a string type. When filterData[key] is called, key has a value of label. filterData["label"] returns a string "City".
try
const searchWord = (word) => Object.values(filterData).filter((data) => data.values?.includes(word));
const handleSearchFilter = (event) => {
const word = event.target.value;
const [newFilter] = searchWord(word)
setFilterData(newFilter);
}
searchWord returns if you search "DL"
[
{
label: 'City',
values: [ 'DL', 'KA', 'GJ', 'MH', 'UP' ]
}
]
Was that the result you were looking for?
Here is the code snippet to prove the solution works:
var filterData = {
meta_city : {
label: "City",
values: ["DL", "KA", "GJ", "MH", "UP"]
},
meta_country : {
label: "Country",
values: ["IN", "US", "CA"]
}
}
const searchWord = (word) => Object.values(filterData).filter((data) => data.values.includes(word));
console.log(searchWord("DL"))

How to filter the entire array based on user input using filter in JavaScript ?

I am trying to use a filter in javascript to search on an array.
Following is my array:-
"customerStatusInfoList": [
{
"customerId": 1110000012,
"caseStatus": "pending",
"customerName": "Robert",
"dateOfRequest": "2018-12-15 00:00:00.0"
},
{
"customerId": 1110000004,
"auditorName": "DcbAdmin",
"caseStatus": "pending",
"customerName": "Sam",
"dateOfRequest": "2018-12-14 12:40:04.0"
}
]
And I am using the following function to filter the array:-
filterTable = event => {
console.log("event.target.value", event.target.value);
console.log("rows", this.state.rows);
if (event.target.value !== "") {
let newRow = this.state.rows.filter(items => {
console.log("item.name", items.customerName);
if (items.customerName!== null)
return items.customerName
.toLowerCase()
.includes(event.target.value.toLowerCase());
});
this.setState({ rows: newRow, query: event.target.value });
} else {
console.log("Query string is empty ", event.target.value);
let newRow = this.state.custList;
console.log("new row :", newRow);
this.setState({ query: event.target.value, rows: newRow });
}
};
I am able to filter on the customerName but when I try to filter using customerId or any other parameter I get customerId.includes is not a function.
But it works on customerName.
How can I filter on the entire table using JavaScript filter?
Any help or suggestion is appreciated. Thank you.
customerId is an integer - you need to cast to string, e.g.:
return `${items.customerId}`
.toLowerCase()
.includes(event.target.value.toLowerCase());
btw, items is a confusing name for the variable - it's a single item
also, you can simplify things a bit by decomposing the item, i.e.:
let newRow = this.state.rows.filter(({customerId, customerName}) =>
`${customerName || ''}`
.toLowerCase()
.includes(event.target.value.toLowerCase())
);
to include any row that matches customerId, customerName, or auditorName:
let newRow = this.state.rows.filter(({customerId, customerName, auditorName}) =>
[customerId, customerName, auditorName].some(field =>
`${field || ''}`
.toLowerCase()
.includes(event.target.value.toLowerCase()))
);
I added a test for type in here.
filterTable = event => {
console.log("event.target.value", event.target.value);
console.log("rows", this.state.rows);
if (event.target.value) {
let newRow = this.state.rows.filter(items => {
console.log("item.name", items.customerName);
if(typeof event.target.value == 'string') {
if (items.customerName!== null)
return items.customerName
.toLowerCase()
.includes(event.target.value.toLowerCase());
} else if(typeof event.target.value === 'number' {
return items.cusomterId === event.target.value);
}
});
this.setState({ rows: newRow, query: event.target.value });
} else {
console.log("Query string is empty ", event.target.value);
let newRow = this.state.custList;
console.log("new row :", newRow);
this.setState({ query: event.target.value, rows: newRow });
}
};
Hi I have added some pure javascipt logic which you can embed in your react component as per your need .
Here you can replace the targetName and targetValue comming from the state values of the component.
Having switch case will allow you to add different logic for different kind of fields.
const data = {
"customerStatusInfoList": [
{
"customerId": 1110000012,
"caseStatus": "pending",
"customerName": "Robert",
"dateOfRequest": "2018-12-15 00:00:00.0"
},
{
"customerId": 1110000004,
"auditorName": "DcbAdmin",
"caseStatus": "pending",
"customerName": "Sam",
"dateOfRequest": "2018-12-14 12:40:04.0"
}
]
}
const targetName = 'customerId';
const targetValue = 1110000004;
callback = (value, index, array) => {
switch (targetName) {
case "customerName":
return value.customerName === targetValue;
case "customerId":
return value.customerId === targetValue;
default:
return value.customerName === targetValue;
}
}
let result = data.customerStatusInfoList.filter(callback, this);

Categories