How would I write this reportCandidates' function better? - javascript

This is the input data in array named candidatesArray:
[
{"name":"george","languages":["php","javascript","java"],"age":19,"graduate_date":1044064800000,"phone":"32-991-511"},
{"name":"anna","languages":["java","javascript"],"age":23,"graduate_date":1391220000000,"phone":"32-991-512"},
{"name":"hailee","languages":["regex","javascript","perl","go","java"],"age":31,"graduate_date":1296525600000,"phone":"32-991-513"}
]
I need to transform in this collection as a result of the function:
{candidates: [
{name: "George", age: 19, phone: "32-991-511"},
{name: "Hailee", age: 31, phone: "32-991-513"},
{name: "Anna", age: 23, phone: "32-991-512"}
],
languages: [
{lang:"javascript",count:1},
{lang:"java", count:2},
{lang:"php", count:2},
{lang:"regex", count:1}
]}
The function repCandidates:
const reportCandidates = (candidatesArray) => {
return repObject}
I need to write it in javascript ES6
I shouldn't use loops(for, while, repeat) but foreach is allowed and it could be better if I use "reduce" function
The candidates should be return by their name, age and phone organized by their graduate_date.
The languages should be returned with their counter in alphabetic order .
Visit https://codepen.io/rillervincci/pen/NEyMoV?editors=0010 to see my code, please.

One option would be to first reduce into the candidates subobject, while pushing the langauges of each to an array.
After iterating, sort the candidates and remove the graduate_date property from each candidate, then use reduce again to transform the languages array into one indexed by language, incrementing the count property each time:
const input = [{
"name": "george",
"languages": ["php", "javascript", "java"],
"age": 19,
"graduate_date": 1044064800000,
"phone": "32-991-511"
}, {
"name": "anna",
"languages": ["java", "javascript"],
"age": 23,
"graduate_date": 1391220000000,
"phone": "32-991-512"
}, {
"name": "hailee",
"languages": ["regex", "javascript", "perl", "go", "java"],
"age": 31,
"graduate_date": 1296525600000,
"phone": "32-991-513"
}];
const output = input.reduce((a, { languages, ...rest }) => {
a.candidates.push(rest);
a.languages.push(...languages);
return a;
}, { candidates: [], languages: [] });
output.candidates.sort((a, b) => a.graduate_date - b.graduate_date);
output.candidates.forEach(candidate => delete candidate.graduate_date);
output.languages = Object.values(
output.languages.reduce((a, lang) => {
if (!a[lang]) a[lang] = { lang, count: 0 };
a[lang].count++;
return a;
}, {})
);
output.languages.sort((a, b) => a.lang.localeCompare(b.lang));
console.log(output);

It's common practice to do everything in a reduce(), but sometimes it's easier to read if you break it up a bit. This creates a counter object as a helper to to track the language counts. map()s over the array to pull out the languages and personal info and then puts it all together:
let arr = [ {"name":"george","languages":["php","javascript","java"],"age":19,"graduate_date":1044064800000,"phone":"32-991-511"},{"name":"anna","languages":["java","javascript"],"age":23,"graduate_date":1391220000000,"phone":"32-991-512"},{"name":"hailee","languages":["regex","javascript","perl","go","java"],"age":31,"graduate_date":1296525600000,"phone":"32-991-513"}]
let lang_counter = {
// helper keeps counts of unique items
counts:{},
add(arr){
arr.forEach(item => this.counts[item] = this.counts[item] ? this.counts[item] + 1 : 1)
},
toarray(){
return Object.entries(this.counts).map(([key, val]) => ({[key]: val}))
}
}
// iterate over object to create candidates
let candidates = arr.map(row => {
let {languages, ...person} = row
lang_counter.add(languages) // side effect
return person
})
// put them together
console.log({candidates, languages:lang_counter.toarray()})

You can use Array.reduce and Object.values like below
let arr = [{"name":"george","languages":["php","javascript","java"],"age":19,"graduate_date":1044064800000,"phone":"32-991-511"},{"name":"anna","languages":["java","javascript"],"age":23,"graduate_date":1391220000000,"phone":"32-991-512"},{"name":"hailee","languages":["regex","javascript","perl","go","java"],"age":31,"graduate_date":1296525600000,"phone":"32-991-513"}]
let res = arr.reduce((o, {name, age, phone, graduate_date, languages}) => {
o.candidates.push({name, age, phone, graduate_date})
languages.forEach(l => {
o.languages[l] = o.languages[l] || { lang:l, count: 0 }
o.languages[l].count++
})
return o
}
, { candidates: [], languages: {}})
res.candidates = res.candidates.sort((a,b) => a.graduate_date - b.graduate_date)
.map(({ graduate_date, ...rest }) => rest)
res.languages = Object.values(res.languages).sort((a,b) => a.lang.localeCompare(b.lang))
console.log(res)

Related

Efficient method to group multiple objects by object property

what is the most efficient way to group by multiple object properties? It groups by category and then by group.
I have detailed my implementation using a single array reduce which I will push into an array (expected output). I am not sure how to conditionally push to sources array which is created within the reduce.
Without using lodash groupBy or third party. I was also thinking of filtering by a key I create e.g.
const key = `${category}-${group}`;
const items = [
{
"name": "Halloumi",
"group": "Cheese",
"category": "Dairy"
},
{
"name": "Mozzarella",
"group": "Cheese",
"category": "Dairy"
}
];
// my current implementation
const groupedItems = items.reduce((map, item) => {
const { category, name, group } = item;
if (map.has(category)) {
map.get(category).push({
name,
group,
});
} else {
map.set(category, [
{
name,
group,
},
]);
}
return map;
}, new Map());
console.log(Object.fromEntries(groupedItems));
// Another attempt
const groupedItems2 = items.reduce((map, item) => {
const { category, group, name } = item;
acc[category] = acc[category] || { category, themes: [] };
const source = {
id,
name
};
const totalSources = [source].length;
const uniqueSources = [...new Set([source])].length;
acc[category].themes.push({
theme,
totalSources,
uniqueSources,
sources: [source],
});
return acc;
return map;
}, {});
console.log(Object.entries(groupedItems2));
// expected output
[
{
"category": "Dairy",
"groups": [
{
"group": "Cheese",
"totalSources": 2,
"sources": [
{
"name": "Halloumi"
},
{
"name": "Mozzarella"
}
]
}
]
}
]
You need a two-level map.
There are many ways to do that. Here is one:
const items = [{"name": "Halloumi","group": "Cheese","category": "Dairy"},{"name": "Mozzarella","group": "Cheese","category": "Dairy"}];
const dict = {};
for (const {name, group, category} of items) {
((dict[category] ??= {})[group] ??= []).push({name});
}
const categories = Object.entries(dict).map(([category, groups]) => ({
category,
groups: Object.entries(groups).map(([group, sources]) => ({
group,
totalSources: sources.length,
sources
}))
}));
console.log(categories);
I was interested in a more generic approach to this problem. It might or might not be useful for you, but I think it offers another way to think about such problems. The idea is that we have a function which takes a configuration like this:
const config = {
prop: 'category',
childName: 'groups',
children: {
prop: 'group',
childName: 'sources',
totalName: 'totalSources',
children: {prop: 'name'}
}
}
It returns a new function which takes an array of flat objects and nests them, giving totals, and changing property names as necessary. Here is an implementation, tested only on this problem (with substantially more data added to test various scenarios):
const regroup = ({prop, newName = prop, childName, totalName, children}) => (xs) =>
childName && children
? Object .entries (groupBy (x => x [prop]) (xs))
.map (([k, vs]) => [k, vs .map (omit ([prop]))])
.map (([k, vs, kids = regroup (children) (vs)]) => ({
[newName]: k,
... (totalName ? {[totalName]: kids .length} : {}),
[childName]: kids
})
)
: prop && newName
? xs .map (({[prop]: n, ...rest}) => ({[newName]: n, ...rest}))
: [ ...xs]
const groupBy = (fn, k) => (xs) => xs .reduce (
(a, x) => ((k = fn (x)), (a [k] = a [k] || []), (a [k] .push (x)), a)
, {}
)
const omit = (names) => (o) => Object .fromEntries (
Object .entries (o) .filter (([k, v]) => ! names.includes (k))
)
const config = {
prop: 'category',
childName: 'groups',
children: {
prop: 'group',
childName: 'sources',
totalName: 'totalSources',
children: {prop: 'name'}
}
}
const groupFoods = regroup (config)
const items = [{name: "Halloumi", group: "Cheese", category: "Dairy"}, {name: "Mozzarella", group: "Cheese", category: "Dairy"}, {name: "Whole", group: "Milk", category: "Dairy"}, {name: "Skim", group: "Milk", category: "Dairy"}, {name: "Potatos", group: "Root", category: "Vegetable"}, {name: "Turnips", group: "Root", category: "Vegetable"}, {name: "Strawberry", group: "Berry", category: "Fruit"}, {name: "Baguette", group: "Yeast", category: "Bread"}]
console .log (JSON.stringify (groupFoods (items), null, 4))
.as-console-wrapper {max-height: 100% !important; top: 0}
Note the two helper functions. omit clones an object removing certain property names. So, for instance, omit ([age, id]) ({id: 101, first: 'Fred', last: 'Flintstone', age: 27}) returns {first: 'Fred', last: 'Flintstone'} groupBy accepts a function which returns a key value for a value, and returns a function which takes an array of values, and returns an object with the keys found, assbociated with an array of values that generate that key. For instance, groupBy (n => n % 10) ([1, 3, 4, 21, 501, 23, 43, 25, 64]) //=> {"1": [1, 21, 501], "3": [3, 23, 43], "4": [4, 64], "5": [25]}. These are genuinely reusable functions that you might keep in your personal utility library.
The main function simply builds our output using the configuration values supplied. If we need to nest, then we recur on the children node of the configuration.

Group multiple fields in JSON data

I am trying to transform a JSON file that comes from an API with a structure similar to this:
{
"fruitType": {"name": "bananas"},
"plantedIn": {"country": "USA", "state": "Minnesota"},
"harvestMethod": {"name": "manual"},
"product": {"id": "841023781723"},
},
... (other fruits, or the same fruit planted/harvested differently)
Into something like this:
"bananas": {
"USA": {
"manual": {
"841023781723": {
"fruitType": {"name": "bananas"},
"plantedIn": {"country": "USA", "state": "Minnesota"},
"harvestMethod": {"name": "manual"},
"product": {"id": "841023781723"},
}
}
}
},
...
So, essentially I want to group first by fruitType name, then by plantedIn country, then by harvestMethod name, and finally product id.
After some research I found that lodash is popular for this kind of grouping. I developed a very naive solution with their chain, groupBy and mapValues methods like so:
const _ = require('lodash');
const groupedData = _.chain(data)
.groupBy('fruitType.name')
.mapValues(values => _.chain(values)
.groupBy('plantedIn.country')
.mapValues(values => _.chain(values)
.groupBy('harvestMethod.name')
.mapValues(values => _.chain(values)
.groupBy('product.id')
.value()
).value()
).value()
).value()
This solution, however functional, feels very verbose and is likely inefficient. Therefore I would like to ask if there is any better alternative, either with loadash or any other way.
You could take an array of function to get the keys and build the structure.
const
data = [{ fruitType: { name: "bananas" }, plantedIn: { country: "USA", state: "Minnesota" }, harvestMethod: { name: "manual" }, product: { id: "841023781723" } }],
keys = [o => o.fruitType.name, o => o.plantedIn.country, o => o.harvestMethod.name, o => o.product.id],
result = data.reduce((r, o) => {
keys.reduce(
(q, fn, i, { length }) => q[fn(o)] ??= i + 1 === length ? o : {},
r
);
return r;
}, {});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
There is already an answer but here is maybe a more readable solution
const grouped = items.reduce((acumm, current, index) => {
acumm[current.fruitType.name] = {
[current.plantedIn.country]: {
[current.harvestMethod.name]: {
[current.product.id]: current,
...(acumm[current.fruitType.name]?.
[current.plantedIn.country]?.
[current.harvestMethod.name] ?? {}
),
},
},
};
return acumm;
}, {});
Stackblitz Example

Filtering an array of objects with multiple filter conditions

Lets assume I have an array of objects:
let users = [{
name: "Mark",
location: "US",
job: "engineer"
},
{
name: "Mark",
location: "US",
job: "clerk"
},
{
name: "Angela",
location: "Europe",
job: "pilot"
},
{
name: "Matthew",
location: "US",
job: "engineer"
}]
and I have a filter object with all categories I want to filter data against (there can be multiple values per key):
const filters = {
name: ["Mark", "Matthew"],
location: ["US"],
job: ["Engineer"]
}
Based on these filters and data the expected result would return:
[{name: "Mark", location: "US", job: "Engineer"}, {name: "Matthew", location: "US", job: "Engineer"}]
I have tried filtering with:
users.filter(user => {
for(let k in filters) {
if(user[k] === filters[k]) {
return true;
}
}
})
however, this method doesn't take into account that a filter category might contain more than one value which I can handle by doing like:
filters[k][0] or filters[k][1]
but it isn't dynamic.
If anyone has any input that would be much appreciated! Thank you.
Use Object.entries() on filters to get an array of [key, values] pairs. Iterate the pairs with Array.every(), and check that each pair includes the value of the current object.
const fn = (arr, filters) => {
const filtersArr = Object.entries(filters)
return arr.filter(o =>
filtersArr.every(([key, values]) =>
values.includes(o[key])
)
)
}
const users = [{"name":"Mark","location":"US","job":"engineer"},{"name":"Mark","location":"US","job":"clerk"},{"name":"Angela","location":"Europe","job":"pilot"},{"name":"Matthew","location":"US","job":"engineer"}]
const filters = {
name: ["Mark", "Matthew"],
location: ["US"],
job: ["engineer"]
}
const result = fn(users, filters)
console.log(result)
One caveat of using Array.includes() is that differences in case would provide a false answer (Engineer and engineer in this case). To solve that convert the current word to a RegExp, with the Case-insensitive search flag (i), and check using Array.some() if it fits any of the words in the array.
const fn = (arr, filters) => {
const filtersArr = Object.entries(filters)
return arr.filter(o =>
filtersArr.every(([key, values]) => {
const pattern = new RegExp(`^${o[key]}$`, 'i')
return values.some(v => pattern.test(v))
})
)
}
const users = [{"name":"Mark","location":"US","job":"engineer"},{"name":"Mark","location":"US","job":"clerk"},{"name":"Angela","location":"Europe","job":"pilot"},{"name":"Matthew","location":"US","job":"engineer"}]
const filters = {
name: ["Mark", "Matthew"],
location: ["US"],
job: ["Engineer"]
}
const result = fn(users, filters)
console.log(result)
You can loop over the entries of the filters object and ensure that the value of each key is one of the allowed ones.
let users = [{name:"Mark",location:"US",job:"Engineer"},{name:"Mark",location:"US",job:"clerk"},{name:"Angela",location:"Europe",job:"pilot"},{name:"Matthew",location:"US",job:"Engineer"}];
const filters = {
name: ["Mark", "Matthew"],
location: ["US"],
job: ["Engineer"]
};
const res = users.filter(o =>
Object.entries(filters).every(([k,v])=>v.includes(o[k])));
console.log(res);

Javascript: map an array of items and call function for data

I am new to javascript. I have an id, name and time that I am trying to get from my data and for each name I am trying to loop through the data and call a function from each name. Any help would be much appreciated. Thank you so much!
This is what I have done
const data = [
[
{
"id": "14hyzdrdsquo",
"name": "Ronald",
"time": '12pm',
},
],
[
{
"id": "1f496w43b8yi",
"name": "Jack",
"time": '1am',
},
],
]
const getData = (id, name, time) => {
const ids = [] // desired ['14hyzdrdsquo','1f496w43b8yi']
const names = []// desired ['Ronald','Jack']
const times = []// desired ['12pm','1am']
ids.push(id) // should have each id in this array
names.push(name) // should have each name in this array
times.push(time) // should have each time in this array
}
var id = Math.random().toString(16).slice(2)
data.map(j => j.map(i => getData(id, i.name, i.time)))
Using a for...of loop, you can loop through your array and group on the keys of each object. Since you have arrays in your outer array, you can use another for loop to loop over those and get each object. Then you can use a for...in loop to over the keys in your object. For each key, you can check if it exists within grouped, and if it does, concatenate the object's value to the grouped array. If it doesn't exist, you can create a new element, and push the value. Once you have grouped everything, you can use destructuring to pull out the array values from your object into variables:
const data = [[{ "id": "14hyzdrdsquo", "name": "Ronald", "time": '12pm', }, ], [{ "id": "1f496w43b8yi", "name": "Jack", "time": '1am', }, ],];
const grouped = {};
for(const arr of data) {
for(const obj of arr) {
for(const key in obj) {
grouped[key] = (grouped[key] || []).concat(obj[key]);
}
}
}
const {id, name, time} = grouped;
console.log(id);
console.log(name);
console.log(time);
The above concept can be achieved with .reduce() as well, where the grouped array gets built by using the accumulator argument of the reduce method, and each object is iterated using Object.entries() with .forEach():
const data = [[{ "id": "14hyzdrdsquo", "name": "Ronald", "time": '12pm', }, ], [{ "id": "1f496w43b8yi", "name": "Jack", "time": '1am', }, ],];
const grouped = data.reduce((acc, arr) => {
arr.forEach(obj => {
Object.entries(obj).forEach(([key, val]) => {
acc[key] = [...(acc[key] || []), val];
});
})
return acc;
}, {});
const {id, name, time} = grouped;
console.log(id);
console.log(name);
console.log(time);
Lastly, if you're happy with doing multiple iterations through your array, you can use .flatMap() to iterate through your array, and for each array, .map() over the objects inside of that. For each object you can extract the key using destructuring assignment. The result of the inner map is then flattened into the outer resulting array due to .flatMap():
const data = [[{ "id": "14hyzdrdsquo", "name": "Ronald", "time": '12pm', }, ], [{ "id": "1f496w43b8yi", "name": "Jack", "time": '1am', }, ],];
const id = data.flatMap(arr => arr.map(({id}) => id));
const name = data.flatMap(arr => arr.map(({name}) => name));
const time = data.flatMap(arr => arr.map(({time}) => time));
console.log(id);
console.log(name);
console.log(time);
You could destructure the double nested array and take the entries of the object and push the values to the same named properties with 's'.
Ath the end destructure the objct for single arrays.
const
data = [[{ id: "14hyzdrdsquo", name: "Ronald", time: '12pm' }], [{ id: "1f496w43b8yi", name: "Jack", time: '1am' }]],
{ ids, names, times } = data.reduce((r, [o]) => {
Object.entries(o).forEach(([k, v]) => (r[k + 's'] ??= []).push(v));
return r;
}, {});
console.log(ids);
console.log(names);
console.log(times);
The problems is you put
const ids = [] // desired ['14hyzdrdsquo','1f496w43b8yi']
const names = []// desired ['Ronald','Jack']
const times = []// desired ['12pm','1am']
inside getData function. That's mean you created new one arrays time when call getData
and for you solution better use .forEach instead of .map.
const data = [
[
{
"id": "14hyzdrdsquo",
"name": "Ronald",
"time": '12pm',
},
],
[
{
"id": "1f496w43b8yi",
"name": "Jack",
"time": '1am',
},
],
]
const ids = [] // desired ['14hyzdrdsquo','1f496w43b8yi']
const names = []// desired ['Ronald','Jack']
const times = []// desired ['12pm','1am']
const getData = (id, name, time) => {
ids.push(id) // should have each id in this array
names.push(name) // should have each name in this array
times.push(time) // should have each time in this array
}
var id = Math.random().toString(16).slice(2)
data.forEach(j => j.forEach(i => getData(id, i.name, i.time)))
console.log(ids)
console.log(names)
console.log(times)
You may do that using reduce inside each loop so you can have lower complexity.
const data = [
[
{
id: '14hyzdrdsquo',
name: 'Ronald',
time: '12pm',
},
{
id: '14hyzdrdsquo',
name: 'Ronald',
time: '12pm',
},
],
[
{
id: '1f496w43b8yi',
name: 'Jack',
time: '1am',
},
{
id: '1f496w43b8yi',
name: 'Jack',
time: '1am',
},
],
];
let ids = [];
let names = [];
let times = [];
data.forEach((elem) => {
const obj = elem.reduce(
(acc, curr, i) => {
acc.ids.push(curr.id);
acc.names.push(curr.name);
acc.times.push(curr.time);
return acc;
},
{
ids: [],
names: [],
times: [],
}
);
ids = [...ids, ...obj.ids];
names = [...names, ...obj.names];
times = [...times, ...obj.times];
});
console.log({
ids,
names,
times,
});

Convert a dictionary into an array keeping the dictionary keys

I'm trying to find a more elegant way of converting a dictionary of dictionaries to an array of dictionaries and keeping the information of the keys of the original dictionary.
In the case of
let data= { boss: { name:"Peter", phone:"123"},
minion: { name:"Bob", phone:"456"},
slave: { name:"Pat", phone: "789"}
}
I want to come up with something that gives me
output = [ { role:"boss", name:"Peter", phone:"123"},
{ role:"minion", name:"Bob", phone:"456"},
{ role:"slave", name:"Pat", phone:"789"}
]
My solution goes by using the Object.keys method, but I think it's not very efficient. Something tells me I'm going the complex-all-around way and there must be an easier path, but I can't get it:
Object.keys(data)
.map((elem, i) => {
return Object.assign({role: e}, Object.values(data)[i]);
})
Is this the cleanest way to do what I intend to?
Thanks
You could map the entries and pick the key/role for a new object.
let data = { boss: { name: "Peter", phone: "123" }, minion: { name: "Bob", phone: "456" }, slave: { name: "Pat", phone: "789" } },
result = Object.entries(data).map(([role, values]) => ({ role, ...values }));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Using Object.entries, you can get [key, value] pairs as array and can get teh result you want using Array.map function.
let data = {
boss: { name:"Peter", phone:"123"},
minion: { name:"Bob", phone:"456"},
slave: { name:"Pat", phone: "789"}
};
const result = Object.entries(data).map(([key, value]) => ({
role: key,
...value
}));
console.log(result);
You can use Array.prototype.reduce and Object.entries to loop over every entry in the dictionary and reduce it to an array:
const data = { boss: { name: "Peter", phone: "123" }, minion: { name: "Bob", phone: "456" }, slave: { name: "Pat", phone: "789" } };
function getArray(data) {
return Object.entries(data).reduce((a, [k, v]) => {
a.push({role: k, ...v});
return a;
}, []);
}
console.log(getArray(data));

Categories