Basically I am trying to bring down the time complexity of a series of functions that I have
const companies = [
{
staff: [
{
id: 1,
qualities: [
{
id: 2,
tags: [
'efficient',
'hard-working'
]
}
]
}
]
}
]
So I have an array of companies and within that is another array of staff and then again within that there is an array of qualities and finally an array of tags
now I have a predetermined list of tags that I need to match users to for each company like so
companies: [
xyz: [
{
tag: 'smart',
users: []
}
],
// ...
];
so basically I need to loop through each pre-determined tag, then loop through each company, then loop through each user and loop through each tag to create this view
so basically something like this
const tags = [
'smart',
'hard-working',
'efficient'
];
getUserTags(tagName) {
const users = [];
companies.forEach(company => {
company.users.forEach(user => {
user.tags.forEach(tag => {
if (tag === tagName) {
users.push(user);
}
});
});
});
return users;
}
as you can see this is super inefficent and the big O works out to be O(n^4) which is horrible.
How can I solve this?
there could be 50 tags […] so for each tag it would have to [call the getUserTags function and] loop through every user and then loop through each users tags to see if its a match to get the total count.
No, that you shouldn't do. Instead, loop only once through the users and the tags of each user, and collect them in a array-by-tagname map data structure. Use something like
getUsersByTags(companies, tags) {
let map = new Map();
for (const tag of tags) {
map.set(tag, []);
}
for (const company of companies) {
for (const user of company.staff) {
for (const quality of user.qualities) {
for (const tag of quality.tags) {
const n = tag.name;
if (map.has(n))
map.get(n).push(user);
}
}
}
}
return map;
}
You cannot avoid traversing the whole company-staff-quality-tag structure to access all your tag data. Just make sure to not do it more than once.
As far as time complexity goes, you say you want 4 layers of abstraction. (Before the edit to your question it was 3 was users, qualities, tags). Now we have Companies, Staff, Qualities, Tags. With this structure you should be expecting O(n^4). The below code does not try to change the level of complexity, but it might be quicker due to only keeping a count rather than items and using includes to short circuit the exit from a loop.
This can be done using reduce and includes.
let getCountOfTags = (users, tagName) => users.reduce(
(acc, item) => acc + item.qualities.reduce(
(qAcc, q) => qAcc + (q.tags.includes(tagName) ? 1 : 0),
0
),
0
);
We use reduce to get a single value from the accumulation of an array by summing the tag values being identified as being included in each quality item.
Related
Users are allowed to search their data by keying Name or City or Code. How can I display matched object on the top of the dropdown list.
Prioritizing Code here as when user enters JOH the following obj will be highlighted.
{
name: 'John Cena',
code: 'JOH',
city: 'California'
}
Below code works only if user enters the full name or code or city. The list has over 1000 records.
Trying to match the 3 characters code first with that of user input or otherwise by name or city.
let input = e.currentTarget.value;
const arr = []
for (const user of List) {
if (user.code.includes(input) > -1) {
arr.push(station)
}
}
}
If you also need some sort of priorization (ie a match in code counts more than a match in name) you will need to weigh each match and then sort by weight. Assuming userList is an array
let results = userList.map(user => {
let weight = 0;
if (user.code.toLowerCase().includes(input)) weight = 900
else if (user.name.toLowerCase().includes(input)) weight = 800
else if (user.city.toLowerCase().includes(input)) weight = 700
return { user, weight };
})
.filter(x => x.weight > 0)
.sort((a,b) => b.weight - a.weight)
.map(x => x.user);
This will return a list of users sorted by the best match (ie match by code) first. It's not super efficent, as it's traversing the list multiple times, but actually I don't think this will be a real issue at just 1000 elements ...
If you run into performance issues, you could for instance refactor userlist.map(...).filter(...) into userlist.reduce() but I leave that up to you ... For understanding the solution, I think map and filter are easier to read.
I have an object of newsfeed items like below.
[{'story_id':130,'pinned':0,....},{'story_id':131,'pinned':1,....},{'story_id':132,'pinned':0,....},{'story_id':133,'pinned':0,....}]
I need to primarily order the news stories by their story_id DESC. But if a story has the property 'pinned'=1 it needs to be first.
filtered_news_feed: function() {
var list= _.orderBy(this.feed_items, ['story_id'],'desc');
return list;
},
The above works, but how do I do pinned items first, then the rest? For some reason the below completely ignores the story_id
var list= _.orderBy(this.feed_items, ['pinned','story_id'],'desc');
Using Array#sort:
const arr = [ {'story_id':130,'pinned':0}, {'story_id':131,'pinned':1}, {'story_id':132,'pinned':0}, {'story_id':133,'pinned':0} ];
const sorted = arr.sort(
({ story_id: storyIdA, pinned: pinnedA }, { story_id: storyIdB, pinned: pinnedB }) =>
pinnedB - pinnedA || storyIdB - storyIdA
);
console.log(sorted);
You can achieve this results in many ways, one of them is:
filtered_news_feed: function() {
const pinnedItems = this.feed_items.filter(item => item.pinned === 1);
const normalItems = this.feed_items.filter(item => item.pinned === 0);
return [
...pinnedItems,
_.orderBy(normalItems, ['story_id'], 'desc')
];
}
First, separate pinned items from normal items. Then return merged array with pinned items at the beginning.
Note: I used modern features of ES here. You should compile it via babel or other tool.
I'm trying to add an item in a specific index inside an array inside a map function and it's been behaving unexpectedly. Here's the code for it
const addItemToLevelTwoArray= (uniqueID, arrayID )=> {
const reportObject = {
id:arrayID,
title:'',
}
data.map(section=>{
section.content.map((report, reportIndex)=>{
if(report.id===uniqueID){
section.content.splice(reportIndex, 0, reportObject);
}
return report;
})
return section;
})
}
Here's a working pen - https://codepen.io/raufabr/pen/vYZYgOV?editors=0011
Expected behaviour is that it would insert an object in the specific index, right above the object where the ID matches.
However, it's acting weirdly and sometimes I'm getting 2 items being added instead of one.
Any tip on what I'm doing would be massively appreciated! I know I'm close but I've been stuck on this for a while now and can't figure out what I'm doing wrong!
Preface: You're using map incorrectly. If you're not using the array that map builds and returns, there's no reason to use it; just use a loop or forEach. More in my post here. And one reason to use an old-fashioned for loop is that you're in control of iteration, which matters because...
However, it's acting weirdly and sometimes I'm getting 2 items being added instead of one.
That's because you're inserting into the array being looped by the map, so on the next pass, it picks up the entry you're adding.
If you do a simple loop, you can easily avoid that by incrementing the index when you insert, or by looping backward; here's the looping backward approach:
const addItemToLevelTwoArray = (uniqueID, arrayID) => {
const reportObject = {
id: arrayID,
title: "",
};
for (const section of data) {
for (let reportIndex = section.content.length - 1; reportIndex >= 0; --reportIndex) {
const report = section.content[reportIndex];
if (report.id === uniqueID) {
section.content.splice(reportIndex, 0, reportObject);
}
}
}
};
Because we're looping backward, we won't pick up the entry we just added on the next pass.
Since the outer loop doesn't have that problem, I used the more convenient for-of.
Since you asked about map, if you do use the array map returns, you can do this by returning an array with the two entries, and then calling flat on the array map builds. (This only works if the array doesn't already contain arrays, because they'll get flattened to.) This is common enough that it's combined in one function: flatMap. It's not what I'd do (I'd do a loop), but it's certainly feasible. Sticking with forEach and flatMap rather than using for-of and for:
const addItemToLevelTwoArray = (uniqueID, arrayID) => {
const reportObject = {
id: arrayID,
title: "",
}
data.forEach(section => {
section.content = section.content.flatMap(report => {
if (report.id === uniqueID) {
// Return the new one and the old one
return [reportObject, report];
}
// Return just the old one
return report;
});
});
};
That assumes it's okay to modify the section object. If it isn't, Alberto Sinigaglia's answer shows creating a new replacement object instead, which is handy in some sitautions.
You can just use flatMap:
const data = [
{
content: [
{
id: 1,
title: "a"
},{
id: 3,
title: "c"
},
]
}
]
const addItemToLevelTwoArray= (uniqueID, arrayID )=> {
const reportObject = {
id:arrayID,
title:'',
}
return data.map(section=> {
return {
...section,
content: section.content.flatMap( report =>
report.id === uniqueID
? [reportObject, report]
: report
)
}
}
)
}
console.log(addItemToLevelTwoArray(3, 2))
The following will extend the inner array .contentwithout modifying the original array data:
const data = [ {id: 0,title:'main',content:[{id:1,title:'Hello'},
{id:2,title:"World"}] } ];
const addItemToLevelTwoArray= (uniqueID, arrayID )=> {
const reportObject = {
id:arrayID,
title:'something new!',
}
return data.map(d=>(
{...d, content:d.content.reduce((acc, rep)=>{
if(rep.id===uniqueID) acc.push(reportObject);
acc.push(rep)
return acc;
},[]) // end of .reduce()
})); // end of .map()
}
const res=addItemToLevelTwoArray(1,123);
console.log(res);
I am pulling from a SQL table device, and displaying its content to a table by mapping from device. I am trying to add a column that pulls information from another SQL table, group, but I haven't figured out how to adjust the mapping in order to pull from both device and group. I am sure the issue is caused since group isn't declared in this scope but I cannot solve how it should be declared in this portion of the script.
Both tables shared a common column, group_id, and I have added useSelector for both:
const device = useSelector((state) => state.device);
const group = useSelector((state) => state.group);
<Table
tableHeaderColor="warning"
tableHead={['Device Name', 'Location', 'Group', 'Release Version']}
tableData={device.deviceData.map((device) => {
return [
device['device_name'],
device['location_name'],
group['group_name'],
device['release'],
];
})}
/>
An alternative fix I have tried is finding the group_name since both tables device and group share the group_id column, but it causes a group.find is not a function error. I am unsure if my syntax is incorrect, as I'm working from this site as a resource.
tableData={device.deviceData.map((device) => {
return [
device['device_name'],
device['location_name'],
group.find(group => group.group_id === device.group_id).group_name,
device['release'],
];
})}
Many thanks for any advice
UPDATE:
Thank you for the answers and comments so far. Here is some additional information:
The reducer does contain the initial state empty array for group (groupdata)
const initialState = {
groupData: [],
result: '',
};
Here are the screenshots of the SQL tables device and group. They do not have the same number of entries, as group lists the groups that a number of devices can be assigned to. Hence there are many more entries under device than group.
device SQL table
group SQL table
If you using group.find you need to make sure the reducer contains the initial state empty array for group
Doing this in a map in the actual JSX might get a bit confusing since you're pulling from two different datasources. Also, assuming that the array in
device.deviceData and the group array share the same number of entries and correlate to each other, searching in group on each loop through device.deviceData seems like an unnecessary performance hit. My preference might be to create my source data in a plain for loop outside of the return JSX, and then just plug it in directly:
let tableData = []
for (let i = 0; i < device.deviceData.length; i += 1) {
const currentDevice = device.deviceData[i];
const currentGroup = group[i];
const entry = [
currentDevice['device_name'],
currentDevice['location_name'],
currentGroup['group_name'],
currentDevice['release'],
];
tableData.push(entry);
}
Then I would simply pass tableData to the <Table /> tableData prop.
I think group is an object and has a structure similar to device.deviceData. By that assumption, group.groupData should be the array which will be useful to us.
What we can do is build a Map of group_id -> group_name which we use later for getting relevant device group_name in Table component.
Here is a code snippet which should give you an idea (this is a JS based answer. I think your use-case can make use of joins at sql level for less work here) :-
const device = {
deviceData: [{
group_id: 1
},
{
group_id: 2
}
]
}
const group = {
groupData: [{
group_id: 1,
group_name: 'Group-1'
},
{
group_id: 2,
group_name: 'Group-2'
}
]
}
const buildMap = () => {
const gMap = {};
for (const {
group_id,
group_name
} of group.groupData) {
gMap[group_id] = group_name;
}
return gMap;
}
const groupMap = buildMap();
device.deviceData.forEach(d => console.log(groupMap[d.group_id]));
I have tagsList which has about 20 tags, and termIds which is an array of up to 3 tag ids.
I'm trying to find the tags that match the ids in termIds in the tagsList, then set their borders. Looking to avoid for loops and object-oriented programming in favor of a functional programming solution using Ramda curry.
A tag in tagsList looks like :
{
term: 'hi',
id: 123
}
And termIds could look like [123, 345, 678]
When I find an id that matches, I give that tag a new key border1:true, border2:true etc...
Goal:
There is a list of tags, I have another array of termIds, goal is to see if any of the tags in the tagsList have an id that matches the termIds. If so give it a border1, if there are 2, then the 2nd gets border2 and finally 3 gets border 3.
What I tried first:
const checkId = _.curry((term_id, tag) => {
if (tag.id === term_id) {
console.log('match found!', tag)
}
});
const matchId = checkId(termIds);
const coloredTags = R.map(matchId, tagsList);
console.log('coloredTags', coloredTags)
return tagsList;
However this did not work because I am preloading the entire termIds array into the checkId function. When instead I want to preload it with the individual items.
Next I tried this which I thought would work but getting a strange error:
const matchId = R.forEach(checkId, termIds);
This seems a reasonable approach:
R.map(tag => {
const index = R.indexOf(tag.id, termIds);
return (index > -1) ? R.assoc('border' + (index + 1), true, tag) : tag
})(tagsList);
//=> [
// {id: 123, term: "hi", border1: true},
// {id: 152, term: "ho"},
// {id: 345, term: "hu", border2: true},
// {id: 72, term: "ha"}
// ]
Although it could probably be made points-free with enough effort, it would likely be much less readable.
You can see this in action on the Ramda REPL.
If you want to make this into a reusable function, you can do it like this:
const addBorders = R.curry((terms, tags) => R.map(tag => {
const index = R.indexOf(tag.id, terms);
return (index > -1) ? R.assoc('border' + (index + 1), true, tag) : tag
})(tags))
addBorders(termIds, tagsList)
(The call to curry is a Ramda habit. It means you can call addBorders(termIds) and get back a reusable function that is looking for the tags. If you don't need that, you can skip the curry wrapper.)
This version is also on the Ramda REPL.
I think pure JS is enough to do it without Ramda. You just need a map :
var tagsList = [{term: 'hi', id: 123}, {term: 'ho', id: 152}, {term: 'hu', id: 345}, {term: 'ha', id: 72}];
var termIds = [123, 345, 678];
var i = 1;
var results = tagsList.map(x => {
if (termIds.indexOf(x.id) !== -1) x["border"+ (i++)] = true;
return x;
});
console.log(results);
Ah just figured it out, I had to curry the logic a 2nd time:
const matchId = R.curry((tag, term_id) => {
if (tag.id === Number(term_id)) {
console.log('match found!', tag)
}
});
const curried = R.curry((termIds, tag) => {
return R.map(matchId(tag), termIds);
});
const coloredTags = R.map(curried(termIds), tagsList);
console.log('coloredTags', coloredTags)
return tagsList;
So at the coloredTags line, a tag from tagsLists goes into the curried(termIds). Ramda functions accept params from right to left.
curried(termIds) is already preloaded with the termIds array. So next in the const curried = line, the termIds array and single tag make it in and the tag gets sent along into the next curried function matchId, also the termIds array is placed in the R.map. Ramda list functions accept the Array of data as the right param.
Finally in matchId I can make my check!
UPDATE
So the above answers the question I asked, about how to curry an item from an Array. However it caused a bug in my app. Since the termIds array could hold up to 3 items, the coloredTags R.map will run up to 3 times and create duplicate tags in my tagsList.
So just for completeness this is how I solved my in problem, much simpler and didn't need to use a double curried function.
const setTagColors = (tagsList, state) => {
const setBorder = (tag) => {
if (tag.id === Number(state.term_id_1)) {
tag.border1 = true;
} else if (tag.id === Number(state.term_id_2)) {
tag.border2 = true;
} else if (tag.id === Number(state.term_id_3)) {
tag.border3 = true;
}
return tag;
};
const coloredTags = R.map(setBorder, tagsList);
return state.term_id_1 ? coloredTags : tagsList;
};