Convert array of objects (sort of groupby) - javascript

I call a service that returns a list of questions. Then, I regroup this question by theme.
i have this conversion :
var questions = [
{
id: 1,
label: 'lorem ispum ?',
theme: {
id: 1,
label: 'dog',
}
},
{
id: 2,
label: 'lorem ispum2 ?',
theme: {
id: 2,
label: 'horse',
}
},
{
id: 3,
label: 'lorem ispum3 ?',
theme: {
id: 1,
label: 'dog',
}
},
];
var groupByTheme = questions.reduce(function(obj,item){
obj[item.theme.label] = obj[item.theme.label] || [];
obj[item.theme.label].push(item);
return obj;
}, {});
/*var result = Object.keys(groupsByTheme).map(function(key){
return {theme: key, questions: groupsByTheme[key]};
});
*/
var result = Object.entries(groupByTheme).map(([theme, questions]) => ({ theme, questions }));
console.log(result);
It works.
But now, i would like to add a property : the id of theme.
{
"themeId": 1,
"theme": "dog",
"questions": [...]
},
{
"themeId": 2,
"theme": "horse",
"questions": [...]
}
I don't find the right way without having a missy code.
Any suggestions ?
Thanks in advance !

You could greate the missing parts by getting the result set in the final stage.
I changed the key from theme to id, because id looks more unique.
var questions = [{ id: 1, label: 'lorem ispum ?', theme: { id: 1, label: 'dog' } }, { id: 2, label: 'lorem ispum2 ?', theme: { id: 2, label: 'horse' } }, { id: 3, label: 'lorem ispum3 ?', theme: { id: 1, label: 'dog' } }],
groupByTheme = questions.reduce(function (obj, item) {
obj[item.theme.id] = obj[item.theme.id] || [];
obj[item.theme.id].push(item);
return obj;
}, {}),
result = Object.entries(groupByTheme).map(([id, questions]) => ({
id,
theme: questions[0].theme.label, // add this
questions
}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

I would recommend you to use lodash library for that. Then the code will be pretty clean. I'll first make the themeMap object which would be map from label to id as :
const themeMap = {};
_.forEach(questions, question => themeMap[question.theme.label] = question.theme.id);
Then we can use the groupBy and map of lodash and code will be really clean as :
const result = _.map(_.groupBy(questions, question => question.theme.label), (group, key) => {
return {questions: group, theme: key, themeId: themeMap[key]};
});
The code will be less messy.
If you don't want to use lodash then you can achieve groupBy by reduce of javascript which you already did.

When you group the questions by the theme name, you can create the exact "theme" object that you want { "themeId": 1, "theme": "dog", "questions": [] }, and push the question into the questions prop. To convert to array use Object#values.
var questions = [{"id":1,"label":"lorem ispum ?","theme":{"id":1,"label":"dog"}},{"id":2,"label":"lorem ispum2 ?","theme":{"id":2,"label":"horse"}},{"id":3,"label":"lorem ispum3 ?","theme":{"id":1,"label":"dog"}}];
var result = Object.values( // get an array of theme objects
questions.reduce(function(obj,item){
obj[item.theme.label] = obj[item.theme.label] ||
Object.assign({ questions: [] }, item.theme); // create an object that includes theme data, and an empty array for questions
obj[item.theme.label].questions.push(item); // push into questions
return obj;
}, {})
);
console.log(result);

Related

Using tidy.js in javascript, string agg / concatenate in summarise after groupBy

We are using https://pbeshai.github.io/tidy/ for some data manipulation in javascript. We are looking to concatenate all strings for a grouped field in a summarise() after using groupby(). Trying to do something like:
let myArr = [
{ group: 1, field: 'okay' },
{ group: 1, field: 'sir' },
{ group: 2, field: 'yes' },
{ group: 2, field: 'there' },
]
tidy(myArr,
groupBy('group', [
summarise({
allStrings: concat('field', sep=' ')
})
])
)
and get as an output:
[
{ group: 1, allStrings: 'okay sir' },
{ group: 2, allStrings: 'yes there' }
]
Not seeing anything in the docs in the summarizers section for this unfortunately. allStrings: concat('field', sep=' ') is invalid as there is no concat summarizer function in tidy.js... Is this possible in tidy.js? If not, is there a straightforward way to string_agg / concat strings within a group in javascript like this?
You're right – there is not yet a concat summarizer in tidyjs, so you need to do it manually. Here's an example of how:
const myArr = [
{ group: 1, field: 'okay' },
{ group: 1, field: 'sir' },
{ group: 2, field: 'yes' },
{ group: 2, field: 'there' },
];
const output = tidy(
myArr,
groupBy('group', [
summarize({
allStrings: (items) => items.map((d) => d.field).join(' '),
}),
])
);
This will produce the following output:
[
{"group": 1, "allStrings": "okay sir"},
{"group": 2, "allStrings": "yes there"}
]
Essentially, you write a custom summarizer that maps each item into just the string value you care about, then you join those with a ' ' to get your final concatenated string.
There are so many group array of objects by key having items either pushed into array/ concatenated/ summed/ counted/ etc examples on this site. But maybe someone will find this useful.
let myArr = [
{ group: 1, field: 'okay' },
{ group: 1, field: 'sir' },
{ group: 2, field: 'yes' },
{ group: 2, field: 'there' },
]
let obj = myArr.reduce(function(agg, item) {
// do your group by logic below this line
agg[item.group] = agg[item.group] || [];
agg[item.group].push (item.field)
// do your group by logic above this line
return agg
}, {});
// this one also useful to convert to array
let result = Object.entries(obj).map(function ([key, value]) {
return {
group: key,
allStrings: value.join(" ")
}
})
console.log(result)
.as-console-wrapper { max-height: 100% !important; top: 0; }

How to nest a list of objects into another and how to do it elegantly?

I have two objects coming from two SQL queries: posts and comments. Now I want to create one "posts"-object with "comments" objects nested in each of the posts. If I JOIN that data in SQL, I get a flat object without nesting. I need it nested thought. I tried the following method in two for loops, but this did not work because the new_object_line does not have a comments key (yet).
joined_object = Object.assign(new_object_line.comments, rows_comments[x])
Original posts list object:
var posts =
{
post1:
{
title: 'Title 1',
id: id1
}
post2:
{
title: 'Title 2',
id: id2
}
post3:
{
title: 'Title 3',
id: id3
}
}
Original comments list object:
var comments =
{
comment1: {
text: 'aaa',
belongs_to_post: id2
}
comment2: {
text: 'bbb',
belongs_to_post: id2
}
comment3: {
text: 'ccc',
belongs_to_post: id3
}
}
This is how the result object joined_object with posts including comments should look like:
var joined_object =
{
post1:
{
title: 'Title 1',
id: 1
}
post2:
{
title: 'Title 2',
id: 2
comments:
{
comment1: {
text: 'aaa',
belongs_to_post: id2
}
comment2: {
text: 'bbb',
belongs_to_post: id2
}
}
}
post3:
{
title: 'Title 3',
id: 3
comments:
{
comment3: {
text: 'ccc',
belongs_to_post: id3
}
}
}
}
You can achieve the expected output using for..in loop by matching the id:
for(let o in posts){
for(let c in comments){
if(posts[o].id == comments[c].belongs_to_post){
// (posts[o].comments ??={})[c] = comments[c]
posts[o].comments = posts[o].comments || {};
posts[o].comments[c] = comments[c];
}
};
}
Your example is almost perfectly fine, but in reality you're much more likely to have an array of post objects like so:
const posts = [
{
"title":"Title 1",
"id":1
},
{
"title":"Title 2",
"id":2,
"comments":[
{
"text":"aaa",
"belongs_to_post":2
},
{
"text":"bbb",
"belongs_to_post":2
}
]
},
{
"title":"Title 3",
"id":3,
"comments":[
{
"text":"ccc",
"belongs_to_post":3
}
]
}
]
As you can see - you now don't have a property on an object that contains your post, rather just a list of posts (Which I would assume matches your database results more closely).
Now, if you'd like to get return a single post based on id, you would do the following:
const postTwo = posts.filter(post => post.id === 2);
You can then access comments on the post 2 easily:
const comments = postTwo.comments;
Take a look into JSON objects basics here, with examples of nested objects:
https://www.w3schools.com/js/js_json_objects.asp
If you want to work with objects, you can use something like lodash and I'd suggest something like this:
const { mapValues, filter, isEmpty } = require('lodash')
const joined = mapValues(posts, post => {
const byPostId = { belongs_to_post: post.id }
const relatedComments = filter(comments, byPostId)
if (isEmpty(relatedComments)) {
return post
}
return { ...post, comments: relatedComments }
})
console.log(joined)

Best way to modify a property in all elements of a nested array of objects

I have an object in the following format
const courses = [
{
degree: 'bsc',
text: 'Some text',
id: 'D001',
},
{
degree: 'beng',
text: 'Some text',
id: 'D002',
},
{
degree: 'bcom',
text: 'Some text',
id: 'D003',
electives: [
{
degree: 'bsc',
course: 'Psychology'
text: 'Some text',
id: 'C010',
},
],
},
];
I would like to transform the text property of all the objects in the array, including the objects in the nested arrays, if any. This is the format I would like to get:
const courses = [
{
degree: 'bsc',
text: translateText('D001'),
id: 'D001',
},
{
degree: 'beng',
text: translateText('D002'),
id: 'D002',
},
{
degree: 'bcom',
text: translateText('D003'),
id: 'D003',
electives: [
{
degree: 'bsc',
course: 'Psychology'
text: translateText('C010'),
id: 'C010',
},
],
},
];
The transformation needs to be done with the Id property passed as a parameter. This is what I have tried:
courses.forEach(course => {
course.text = translateText(course.id);
if (course.degree === 'bcom') {
course.electives.forEach(elective => {
elective.text = translateText(elective.id);
})
}
});
I do not like this approach since it may end up being clogged with a lot of if statements if arrays will be added as properties to more degree types. There may also be a performance cost (not sure if it can perform any better).
Is there a better and cleaner way of achieving the above?
#Nenad beat me to the recursive function, but I'm going to post this answer anyway, because you may want to recurse through any nested objects, not just the electives property. Here's how you could do that:
const courses = [
{
degree: 'bsc',
text: 'Some text',
id: 'D001',
},
{
degree: 'beng',
text: 'Some text',
id: 'D002',
},
{
degree: 'bcom',
text: 'Some text',
id: 'D003',
electives: [
{
degree: 'bsc',
course: 'Psychology'
text: 'Some text',
id: 'C010',
},
],
},
];
const translateTextProperty = function(obj) {
if( typeof obj.text != 'undefined' )
obj.text = translateText(obj.id);
for(let key in obj ) {
if( obj[key] instanceof Object ) {
translateTextProperty( obj[key] );
}
}
}
courses.forEach( c => translateTextProperty(c) );
You could use recursive function that will look for electives key and if the property exists in the object it will update the nested data structure.
const courses = [{"degree":"bsc","text":"Some text","id":"D001"},{"degree":"beng","text":"Some text","id":"D002"},{"degree":"bcom","text":"Some text","id":"D003","electives":[{"degree":"bsc","course":"Psychology","text":"Some text","id":"C010"}]}]
const f = text => text + ' - updated';
const update = data => {
data.forEach(e => {
if (e.text) {
e.text = f(e.text)
}
if (e.electives) {
update(e.electives)
}
})
}
update(courses);
console.log(courses)

Merge an item attribute of one array into items of another array

I have a few questions in regards to what would be the best approach to do the following:
Call two different API:
axios.get(contents);
axios.get(favorites);
Response will Look like this:
contents: [
{
id: 1,
value: someValue
},
{
id: 2,
value: someValue
}
];
favorites: [
{
id: 1,
contentId: 2
}
];
What would be the best approach to loop through each favorite and add an element to the contens array such as isFavorite: true when the contentId matches the id. It should look as follows:
contents: [
{
id: 1,
value: someValue
{,
{
id: 2,
value: someValue
isFavorite: true
{
];
What would be the best place to do this and is there any ES6 syntax that can easily do this? I currently have the two actions separate, one that gets the contents and one that gets the favorites, I could possibly merge those or combine them at the reducer.
Any suggestions?
You can use a Set to collect all contentId values from favorites and then iterate through your contents array. This has better time complexity than using some on an array because calling .has() on a Set is O(1):
let contents = [{
id: 1,
value: 'someValue1'
},
{
id: 2,
value: 'someValue2'
},
{
id: 3,
value: 'someValue'
}
];
let favorites = [{
id: 1,
contentId: 2
},
{
id: 2,
contentId: 3
}
];
let favoriteContents = new Set(favorites.map(f => f.contentId));
contents.forEach(c => {
if (favoriteContents.has(c.id)) c.isFavorite = true;
});
console.log(contents);
const newContents = contents.map((content) => {
const foundFavorite = favorites.find((favorite) => favorite.contentId === content.id)
if (foundFavorite) {
return {
...content,
isFavorite: true,
}
}
return content
});
You firstly need to have the promises from your API calls, and when both of them are complete you can then carry out the merge of the results.
const contentsApi = () => Promise.resolve([
{
id: 1,
value: 'foo'
},
{
id: 2,
value: 'bar'
}
])
const favouritesApi = () => Promise.resolve([
{
id: 1,
contentId: 2
}
])
let contents;
let favourites;
const contentsApiCall = contentsApi().then(res => {
contents = res;
})
const favouritesApiCall = favouritesApi().then(res => {
favourites = res;
})
Promise.all([contentsApiCall, favouritesApiCall]).then(() => {
const merged = contents.map(content => {
if(favourites.some(favourite => favourite.contentId === content.id)){
return {
...content,
isFavourite: true
}
} else {
return content;
}
})
console.log(merged)
// do whatever you need to do with your result, either return it if you want to chain promises, or set it in a variable, etc.
})

Using filter() in combination with includes() to get partial matches

I have an array with objects I want to search through. The searchable array looks like this:
[
{ value: 0, label: 'john' },
{ value: 1, label: 'johnny' },
{ value: 2, label: 'peter' },
{ value: 3, label: 'peterson' }
]
I search through this using the Lodash filter method:
search = (text) => {
let results = _.filter(
this.props.options,
{ label: text }
);
}
This only shows the result that exactly matches the search query (text parameter). I need to make this work with partial matches. So if I insert j or johnny it should be able to find both 'John' and 'Johnny'.
I have tried:
search = (text) => {
let results = _.filter(
this.props.options =>
this.props.options.includes({ label: text })
);
}
But, no luck. No error and no results. How can I make this work?
Since you are using includes which is a part of ES6 standat, then I would solve this task with the ES6 Array.prototype.filter instead of lodash-filter:
let search = (list, text) =>
list.filter(i => i.label.toLowerCase().includes(text.toLowerCase()));
let list = [
{ value: 0, label: 'john' },
{ value: 1, label: 'johnny' },
{ value: 2, label: 'peter' },
{ value: 3, label: 'peterson' }
];
let result = search(list, 'j');
console.log(result); // [{value: 0, label: "john"}, {value: 1, label: "johnny"}]
Also, with .toLowerCase you may use "John" instead of "john".
String#includes accepts a string as a needle. If the the needle is not a string, it's converted to string, and it the case of an object it's [object Object].
You should get the value of label, and use the string's includes method:
const options = [
{ value: 0, label: 'john' },
{ value: 1, label: 'johnny' },
{ value: 2, label: 'peter' },
{ value: 3, label: 'peterson' }
];
const search = (text) => options.filter(({ label }) => label.includes(text));
const result = search('jo');
console.log(result);
That's not how you use String.prototype.includes. You should provide a string to it not an object. And you should provide a function that wraps the call to includes:
search = (text) => {
let results = _.filter(
this.props.options, // first parameter to _.filter is the array
option => option.label.includes(text) // the second parameter is a funtion that takes an option object and returns a boolean (wether the label of this option includes the text text or not)
);
}

Categories