Related
I currently have the following items and implementation below. I am not sure if using reduce within another reduce is performant. Is there a better way to sum up nested arrays?
const items = [
{
id: 111,
fruits: [
{
name: "apple",
total: 5
},
{
name: "pineapple",
total: 1
}
]
},
{
id: 222,
fruits: [
{
name: "kiwi",
total: 2
}
]
}
];
// my implementation using reduce within another to get the sum of totals.
const sumOfFruits = items
.reduce((sum, curr) => sum + curr.fruits
.reduce((fruitSum, fruitCurr) => fruitSum + fruitCurr.total));
console.log(sumOfFruits);
// returns 8
A cleaner (not necessarily faster) way to do this would be:
collect all "fruits" (flatMap)
pick "totals" (map)
sum the result
const items = [
{
id: 111,
fruits: [
{
name: "apple",
total: 5
},
{
name: "pineapple",
total: 1
}
]
},
{
id: 222,
fruits: [
{
name: "kiwi",
total: 2
}
]
}
];
//
res = items
.flatMap(x => x.fruits)
.map(x => x.total)
.reduce((s, n) => s + n, 0)
console.log(res)
Regarding performance, the thing about javascript is that it's performant enough unless you have millions of objects. And if you do have millions of objects, you shouldn't be using javascript in the first place.
Your code does not produce the desired output: it coerces an object to string and performs string concatenation.
This is because reduce is called without second argument, and thus the accumulator gets the first object as value, while you want the accumulated value to be a number.
So you need to add 0 as second argument for the outer reduce call. For the inner reduce call you can improve a little bit and provide sum as initial value. That way you don't have to do sum + anymore.
You can also make use of destructuring in the callback parameters:
This leads to the following code:
const items = [{id: 111,fruits: [{name: "apple",total: 5},{name: "pineapple",total: 1}]},{id: 222,fruits: [{name: "kiwi",total: 2}]}];
const sumOfFruits = items.reduce(
(sum, {fruits}) => fruits.reduce(
(fruitSum, {total}) => fruitSum + total,
sum // Continue with the sum we already have
), 0 // Start with 0 for the accumulator
);
console.log(sumOfFruits); // 8
Many would agree that this is how it should be done. If performance really is an issue, then you can get a slight improvement by using plain old for loops. These do not use callbacks, and so can be expected to do the job a little bit faster, but with less elegant code. In my experience they also perform a tiny bit faster than for..of loops:
var items = [{id: 111,fruits: [{name: "apple",total: 5},{name: "pineapple",total: 1}]},{id: 222,fruits: [{name: "kiwi",total: 2}]}];
var sumOfFruits = 0;
for (var i = 0; i < items.length; i++) {
var fruits = items[i].fruits;
for (var j = 0; j < fruits.length; j++) {
sumOfFruits += fruits[j].total;
}
}
console.log(sumOfFruits); // 8
It probably is not worth the millisecond you would gain from this with normal sized input.
Overview
I need to make a chart in my react project.
Using data from a json (Object Array).
Example json:
[
{recruiter_id: 1, datetime_created: "1/01/2021", name: "Aaron"},
{recruiter_id: 2, datetime_created: "9/01/2021", name: "Bob"},
{recruiter_id: 1, datetime_created: "9/01/2021", name: "Aaron"},
{recruiter_id: 3, datetime_created: "20/01/2021", name: "Jane"}
]
Result object array structure required:
[
{name: name,
recruiter_id: recruiter_id,
week_qty: [0,2,1,0,2,0,0,0,0,0,0,0,0,1,0,0,0,...] },
...]
// week_qty will be an array of 52 to represent each week of the year. It will be a 0 if there was no dates for that week.
Goal
This is what the new object array should look like, if we used the example json.
[
{name: "Aaron", recruiter_id:1, week_qty: [1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...]},
{name: "Bob", recruiter_id:2, week_qty: [0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...]},
{name: "Jane", recruiter_id:3, week_qty: [0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,...]}
]
What I have
I dont have any working code yet. I am currently working on object[0] to attempt to put the dates into the 52 array. And then after that I will then turn it into a loop to work on each object. Once I have it semi working, I will post it for example.
--- Edit ---
var array = result
var flags = [], output = [], l = array.length, i;
for (i = 0; i < l; i++) {
if (flags[array[i].recruiter_id]) continue;
flags[array[i].recruiter_id] = true;
var temp = {}
temp.Recruiter_id = array[i].recruiter_id
temp.Name = array[i].name
temp.QTY = []
output.push(temp);
}
console.log("output : ", output)
This produces the new object array structure with the id and name filled out.
[
{name: name,
recruiter_id: recruiter_id,
week_qty: [] },
...]
It only has 1 object for each id
Now I need to work on getting the week numbers for the dates and put them into each of those objects.
Question
Any code suggestions on how to get this result?
Side Note
If your curious to know how I then plan on using the new object array to use with my chart.
I will let the user select the week. Lets say week 1.
I will then map through the object array and get the week_qty for index 1 and the name value of the object.
I will store that week_qty and the name in a new new object array.
That new new object array will then look like this
[{name: "Aaron",QTY: 2},{name: "Bob",QTY: 1,]
That will then be passed as the x and y value to the chart.
You can use reduce and increase the week counter after parsing each date and getting the week (using moment.js for that part here)
But you can see Get week of year in JavaScript like in PHP for more details on how to calculate it yourself
const data = [
{recruiter_id: 1, datetime_created: "1/01/2021", name: "Aaron"},
{recruiter_id: 2, datetime_created: "9/01/2021", name: "Bob"},
{recruiter_id: 1, datetime_created: "9/01/2021", name: "Aaron"},
{recruiter_id: 3, datetime_created: "20/01/2021", name: "Jane"}
];
const weekly = data.reduce((acc, item, index, array) => {
const {
recruiter_id,
datetime_created,
name
} = item;
let existing = acc.find(({
recruiter_id: id
}) => id === recruiter_id);
if (!existing) {
existing = {recruiter_id, name, week_qty:Array(52).fill(0)};
acc.push(existing);
}
const week = moment(datetime_created,'D/M/YYYY').week()-1;
existing.week_qty[week]++;
return acc;
}, []);
console.log(JSON.stringify(weekly))
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js" integrity="sha512-qTXRIMyZIFb8iQcfjXWCO8+M5Tbc38Qi5WzdPOYZHIlZpzBHG3L3by84BBBOiRGiEb7KKtAOAs5qYdUiZiQNNQ==" crossorigin="anonymous"></script>
I recently started using Ramda and trying to find a pointfree way to write a method to reduce an array of objects.
Here is the array of object :
const someObj = [
{
name: 'A',
city: 1,
other: {
playtime: 30
}
},
{
name: 'B',
city: 2,
other: {
playtime: 20
}
},
{
name: 'c',
city: 1,
other: {
playtime: 20
}
}
];
What I am trying is to reduce the object using ramda in poinfree style like
{
'1': {
count: 2,
avg_play_time: 20 + 30 / count
},
'2': {
count: 1,
avg_play_time: 20 / count
}
}
I can do it using an array reduce method but not sure how can I write the same in ramda pointfree style. Any suggestion will be appreciated.
One solution would be to do something like this:
// An optic to extract the nested playtime value
// Coupled with a `lift` operation which allows it to be applied over a collection
// Effectively A -> B => A[] -> B[]
const playtimes = R.lift(R.path(['other', 'playtime']))
R.pipe(
// Group the provided array by the city value
R.groupBy(R.prop('city')),
// Return a body specification which computes each property based on the
// provided function value.
R.map(R.applySpec({
count: R.length,
average: R.pipe(playtimes, R.mean)
}))
)(someObj)
Ramda also has another function called R.reduceBy which provides something inbetween reduce and groupBy, allowing you to fold up values with matching keys together.
So you can create a data type like the following that tallies up the values to averaged.
const Avg = (count, val) => ({ count, val })
Avg.of = val => Avg(1, val)
Avg.concat = (a, b) => Avg(a.count + b.count, a.val + b.val)
Avg.getAverage = ({ count, val }) => val / count
Avg.empty = Avg(0, 0)
Then combine them together using R.reduceBy.
const avgCities = R.reduceBy(
(avg, a) => Avg.concat(avg, Avg.of(a.other.playtime)),
Avg.empty,
x => x.city
)
Then pull the average values out of the Avg into the shape of the final objects.
const buildAvg = R.applySpec({
count: x => x.count,
avg_play_time: Avg.getAverage
})
And finally pipe the two together, mapping buildAvg over the values in the object.
const fn = R.pipe(avgCities, R.map(buildAvg))
fn(someObj)
I would write it like this, hope it helps!
const stats = R.pipe(
R.groupBy(R.prop('city')),
R.map(
R.applySpec({
count: R.length,
avg_play_time: R.pipe(
R.map(R.path(['other', 'playtime'])),
R.mean,
),
}),
),
);
const data = [
{ name: 'A', city: 1, other: { playtime: 30 } },
{ name: 'B', city: 2, other: { playtime: 20 } },
{ name: 'c', city: 1, other: { playtime: 20 } },
];
console.log('result', stats(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
Here's another suggestion using reduceBy with mapping an applySpec function on each property of the resulting object:
The idea is to transform someObj into this object using getPlaytimeByCity:
{ 1: [30, 20],
2: [20]}
Then you can map the stats function on each property of that object:
stats({ 1: [30, 20], 2: [20]});
// { 1: {count: 2, avg_play_time: 25},
// 2: {count: 1, avg_play_time: 20}}
const someObj = [
{ name: 'A',
city: 1,
other: { playtime: 30 }},
{ name: 'B',
city: 2,
other: { playtime: 20 }},
{ name: 'c',
city: 1,
other: { playtime: 20 }}
];
const city = prop('city');
const playtime = path(['other', 'playtime']);
const stats = applySpec({count: length, avg_play_time: mean});
const collectPlaytime = useWith(flip(append), [identity, playtime]);
const getPlaytimeByCity = reduceBy(collectPlaytime, [], city);
console.log(
map(stats, getPlaytimeByCity(someObj))
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {prop, path, useWith, flip, append, identity, applySpec, length, mean, reduceBy, map} = R;</script>
I like all the other answers given so far. So naturally I want to add my own. ;-)
Here is a version that uses reduceBy to keep a running track of the count and mean. This would not work if you were looking for the median value or some other statistic, but given a count, an average, and a new value, we can calculate the new count and average directly. This allows us to iterate the data only once at the expense of doing some arithmetic on every iteration.
const transform = reduceBy(
({count, avg_play_time}, {other: {playtime}}) => ({
count: count + 1,
avg_play_time: (avg_play_time * count + playtime) / (count + 1)
}),
{count: 0, avg_play_time: 0},
prop('city')
)
const someObj = [{city: 1, name: "A", other: {playtime: 30}}, {city: 2, name: "B", other: {playtime: 20}}, {city: 1, name: "c", other: {playtime: 20}}]
console.log(transform(someObj))
<script src="https://bundle.run/ramda#0.26.1"></script>
<script>
const {reduceBy, prop} = ramda
</script>
This is not point-free. Although I'm a big fan of point-free style, I only use it when it's applicable. I think seeking it out for its own sake is a mistake.
Note that the answer from Scott Christopher could easily be modified to use this sort of calculation
I've got two arrays that have multiple objects
[
{
"name":"paul",
"employee_id":"8"
}
]
[
{
"years_at_school": 6,
"department":"Mathematics",
"e_id":"8"
}
]
How can I achieve the following with either ES6 or Lodash?
[
{
"name":"paul",
"employee_id":"8"
"data": {
"years_at_school": 6
"department":"Mathematics",
"e_id":"8"
}
}
]
I can merge but I'm not sure how to create a new child object and merge that in.
Code I've tried:
school_data = _.map(array1, function(obj) {
return _.merge(obj, _.find(array2, {employee_id: obj.e_id}))
})
This merges to a top level array like so (which is not what I want):
{
"name":"paul",
"employee_id":"8"
"years_at_school": 6
"department":"Mathematics",
"e_id":"8"
}
The connector between these two is "employee_id" and "e_id".
It's imperative that it's taken into account that they could be 1000 objects in each array, and that the only way to match these objects up is by "employee_id" and "e_id".
In order to match up employee_id and e_id you should iterate through the first array and create an object keyed to employee_id. Then you can iterate though the second array and add the data to the particular id in question. Here's an example with an extra item added to each array:
let arr1 = [
{
"name":"mark",
"employee_id":"6"
},
{
"name":"paul",
"employee_id":"8"
}
]
let arr2 = [
{
"years_at_school": 6,
"department":"Mathematics",
"e_id":"8"
},
{
"years_at_school": 12,
"department":"Arr",
"e_id":"6"
}
]
// empObj will be keyed to item.employee_id
let empObj = arr1.reduce((obj, item) => {
obj[item.employee_id] = item
return obj
}, {})
// now lookup up id and add data for each object in arr2
arr2.forEach(item=>
empObj[item.e_id].data = item
)
// The values of the object will be an array of your data
let merged = Object.values(empObj)
console.log(merged)
If you perform two nested O(n) loops (map+find), you'll end up with O(n^2) performance. A typical alternative is to create intermediate indexed structures so the whole thing is O(n). A functional approach with lodash:
const _ = require('lodash');
const dataByEmployeeId = _(array2).keyBy('e_id');
const result = array1.map(o => ({...o, data: dataByEmployeeId.get(o.employee_id)}));
Hope this help you:
var mainData = [{
name: "paul",
employee_id: "8"
}];
var secondaryData = [{
years_at_school: 6,
department: "Mathematics",
e_id: "8"
}];
var finalData = mainData.map(function(person, index) {
person.data = secondaryData[index];
return person;
});
Sorry, I've also fixed a missing coma in the second object and changed some other stuff.
With latest Ecmascript versions:
const mainData = [{
name: "paul",
employee_id: "8"
}];
const secondaryData = [{
years_at_school: 6,
department: "Mathematics",
e_id: "8"
}];
// Be careful with spread operator over objects.. it lacks of browser support yet! ..but works fine on latest Chrome version for example (69.0)
const finalData = mainData.map((person, index) => ({ ...person, data: secondaryData[index] }));
Your question suggests that both arrays will always have the same size. It also suggests that you want to put the contents of array2 within the field data of the elements with the same index in array1. If those assumptions are correct, then:
// Array that will receive the extra data
const teachers = [
{ name: "Paul", employee_id: 8 },
{ name: "Mariah", employee_id: 10 }
];
// Array with the additional data
const extraData = [
{ years_at_school: 6, department: "Mathematics", e_id: 8 },
{ years_at_school: 8, department: "Biology", e_id: 10 },
];
// Array.map will iterate through all indices, and gives both the
const merged = teachers.map((teacher, index) => Object.assign({ data: extraData[index] }, teacher));
However, if you want the data to be added to the employee with an "id" matching in both arrays, you need to do the following:
// Create a function to obtain the employee from an ID
const findEmployee = id => extraData.filter(entry => entry.e_id == id);
merged = teachers.map(teacher => {
const employeeData = findEmployee(teacher.employee_id);
if (employeeData.length === 0) {
// Employee not found
throw new Error("Data inconsistency");
}
if (employeeData.length > 1) {
// More than one employee found
throw new Error("Data inconsistency");
}
return Object.assign({ data: employeeData[0] }, teacher);
});
A slightly different approach just using vanilla js map with a loop to match the employee ids and add the data from the second array to the matching object from the first array. My guess is that the answer from #MarkMeyer is probably faster.
const arr1 = [{ "name": "paul", "employee_id": "8" }];
const arr2 = [{ "years_at_school": 6, "department": "Mathematics", "e_id": "8" }];
const results = arr1.map((obj1) => {
for (const obj2 of arr2) {
if (obj2.e_id === obj1.employee_id) {
obj1.data = obj2;
break;
}
}
return obj1;
});
console.log(results);
Below are my two arrays.
let clientCollection = ["1","ABC","X12","OE2","PQ$"];
let serverCollection = [{
"Id": "1",
"Name": "Ram",
"Other": "Other properties"
},
{
"Id": "ABC",
"Name": "Shyam",
"Other": "Other properties"
},
{
"Id": "OE2",
"Name": "Mohan",
"Other": "Other properties"
}]
Now I am in need to compare the above two collections & create two sub arrays
let matchedIds = [];
let unMatchedIds = [];
Now this is what I am doing currently.
for(let i =0 ; i < clientsCollection.length;i++)
{
if(_.indexOf(serverCollection, clientCollection[i]) >= 0)
{
matchedIds.push(clientCollection[i]);
}
else
{
unMatchedIds.push(clientCollection[i]);
}
}
In my application, the size of these arrays can increase to 1000 or more. This could be have efficieny issues
I am using underscore & tried if I can get some better solution but couldn't find yet.
Can someone please suggest if I can do the same in better efficient way using underscore + ES6??
I think, this would be a good way for matchedIds population:
for(let i = serverCollection.length - 1; i >= 0; i--) {
const id = serverCollection[i]['Id'];
if(clientCollection.indexOf(id) !== -1) {
matchedIds.push(id);
}
}
And this one is for unMatchedIds after the matchedIds is done:
for (var i = clientCollection.length - 1; i >= 0; i--) {
if (matchedIds.indexOf(clientCollection[i]) === -1) {
unMatchedIds.push(clientCollection[i]);
}
}
None of filter, reduce etc is faster than basic indexOf!
UPD
I created a plunker: https://plnkr.co/edit/UcOv6SquUgC7Szgfn8Wk?p=preview. He says that for 10000 items this solution is up to 5 times faster than other 2 solutions suggested here.
I would make a Set from all the server ids. Then just loop through and see if the id is in the Set and add it to the two arrays:
let serverCollection = [
{
Id: '1',
Name: 'Ram',
Other: 'Other properties'
},
{
Id: 'ABC',
Name: 'Shyam',
Other: 'Other properties'
},
{
Id: 'OE2',
Name: 'Mohan',
Other: 'Other properties'
}
];
let clientCollection = ['1', 'ABC', 'X12', 'OE2', 'PQ$'];
const serverIds = new Set(serverCollection.map((server) => server.Id));
let matchedIds = [];
let unmatchedIds = [];
for (let id of clientCollection) {
if (serverIds.has(id)) {
matchedIds.push(id);
} else {
unmatchedIds.push(id);
}
}
console.log('matched', matchedIds);
console.log('unmatched', unmatchedIds);
As the length of clientCollection and serverCollection increases, the cost of looping through each item becomes more and more apparent.
See a plunkr measuring performance
Create a Set of server ids. Use Array#reduce to iterate the clients, and assign the id to a sub array according to it's existence in the server's set. Extract the sub arrays to variables using destructuring assignment.
const clientCollection = ["1","ABC","X12","OE2","PQ$"];
const serverCollection = [{"Id":"1","Name":"Ram","Other":"Other properties"},{"Id":"ABC","Name":"Shyam","Other":"Other properties"},{"Id":"OE2","Name":"Mohan","Other":"Other properties"}];
// create a set of Ids. You can use underscore's pluck instead of map
const serverSet = new Set(serverCollection.map(({ Id }) => Id));
// reduce the ids to an array of two arrays (matchedIds, unMatchedIds), and then get assign to variables using destructuring assignment
const [matchedIds, unMatchedIds] = clientCollection.reduce((r, id) => {
r[serverSet.has(id) ? 0 : 1].push(id); // push to a sub array according to existence in the set
return r;
}, [[], []])
console.log(matchedIds);
console.log(unMatchedIds);