Match object by array keys - Lodash - javascript

Hello i have an array of objects that looks like:
[
{
"id": "ae588a6b-4540-5714-bfe2-a5c2a65f547b",
"name": "Peter casa",
"skills": [
"javascript",
"es6",
"nodejs"
]
},
{
"id": "ae588a6b-4540-5714-bfe2-a5c2a65f547a",
"name": "Peter Coder",
"skills": [
"javascript",
"es6",
"nodejs",
"express"
]
}
]
and i'm trying to get the best match based on skills, i'm using lodash and the idea is resolve:
passing "skills": [
"javascript",
"es6",
"nodejs",
"express"
] get "Peter Coder" instead of "Peter casa" because the first one match all the skills in the array given, is there any lodash method to perform this?
Edit:
In this particular example, the function needs to get the candidate with is a better fit, for instance, if i pass skills: "skills": [
"javascript",
"es6",
"nodejs"
] even if it is a perfect match for the first one i need to choose the second because it matches the skills and has more skills, so is a better match

With lodash you could use intersection as #Tuan Anh Tran suggested:
const users = [
{
id: 'ae588a6b-4540-5714-bfe2-a5c2a65f547b',
name: 'Peter casa',
skills: ['javascript', 'es6', 'nodejs'],
},
{
id: 'ae588a6b-4540-5714-bfe2-a5c2a65f547a',
name: 'Peter Coder',
skills: ['javascript', 'es6', 'nodejs', 'express'],
},
]
let skillsByUsers = {}
const skills = ['javascript', 'es6', 'nodejs', 'express']
users.map(user => {
const skillValue = _.intersection(user.skills, skills).length
skillsByUsersTwo = {
...skillsByUsersTwo,
[user.id]: skillValue
}
})
Without lodash:
let skillsByUsers = {}
const skills = ['javascript', 'es6', 'nodejs', 'express']
skills.map(skill => {
users.map(user => {
if (user.skills.includes(skill)) {
const userSkillValue = skillsByUsers[user.id] || 0
skillsByUsers = {
...skillsByUsers,
[user.id]: userSkillValue + 1,
}
}
})
})
they print exactly the same output:
lodash
{
'ae588a6b-4540-5714-bfe2-a5c2a65f547b': 3,
'ae588a6b-4540-5714-bfe2-a5c2a65f547a': 4
}
without lodash
{
'ae588a6b-4540-5714-bfe2-a5c2a65f547b': 3,
'ae588a6b-4540-5714-bfe2-a5c2a65f547a': 4
}

lodash has a method call intersection https://lodash.com/docs/#intersection
you can pass the array of skill and intersect it with each developer's skill. => get the length of that intersection as score and return the one with highest score.

Related

MongoDB - Decrement a certain value in a nested object, but dont let it go below 0

My MongooseSchema (simplified):
_id: (ObjectId)
storage: [
{
location: String
storedFood:[
{
"name": String
"code": String
"weight": Number
}
]
}
]
I want to dec the weight, but not below 0. There is a stackoverflow answer that does this(The second answer from #rpatel). Great! The problem here is that he uses a update-pipeline WITHOUT nested documents. I didnt find a source where I could learn something about mongdob pipelines and nested object (If you have any please let me know, I really want to learn complex pipelines)
Is someone here who could adapt the following code to decrement weight,where location equals for example "Dubai" and code equals for example "38102371982" ?
Code from #rpatel:
Mongo-Playground
Example-Document:
{
"key": 1,
value: 30
}
db.collection.update({},
[
{
$set: {
"value": {
$max: [
0,
{
$subtract: [
"$value",
20
]
}
]
}
}
}
])
A ready playground.
One option is to use double $map with $cond in order to get into the double nested array item:
db.collection.update(
{storage: {$elemMatch: {location: wantedLoc}}},
[{$set: {
storage: {
$map: {
input: "$storage",
as: "st",
in: {$cond: [
{$eq: ["$$st.location", wantedLoc]},
{location: "$$st.location",
storedFood: {$map: {
input: "$$st.storedFood",
in: {$cond: [
{$eq: ["$$this.code", wantedCode]},
{$mergeObjects: [
"$$this",
{weight: {$max: [0, {$subtract: ["$$this.weight", reduceBy]}]}}
]},
"$$this"
]}
}
}
},
"$$st"
]}
}
}
}}]
)
See how it works on the playground example

How do I make deduped array from object property?

I am looking for a way to loop over an array of objects and make a new array from the contents of an object property. See the array below. I want to make an array called topics (with no duplicates) from each of the topic properties on each object.
const data = [
{
topics [
"tutorial",
"JS",
"Video"
],
...
},
{
topics [
"tutorial",
"CSS",
"Testing"
],
...
},
{
topics [
"HTML",
"JS",
"Music"
],
...
}
]
I was thinking of:
let topics = []
data.forEach((item) => {
topics.push(item.topics, ...topics)
})
You need to use flatMap method to flat the topics together, and with new Set(Array) you will get the unique values.
const data = [
{
topics: [
"tutorial",
"JS",
"Video"
],
},
{
topics: [
"tutorial",
"CSS",
"Testing"
],
},
{
topics: [
"HTML",
"JS",
"Music"
],
}
]
const topics = new Set(data.flatMap(item => item.topics))
console.log(Array.from(topics))

How to extract and process nested arrays?

I'm struggling to extract rules array from a JSON test report. Can anyone point me in the right direction?
// input
const json = {
"data": {
"allTests": [
{
"id": "foo",
"testStatus": "PASS",
"ruleList": [
{
"outcome": "PASS",
"name": "Image should be awesome"
}
]
},
{
"id": "bar",
"testStatus": "FAIL",
"ruleList": [
{
"outcome": "HARD_FAIL",
"name": "Image should be awesome"
}
]
},
{
"id": "baz",
"testStatus": "FAIL",
"ruleList": [
{
"outcome": "SOFT_FAIL",
"name": "Image should be awesome"
}
]
},
]
}
}
Expected outcome:
[{
"name": "Image should be awesome",
"HARD_FAIL": 1,
"SOFT_FAIL": 1,
"PASS": 1
}]
(I took the liberty to work only with json.data.allTests)
What I would do:
Extract all rules in all ruleList with chain
While you do that, revert the outcome property e.g. {outcome: 'PASS'} => {PASS: 1}
Group by name, summing up all outcomes (assuming that e.g. PASS: 2 is possible)
Extract all values
const with_ramda =
pipe(
chain(x => x.ruleList.map(({outcome, name}) => ({[outcome]: 1, name}))),
reduceBy(({name: _, ...acc}, x) => mergeWith(add, acc, x), {}, prop('name')),
values);
console.log(with_ramda(input));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>
<script>const {pipe, chain, reduceBy, mergeWith, add, prop, values} = R;</script>
<script>
const input =
[ { "id": "foo"
, "testStatus": "PASS"
, "ruleList":
[ { "outcome": "PASS"
, "name": "Image should be awesome"
}
]
}
, { "id": "bar"
, "testStatus": "FAIL"
, "ruleList":
[ { "outcome": "HARD_FAIL"
, "name": "Image should be awesome"
}
]
}
, { "id": "baz"
, "testStatus": "FAIL"
, "ruleList":
[ { "outcome": "SOFT_FAIL"
, "name": "Image should be awesome"
}
]
}
];
</script>
In case you're interested a vanilla solution is also possible and isn't necessarily more complicated:
const with_vanillajs =
xs =>
Object.values(
xs.flatMap(x => x.ruleList)
.reduce((acc, {outcome, name}) =>
( acc[name] = acc[name] || {name}
, acc[name][outcome] = (acc[name][outcome] || 0) + 1
, acc), {}));
console.log(with_vanillajs(input));
<script>
const input =
[ { "id": "foo"
, "testStatus": "PASS"
, "ruleList":
[ { "outcome": "PASS"
, "name": "Image should be awesome"
}
]
}
, { "id": "bar"
, "testStatus": "FAIL"
, "ruleList":
[ { "outcome": "HARD_FAIL"
, "name": "Image should be awesome"
}
]
}
, { "id": "baz"
, "testStatus": "FAIL"
, "ruleList":
[ { "outcome": "SOFT_FAIL"
, "name": "Image should be awesome"
}
]
}
];
</script>
Flatten the ruleList arrays using R.chain, group them by the name, and convert to pairs of [name, array of rules], map the pairs, and convert each pair to object. Use R.countBy to calculate the outcomes' scores:
const { countBy, prope, pipe, chain, prop, groupBy, toPairs, map } = R
const countByOutcome = countBy(prop('outcome'))
const fn = pipe(
chain(prop('ruleList')), // flatten the ruleList
groupBy(prop('name')), // group by the name
toPairs, // convert to [name, values] pairs
map(([name, val]) => ({ // map the pairs to objects
name,
...countByOutcome(val) // count the outcomes
})),
)
const input = [{"id":"foo","testStatus":"PASS","ruleList":[{"outcome":"PASS","name":"Image should be awesome"}]},{"id":"bar","testStatus":"FAIL","ruleList":[{"outcome":"HARD_FAIL","name":"Image should be awesome"}]},{"id":"baz","testStatus":"FAIL","ruleList":[{"outcome":"SOFT_FAIL","name":"Image should be awesome"}]}]
console.log(fn(input))
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous"></script>
Adding an answer similar to the one from Ori Drori because it's just enough different to be interesting.
First of all, the one example you gave is really not enough to demonstrate exactly what you want to do, but several other answers make the same assumptions I do, so I'm guessing we're all right. But to be explicit, I'm assuming that:
the 1s in the result are counts and not boolean markers that simply indicate that the outcome is included. My test data checks this by including a second "PASS" scenario for the name "Image should be awesome".
There can be more than one name in the data. Your example shows only one. I added another to my test case.
The output objects are grouped by these names, and not by, for example, by other fields parallel to allTests.
With those assumptions, I wrote this:
const transform = pipe (
path (['data', 'allTests']),
chain (prop ('ruleList')),
groupBy (prop ('name')),
map (pluck ('outcome')),
map (countBy (identity)),
toPairs,
map (([name, rest]) => ({name, ...rest})),
)
const json = {data: {allTests: [{id: "foo", testStatus: "PASS", ruleList: [{outcome: "PASS", name: "Image should be awesome"}, {outcome: "HARD_FAIL", name: "Image should be chocolate"}]}, {id: "bar", testStatus: "FAIL", ruleList: [{outcome: "HARD_FAIL", name: "Image should be awesome"}]}, {id: "baz", testStatus: "FAIL", ruleList: [{outcome: "SOFT_FAIL", name: "Image should be awesome"}, {outcome: "PASS", name: "Image should be awesome"}]}]}}
console .log (transform (json))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
<script> const {pipe, path, chain, prop, groupBy, map, pluck, countBy, identity, toPairs} = R </script>
I tend to write transformation functions step-by-step, each time trying to move my output a little closer to my final goal. So I wrote this going down the pipeline. There may be easier ways to change these, or combine steps. Ori Drori extracted a useful function, and we could do so as well here. But I think it reads reasonably well, and you can check what it does by commenting out any tail of functions in the pipeline to see the intermediate results.
If you're a point-free fetishist, you can replace the last line of the function with map (apply (useWith (mergeRight, [objOf('name')]))). I don't see the need, but that would lead to an entirely point-free solution.
Well this might work:
var Result = [{}];
json.data.allTests.forEach (Entry => {
var Rules = Entry.ruleList || [];
Rules.forEach (Rule => {
var { outcome, name } = Rule;
console.log ({ outcome, name });
Result [ 0 ] [ outcome ] = 1;
Result [ 0 ].name = name;
});
});
console.log ({ Result });

Query Comparing Arrays of Objects

I'm wondering how I can compare arrays of (nested) objects in Mongoose.
Considering the data below, I would like to get results when the name properties match. Could anyone help me with this?
Organisation.find( {
$or: [
{ "category_list": { $in: cat_list } },
{ "place_topics.data": { $in: place_tops } }
]
}
)
Let's say that this is the data stored in my MongoDB:
"category_list": [
{
"id": "197750126917541",
"name": "Pool & Billiard Hall"
},
{
"id": "197871390225897",
"name": "Cafe"
},
{
"id": "218693881483234",
"name": "Pub"
}
],
"place_topics": {
"data": [
{
"name": "Pool & Billiard Hall",
"id": "197750126917541"
},
{
"name": "Pub",
"id": "218693881483234"
}
]
}
And let's say that these are the arrays I want to compare against (almost the same data):
let cat_list = [
{
"id": "197750126917541",
"name": "Pool & Billiard Hall"
},
{
"id": "197871390225897",
"name": "Cafe"
},
{
"id": "218693881483234",
"name": "Pub"
}
]
let place_tops = [
{
"name": "Pool & Billiard Hall",
"id": "197750126917541"
},
{
"name": "Pub",
"id": "218693881483234"
}
]
When there are "multiple conditions" required for each array element is when you actually use $elemMatch, and in fact "need to" otherwise you don't match the correct element.
So to apply multiple conditions, you would rather make an array of conditions for $or instead of shortcuts with $in:
Organizations.find({
"$or": [].concat(
cat_list.map( c => ({ "category_list": { "$elemMatch": c } }) ),
place_tops.map( p => ({ "place_topics": { "$elemMatch": p } }) )
)
})
However, if you take a step back and think logically about it, you actually named one of the properties "id". This would generally imply in all good practice that the value is in fact ""unique".
Therefore, all you really should need to do is simply extract those values and stick with the original query form:
Organizations.find({
"$or": [
{ "category_list.id": { "$in": cat_list.map(c => c.id) } },
{ "place_topics.id": { "$in": place_tops.map(p => p.id) } }
]
})
So simply mapping both the values and the property to "match" onto the "id" value instead. This is a simple "dot notation" form that generally suffices when you have one condition per array element to test/match.
That is generally the most logical approach given the data, and you should apply which one of these actually suits the data conditions you need. For "multiple" use $elemMatch. But if you don't need multiple because there is a singular match, then simply do the singular match

Change MongoDB find results dynamically

I'm using mongoose with node.js.
Let's say I have 'Posts' DB where each document in it is a post.
Each post has a 'ReadBy' array which holds names of users that had read this post.
When I'm searching for documents in this DB, I want to "change" the 'ReadBy' value to show by Boolean value if the user that is searching for it is in this array or not.
For example, let's say these are 2 documents that are in this DB:
{ "PostName": "Post Number 1", "ReadBy": ["Tom", "John", "Adam"] }
{ "PostName": "Post Number 2", "ReadBy": ["John", "Adam"] }
If I'm user 'Tom', I want to get the results like this:
[
{
"PostName": "Post Number 1",
"ReadBy": true,
},
{
"PostName": "Post Number 2",
"ReadBy": false,
}
]
Now, I know that I can get the documents and go over each one of them with forEach function, and then use forEach again on the "ReadBy" array and change this field.
I'm asking if there is more efficient way to do it in the mongoDB query itself, or some other way in the code.
If there is another way with mongoose - even better.
Using mongoDb $setIntersection in aggregation you get the result like this :
db.collectionName.aggregate({
"$project": {
"ReadBy": {
"$cond": {
"if": {
"$eq": [{
"$setIntersection": ["$ReadBy", ["Tom"]]
},
["Tom"]
]
},
"then": true,
"else": false
}
},
"PostName": 1
}
})
So above working first like this
{ $setIntersection: [ [ "Tom", "John", "Adam"], [ "Tom"] ] }, return [ "Tom"]
{ $setIntersection: [ [ "John", "Adam"], [ "Tom"] ] }, return [ ]
and $eq to check whether setIntersection results matched with ["Tom"] if yes then return true else false
You can try something similar to
var unwind = {"$unwind": "$ReadBy"}
var eq = {$eq: ["$ReadBy", "Bob"]}
var project = {$project: {PostName: 1, seen: eq}}
db.posts.aggregate([unwind, project])
Just notice that you solution is highly inefficient. Both for storing the data ( growing array) and for searching.

Categories

Resources