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;
};
Related
I have an order form where a user can add JOBS and each JOB is broken down into LABOR lines and PARTS lines like so:
JOB1
LABOR1
LABOR2
PART1
PART2
JOB2
LABOR1
LABOR2
PART1
PART2
I have set up the data as an array of objects where each JOB is an object and within that JOB object there are LABOR and PART arrays with objects for each line. I can add new JOBS and LABOR and PARTS to each JOB but I cannot, for the life of me, figure out how to remove a single LABOR or PART line from the nested arrays.
I have:
const jobs = ref([
{
jobId: idGen(), // idGen() is simply a unique ID generator
jobTitle: 'Job 1',
jobLabor: [],
jobPart: []
}
])
const addJob = () => { // This function works fine and adds a new JOB to the order
jobs.value.push({
jobId: idGen(),
jobTitle: 'New Job',
jobLabor: [],
jobPart: []
})
}
const addLabor = (addToJobId) => {
const job = jobs.value.filter(job => jobId === addToJobId)
job[0].jobLabor.push({
laborId: idGen(),
description: '',
quantity: 1,
amount: 0 // This function adds a LABOR line to job
})
}
const addPart = (addToJobId) => {
const job = jobs.value.filter(job => jobId === addToJobId)
job[0].jobPart.push({
laborId: idGen(),
description: '',
quantity: 1,
amount: 0 // This function adds a PART line to job
})
}
const deleteJob = (deleteJobId) => { // This function deleted a WHOLE JOB
jobs.value = jobs.value.filter(job => job.jobId !== deleteJobId)
if (jobs.value.length === 0) { addJob() } // Making sure the form is not empty/unusable
}
const deleteLaborLine = (deleteFromJobId, deleteLaborId) => {
const job = jobs.value.filter(job => job.jobId === deleteFromJobId)
job[0].jobLabor.filter(labor => labor.laborId !== deleteLaborId)
}
I can't get deleteLaborLine(jobId, laborId) to work. I just want the function to delete ONE labor line from an array of other labor lines within a jobs array. I hope this all makes sense. Any help or insight is appreciated.
1. const deleteLaborLine = (deleteFromJobId, deleteLaborId) => {
2. const job = jobs.value.filter(job => job.jobId === dleteFromJobId)
3. job[0].jobLabor.filter(labor => labor.laborId !== deleteLaborId)
4. }
Please check the spelling of deleteFromJobId on line no 2
change from dleteFromJobId to deleteFromJobId
and try again, it should work fine.
Thanks.
Filter doesn't mutate array, you need to explicitly assign its result.
Try replacing last line in deleteLaborLine function with job[0].jobLabor = job[0].jobLabor.filter(labor => labor.laborId !== deleteLaborId).
1. const deleteLaborLine = (deleteFromJobId, deleteLaborId) => {
2. const job = jobs.value.filter(job => job.jobId === dleteFromJobId)
3. job[0].jobLabor.filter(labor => labor.laborId !== deleteLaborId)
4. }
Please check the spelling of deleteFromJobId on line no 2
change from dleteFromJobId to deleteFromJobId
and try again, it should work fine.
And second issue is that when you are filtering jobLabor array in line no 3 it will not assign any filter unless you store it
you should follow this or slice method of javascript
job[0].jobLabor = job[0].jobLabor.filter(labor => labor.laborId !== deleteLaborId)
Thanks.
The issue with the code is in the line
job[0].jobLabor.filter(labor => labor.laborId !== deleteLaborId)
Array.filter simply return an array with the specified condition. As you have written, this will return an array of jobLabour where the laborId !== deleteLaborId, but this will not change your original job[0].jobLabor array.
You can either reassign job[0].jobLabor with the result of this filter as mentioned by #MatijaSirk, or you can simply splice the array after finding the index to delete the node from job[0].jobLabor.
const deleteLaborLine = (deleteFromJobId, deleteLaborId) => {
const job = jobs.value.filter(job => job.jobId === dleteFromJobId)
const index = job[0].jobLabor.findIndex(labor => labor.laborId !== deleteLaborId)
if (index !== -1) {
job[0].jobLabor.splice(index, 1)
}
}
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);
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.
Here is some code from I project I am working in:
const profile = userdataDocs
.filter(isValidUserdataDocument)
.find((document: ICouchDBDocumentDoc) => document._id === profileId);
if (profile) {
return {
id: hashSensitive(profile._id, environment),
type: profile.type,
creationDate: profile.creationDate,
updatedDate: profile.updatedDate,
entityVersion: profile.entityVersion,
};
}
Here is how I would like to have my code look:
return userdataDocs
.filter(isValidUserdataDocument)
.filter((document: ICouchDBDocumentDoc) => document._id === profileId)
.map((profile: ICouchDBDocumentDoc) => ({
id: hashSensitive(profile._id, environment),
type: profile.type,
creationDate: profile.creationDate,
updatedDate: profile.updatedDate,
entityVersion: profile.entityVersion,
}))
.slice(0, 1);
But I get feedback from the rest of my team that I should not use filter because it will continue searching after having found an item. Premature optimization in mind, but still a pretty valid and popular opinion.
Is there some other array method (or altogether different solution) that I can use to write code the way I want, with 'pipes', without getting the performance penalty of moving from find to filter?
Also let me know if I am an idiot and should let go of the pipe dream (pun intended).
Let me start that I like the first solution. In my opinion, it looks good.
But if you are really desperate for a solution that fulfills your pipe dream
const array = [10, 20, 30];
function singleMapFind(args, fn) {
const currentArray = args[2];
const duplicate = [...currentArray];
currentArray.splice(1, currentArray.length - 1);
return duplicate.find(fn);
}
const modified = array.map((...args) => singleMapFind(args, (e) => e > 20));
I would never use it though. Wish you luck with the PR.