How to extract and process nested arrays? - javascript

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 });

Related

Destructuring object with inner array without all keys

I have an object like this:
const objBefore:
{
"id": "3pa99f64-5717-4562-b3fc-2c963f66afa1",
"number": "5000",
"enabled": true,
"classes": [
{
"id": "2fc87f64-5417-4562-b3fc-2c963f66afa4",
"name": "General"
},
{
"id": "7ffcada8-0215-4fb0-bea9-2266836d3b18",
"name": "Special"
},
{
"id": "6ee973f7-c77b-4738-b275-9a7299b9b82b",
"name": "Limited"
}
]
}
Using es6, I want to grab everything in the object except the name key of the inner classes array to pass it to an api.
So:
{
"id": "3pa99f64-5717-4562-b3fc-2c963f66afa1",
"number": "5000",
"enabled": true,
"classes": [
{"id": "2fc87f64-5417-4562-b3fc-2c963f66afa4"},
{"id": "7ffcada8-0215-4fb0-bea9-2266836d3b18"},
{"id": "6ee973f7-c77b-4738-b275-9a7299b9b82b"}
]
}
The closest I got was: let {id, number, enabled, classes: [{id}]} = objBefore;
But it only gets me one id in classes. I've tried spreading above using [...{id}] or [{...id}]. Same thing.
I find it challenging to get the right mental model for how to think about this when it's on multiple levels. In my mind, when I say [...{id}] I'm thinking, "I want the id property as an object in the outer classes array, but give me every id in the array!"
Clearly I'm not thinking about this correctly.
I've tried it using map to get that part but I'm still having trouble combining it back to the original to produce the desired result. for example:
let classIds = objBefore.classes.map(({id}) => {
return {
id
}
})
(Using the map syntax, how can I destructure in the function the other keys that are one level higher?)
To combine them I started trying anything and everything, :
let {id, number, enabled, classIds} = {objBefore, [...classIds]} // returns undefined for all
I'd prefer to do it in one statement. But if that's not possible, then what's a clean way to do it using map?.
You can't destructure and map at the same time in the way you're looking to do it. The main purpose of destructuring assignment is to extract data from an array/object and not for manipulating data. In your case, as you're after an object with the same keys/value as your original object, just with a different classes array, I would instead suggest creating a new object and spreading ... the original object into that. Then you can overwrite the classes array with a mapped version of that array:
const objBefore = { "id": "3pa99f64-5717-4562-b3fc-2c963f66afa1", "number": "5000", "enabled": true, "classes": [ { "id": "2fc87f64-5417-4562-b3fc-2c963f66afa4", "name": "General" }, { "id": "7ffcada8-0215-4fb0-bea9-2266836d3b18", "name": "Special" }, { "id": "6ee973f7-c77b-4738-b275-9a7299b9b82b", "name": "Limited" } ] };
const newObj = {
...objBefore,
classes: objBefore.classes.map(({id}) => ({id}))
};
console.log(newObj);
How about using simple util method with object destructuring, spread operator and map
const objBefore = {
id: "3pa99f64-5717-4562-b3fc-2c963f66afa1",
number: "5000",
enabled: true,
classes: [
{
id: "2fc87f64-5417-4562-b3fc-2c963f66afa4",
name: "General",
},
{
id: "7ffcada8-0215-4fb0-bea9-2266836d3b18",
name: "Special",
},
{
id: "6ee973f7-c77b-4738-b275-9a7299b9b82b",
name: "Limited",
},
],
};
const process = ({ classes, ...rest }) => ({
...rest,
classes: classes.map(({ id }) => ({ id })),
});
console.log(process(objBefore))
In one line, you could do this:
const objAfter = { ...objBefore, classes: objBefore.classes.map(item => ({ id: item.id })) };
Or, if you prefer:
const objAfter = {...objBefore, classes: objBefore.classes.map(({id}) => ({id}))};
There isn't any way in object destructing to copy an entire array of objects into a different array of objects by removing properties so you use .map() for that.

Using Lodash to get value by It's key while some keys don't exist

How to use Lodash while I am mapping through an array to get and assign the relevant values?
This is what my unfiltered array looks like, I want to extract the ID and name[x].text into an array. But as you can see some objects do not have french text in them, In that case I want to take the English text instead.
const response = {
"data": {
"Animals": [
{
"id": "1",
"name": [
{
"language": "en-US",
"text": "Horse"
}
]
},
{
"id": "2",
"name": [
{
"language": "en-US",
"text": "Pig"
}
]
},
{
"id": "3",
"name": [
{
"language": "en-US",
"text": "Cat"
},
{
"language": "fr-CA",
"text": "Un Chat"
}
]
},
{
"id": "4",
"name": [
{
"language": "en-US",
"text": "Dog"
},
{
"language": "fr-CA",
"text": "Un Chien"
}
]
}
]
}
}
console.log(response.data.Animals.map(animal => animal.name[0].text)); // If I do this I get all the English text, but If I map the same with index of the French, I get undefined error and It crashed my application.
This is more suitable as It returns the French and English text but there are two improvements I want to make,
Get the value from it's key ("en-US" and "fr-CA") so that I know for sure I have the correct value.
I am mapping to populate a table, So I do not need a new array with values because I don't want to lose the index if the parent array, If text it not available it should go find the English text.
This is an extra question but it will help me learn to use Lodash better, Incase I want to set the value(either in En or Fr), how would I do that?
arr.map((obj) => {
const id = obj.id;
const englishtext = _.get(obj, 'name[0].text', 'N/A');
const frenchtext = _.get(obj, 'name[1].text', englishtext);
return { id, englishtext, frenchtext };
}
UPDATE:
I apologize my question was unclear, to be honest I am a bit confused as well. So here is the expected output I am looking for. So when I map the given variable response, I want an another array that looks like this,
EXPECTED OUTPUT:
[
{
id: 1,
engname: Horse,
frenchname: Horse (because french is not available),
},
.
.
.
{
id:3,
engname:Cat,
frenchname: Un Chat, (because the French array is there this time)
}
.
.
.
and so on..
]
I would post a non-Lodash version of solution because I don't find Lodash necessary in this case. But I know you asked for Lodash solution, so ignore this if you must use Lodash.
If I understand your question correctly, the rule set you want to employ for extracting the values is the following:
If there is a French text, use that regardless of the existence of the English text. Also, the French text always comes after the English text in the array (French text always has the index of -1).
If there is no French text, use the English text (English text is guaranteed to be there for all items).
In that case, I think you could use either Array.prototype.pop() or Array.prototype.at() to achieve what you want.
Array.prototype.pop() solution
response.data.Animals.map(animal => animal.name.pop().text)
Pros:
Widely available among all the modern browsers.
Cons:
This mutates the original data and changes the length of the original array.
Array.prototype.at() solution
response.data.Animals.map(animal => animal.name.at(-1).text)
Pros:
This operation is immutable, so original data is intact.
Cons:
Some browsers might not support this.
If you are open to non-Lodash solutions, I suppose you could use one of these.
Update
Thank you #WildThing for updating the question. I think you can use this to achieve the mapping you want:
response.data.Animals.map(animal => {
engName = animal.name.find(name => name.language === "en-US");
frenchNameIfExists = animal.name.find(name => name.language === "fr-CA");
return {
id: animal.id,
engname: engName.text,
frenchname: (frenchNameIfExists || engName).text,
}
})
May be there is no need lodash:
const toMyArray = (arr, frenchnameCode = "fr-CA", engnameCode = "en-US") => {
return arr.map(({ id, name }) => {
const engname = name
.find(({ language }) => language === engnameCode)
.text;
const frenchname = name
.find(({ language }) => language === frenchnameCode)
?.text;
return { id, engname, frenchname: frenchname ?? engname }
});
};
console.log(toMyArray(response.data.Animals));
// [
// {id: '1', engname: 'Horse', frenchname: 'Horse'},
// {id: '2', engname: 'Pig', frenchname: 'Pig'},
// {id: '3', engname: 'Cat', frenchname: 'Un Chat'},
// {id: '4', engname: 'Dog', frenchname: 'Un Chien'},
// ]

How to get all values of given specific keys (for e.g: name) without loop from json?

I want to fetch all the names and label from JSON without loop. Is there a way to fetch with any filter method?
"sections": [
{
"id": "62ee1779",
"name": "Drinks",
"items": [
{
"id": "1902b625",
"name": "Cold Brew",
"optionSets": [
{
"id": "45f2a845-c83b-49c2-90ae-a227dfb7c513",
"label": "Choose a size",
},
{
"id": "af171c34-4ca8-4374-82bf-a418396e375c",
"label": "Additional Toppings",
},
],
},
]
}
When you say "without loops" I take it as without For Loops. because any kind of traversal of arrays, let alone nested traversal, involve iterating.
You can use the reduce method to have it done for you internally and give you the format you need.
Try this :
const data = {
sections: [
{
id: "62ee1779",
name: "Drinks",
items: [
{
id: "1902b625",
name: "Cold Brew",
optionSets: [
{
id: "45f2a845-c83b-49c2-90ae-a227dfb7c513",
label: "Choose a size"
},
{
id: "af171c34-4ca8-4374-82bf-a418396e375c",
label: "Additional Toppings"
}
]
}
]
}
]
};
x = data.sections.reduce((acc, ele) => {
acc.push(ele.name);
otherName = ele.items.reduce((acc2, elem2) => {
acc2.push(elem2.name);
label = elem2.optionSets.reduce((acc3, elem3) => {
acc3.push(elem3.label);
return acc3;
}, []);
return acc2.concat(label);
}, []);
return acc.concat(otherName);
}, []);
console.log(x);
Go ahead and press run snippet to see if this matches your desired output.
For More on info reduce method
In the context of cJSON
yes, we can fetch the key value for any of the object.
1 - each key value is pointed by one of the objects. will simply fetch that object and from there will get the key value.
In the above case for
pre-requisition: root must contain the json format and root must be the cJSON pointer. if not we can define it and use cJSON_Parse() to parse the json.
1st name object is "sections" will use
cJSON *test = cJSON_GetObjectItem(root, "sections");
char *name1 = cJSON_GetObjectItem(test, "name" )->valuestring;
2nd name key value
cJSON *test2 = cJSON_GetObjectItem(test, "items");
char *name2 = cJSON_GetObjectItem(tes2, "name")->valuestring;
likewise, we can do for others as well to fetch the key value.

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

How to extract multiple hashtags from a JSON object?

I am trying to extract "animal" and "fish" hashtags from the JSON object below. I know how to extract the first instance named "animal", but I have no idea how to extract both instances. I was thinking to use a loop, but unsure where to start with it. Please advise.
data = '{"hashtags":[{"text":"animal","indices":[5110,1521]},
{"text":"Fish","indices":[122,142]}],"symbols":[],"user_mentions":
[{"screen_name":"test241","name":"Test
Dude","id":4999095,"id_str":"489996095","indices":[30,1111]},
{"screen_name":"test","name":"test","id":11999991,
"id_str":"1999990", "indices":[11,11]}],"urls":[]}';
function showHashtag(data){
i = 0;
obj = JSON.parse(data);
console.log(obj.hashtags[i].text);
}
showHashtag(data);
Use Array.prototype.filter():
let data = '{"hashtags":[{"text":"animal","indices":[5110,1521]},{"text":"Fish","indices":[122,142]}],"symbols":[],"user_mentions":[{"screen_name":"test241","name":"Test Dude","id":4999095,"id_str":"489996095","indices":[30,1111]}, {"screen_name":"test","name":"test","id":11999991, "id_str":"1999990", "indices":[11,11]}],"urls":[]}';
function showHashtag(data){
return JSON.parse(data).hashtags.filter(e => /animal|fish/i.test(e.text))
}
console.log(showHashtag(data));
To make the function reusable, in case you want to find other "hashtags", you could pass an array like so:
function showHashtag(data, tags){
let r = new RegExp(tags.join("|"), "i");
return JSON.parse(data).hashtags.filter(e => r.test(e.text))
}
console.log(showHashtag(data, ['animal', 'fish']));
To get only the text property, just chain map()
console.log(showHashtag(data, ['animal', 'fish']).map(e => e.text));
or in the function
return JSON.parse(data).hashtags
.filter(e => /animal|fish/i.test(e.text))
.map(e => e.text);
EDIT:
I don't really get why you would filter by animal and fish if all you want is an array with ['animal', 'fish']. To only get the objects that have a text property, again, use filter, but like this
let data = '{"hashtags":[{"text":"animal","indices":[5110,1521]},{"text":"Fish","indices":[122,142]}],"symbols":[],"user_mentions":[{"screen_name":"test241","name":"Test Dude","id":4999095,"id_str":"489996095","indices":[30,1111]}, {"screen_name":"test","name":"test","id":11999991, "id_str":"1999990", "indices":[11,11]}],"urls":[]}';
function showHashtag(data){
return JSON.parse(data).hashtags
.filter(e => e.text)
.map(e => e.text);
}
console.log(showHashtag(data));
For me, Lodash can be of great use here, which have different functions in terms of collections. For your case i'd use _.find function to help check the array and get any of the tags with the creteria passed in as second argument like so:
.find(collection, [predicate=.identity], [fromIndex=0])
source npm package
Iterates over elements of collection, returning the first element
predicate returns truthy for. The predicate is invoked with three
arguments: (value, index|key, collection).
with your case this should work
var data = '{ "hashtags": [ { "text": "animal", "indices": [ 5110, 1521 ] }, { "text": "Fish", "indices": [ 122, 142 ] } ], "symbols": [], "user_mentions": [ { "screen_name": "test241", "name": "Test \n Dude", "id": 4999095, "id_str": "489996095", "indices": [ 30, 1111 ] }, { "screen_name": "test", "name": "test", "id": 11999991, "id_str": "1999990", "indices": [ 11, 11 ] } ], "urls": [] }';
var obj = JSON.parse(data);
_.find(obj.hashtags, { 'text': 'animal' });
// => { "text": "animal", "indices": [ 5110, 1521 ] }
For simple parsing like this one, I would use the plain old obj.forEach() method, it is more readable and easy to understand, especially for javascript beginner.
obj = JSON.parse(data).hashtags;
obj.forEach(function(element) {
console.log(element['text']);
});

Categories

Resources