Group array of objects with a specific key value pushed first - javascript

Given the following Array of Objects:
[
{
"teamFK": 8650,
"code": "yellow_cards",
"typeId": 554,
"value": "5",
"side": "home"
},
{
"teamFK": 8650,
"code": "goals",
"typeId": 554,
"value": "1",
"side": "home"
},
{
"teamFK": 8990,
"code": "yellow_cards",
"typeId": 555,
"value": "2",
"side": "away"
},
{
"teamFK": 8990,
"code": "goals",
"typeId": 555,
"value": "0",
"side": "away"
}
]
I would like to group this data by code and get this result:
{
"stats": [
{
"name": "yellow_cards",
"stats": ["5","2"]
},
{
"name": "goals",
"stats": ["2","0"]
}
]
}
What I've done is the following which works but I want to make sure that the alway the stat with "side":"home" always pushed first into the array "stats": []:
const groupedStats = Object.entries(
query.reduce((acc, { typeId, value, code, side }) => {
if (!acc[code]) {
acc[code] = [];
}
acc[code].push(value);
return acc;
}, {}),
).map(([name, stats]) => ({ name, stats }));

My approach is sort it first by side using Array.sort() and then looping through the objects and adding it to stats
i created a const match to find if there is a match already so i dont have to add the name and value again basically if its not a match i'll add it to the stats array and if its a match then i'll just update the current index
const objs = [
{
teamFK: 8650,
code: "yellow_cards",
typeId: 554,
value: "5",
side: "home",
},
{
teamFK: 8650,
code: "goals",
typeId: 554,
value: "1",
side: "away",
},
{
teamFK: 8990,
code: "yellow_cards",
typeId: 555,
value: "2",
side: "away",
},
{
teamFK: 8990,
code: "goals",
typeId: 555,
value: "0",
side: "home",
},
];
let stats = [];
const transformedObj = objs
.sort((a, b) => {
if (a.side > b.side) {
return -1;
}
if (a.side < b.side) {
return 1;
}
return 0;
})
.forEach((obj) => {
const match = stats.find((stat) => stat.name === obj.code);
const statsIndex = stats.findIndex((stat) => stat.name === obj.code);
if (!match) {
stats = [...stats, { name: obj.code, value: [obj.value] }];
} else {
stats[statsIndex] = {
name: stats[statsIndex].name,
value: [...stats[statsIndex].value, obj.value],
};
}
});
console.log(stats);

You can sort array and use key grouping approach:
const data = [{"teamFK": 8650,"code": "yellow_cards","typeId": 554,"value": "5","side": "home"},{"teamFK": 8650,"code": "goals","typeId": 554,"value": "1","side": "home"},{"teamFK": 8990,"code": "yellow_cards","typeId": 555,"value": "2","side": "away"},{"teamFK": 8990,"code": "goals","typeId": 555,"value": "0","side": "away"}];
const groups = data
.sort(({ side: a }, { side: b }) => b.localeCompare(a))
.reduce((acc, { code, value }) => {
acc[code] ??= { name: code, stats: [] };
acc[code]['stats'].push(value);
return acc;
}, {});
const result = { stats: Object.values(groups) };
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0 }

Related

How to flatten nested array of objects and create new array

I have a data structure as the following
[
{
"models":[
{
"name":"xyz",
"options":[
{
"label":"blue"
},
{
"label":"brown"
},
]
},
{
"name":"abc",
"options":[
{
"label":"yellow"
}
]
},
{
"name":"def",
"options":[
{
"label":"green"
}
]
}
]
}
]
The end result should be an array with all of the labels and name like xyz: blue, xyz: brown, abc: yellow, def: green
so something like this
['xyz: blue', 'xyz: brown', 'abc: yellow','def: green']
I'm trying different approaches, one with RxJS operators and another with reduce
let flat = (acc, current) => {
}
models.reduce(flat, [])
You can use a reduce and a map like this.
const arr = [
{
"models":[
{
"name":"xyz",
"options":[
{
"label":"blue"
},
{
"label":"brown"
},
]
},
{
"name":"abc",
"options":[
{
"label":"yellow"
}
]
},
{
"name":"def",
"options":[
{
"label":"green"
}
]
}
]
}
];
const result = arr[0].models.reduce(
(acc, model) => [...acc, ...model.options.map(i => ({ [model.name]: i.label }))]
, []
);
console.log(result);
If the top level array can have multiple items rather than arr[0] you would need to add another reduce feeding it's accumulator in to the second reduce as it's starting accumulator rather than the empty starting array.
const arr = [
{
"models":[
{
"name":"xyz",
"options":[
{
"label":"blue"
},
{
"label":"brown"
},
]
},
{
"name":"abc",
"options":[
{
"label":"yellow"
}
]
},
{
"name":"def",
"options":[
{
"label":"green"
}
]
}
]
},
{
"models":[
{
"name":"ghi",
"options":[
{
"label":"gold"
},
{
"label":"pink"
},
]
}
]
}
];
const result = arr.reduce(
(acc, item) =>
item.models.reduce(
(acc2, model) => [...acc2, ...model.options.map((i) => ({ [model.name]: i.label }))]
, acc
),
[]
);
console.log(result);
Not sure where RxJs comes into this question but if you are looking to transform an object like this that comes back from a http request you would pipe it into the map operator and then use this function inside the map. If you are looking to do a reduce on a stream there is a reduce operator that emits the accumulator when the source stream completes or the scan operator that emits the accumulator each time the source emits.
Use nested calls to flatMap(), and in the innermost call you concatenate the model name with the option label.
const data = [{
"models": [{
"name": "xyz",
"options": [{
"label": "blue"
},
{
"label": "brown"
},
]
},
{
"name": "abc",
"options": [{
"label": "yellow"
}]
},
{
"name": "def",
"options": [{
"label": "green"
}]
}
]
}];
let result = data.flatMap(d => d.models.flatMap(model => model.options.map(option => `${model.name}: ${option.label}`)));
console.log(result);
Here is using multiple forEach and destructuring
const flat = (arr, res = []) => {
arr.forEach(({ models }) =>
models.forEach(({ name, options }) =>
options.forEach(({ label }) => res.push({ [name]: label }))
)
);
return res;
};
const data = [
{
models: [
{
name: "xyz",
options: [
{
label: "blue",
},
{
label: "brown",
},
],
},
{
name: "abc",
options: [
{
label: "yellow",
},
],
},
{
name: "def",
options: [
{
label: "green",
},
],
},
],
},
];
console.log(flat(data));
const response = array[0].models.reduce((initial, model) => {
if (model.options.length === 1)
initial.push(`${model.name}: ${model.options[0].label}`);
else {
model.options.forEach((option) =>
initial.push(`${model.name}: ${option.label}`),
);
}
return initial;
}, []);
console.log(response)
;
// there is no need if the inner option has just one object you can just access it by model.options[0].label, that why there is a check to see if it one

Insert new JSON objects in nested JS array based on condition

For one of my e-commerce application requirement, I have a nested array of the form (Sample):
const data = [
{
"id": 1,
"group": "upper-wear",
"labels": [
{
"type": "shirts",
"quantity": "20",
},
],
popular: true
},
{
"id": 2,
"group": "bottom-wear",
"lables": [
{
"type": "trousers",
"quantity": "31",
},
],
popular: true
},
]
To this array, I need to insert new objects to the array 'labels' if the group value equals 'upper-wear'.
const newDataToInsert = [
{
"type": 'blazers',
"quantity": 19
},
]
This is what I tried so far, considering that for now I only need to insert to single label (i.e. 'upper-wear') (in future, there can be multiple labels category 'upper-wear', 'bottom-wear', to be inserted into):
const updatedArray = data.map((datum) => {
if (datum.group === 'upper-wear') {
return {
...datum,
labels: [...datum.labels, ...newDataToInsert]
};
}
});
console.log(updatedArray);
But there seems to be a silly issue that I am missing as the result returns like this:
[
{
id: 1,
group: 'upper-wear',
labels: [ [Object], [Object] ],
popular: true
},
undefined
]
I know there may be better approaches available, but this is what I can think of as the minimum solution for now.
any help to resolve the current or any better solution will be highly appreciated.
Try with this
updatedArray = data.map((d) => {
if (d.group && d.group === 'upper-wear') {
return { ...d, labels: d.labels.concat(newDataToInsert) }
} else {
return d;
}
})
const data = [
{
"id": 1,
"group": "upper-wear",
"labels": [
{
"type": "shirts",
"quantity": "20",
},
],
popular: true
},
{
"id": 2,
"group": "bottom-wear",
"lables": [
{
"type": "trousers",
"quantity": "31",
},
],
popular: true
},
];
const newDataToInsert = [
{
"type": 'blazers',
"quantity": 19
},
];
const updatedArray = data.map((d) => {
if (d.group && d.group === 'upper-wear') {
return { ...d, labels: d.labels.concat(newDataToInsert) }
} else {
return d;
}
});
console.log(updatedArray)
Explaination
Here while mapping the data, we check for the condition
IF
If it matches then we will first copy the whole object from the variable b return { ...b }
after that we take another variable with the same name lables return { ...d, labels: d.labels.concat(newDataToInsert) },As per the JSON default nature the new variable with the same name will hold the latest value
Here in labels we first take a copy of old data and then merge it with newDataToInsert array labels: d.labels.concat(newDataToInsert), It will merge 2 arrays and store them in JSON with the name labels
Else
In else we just return the current values else { return d; }
You don't actually need to iterate with map over the array. Just find an object in the array and change what you want.
const data=[{id:1,group:"upper-wear",labels:[{type:"shirts",quantity:"20"}],popular:true},{id:2,group:"bottom-wear",lables:[{type:"trousers",quantity:"31"}],popular:true}];
const newDataToInsert=[{type:"blazers",quantity:19}];
data.find(({ group }) => group === 'upper-wear')?.labels.push(...newDataToInsert);
console.log(data);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You're not returning all objects from your map. you're only returning a result when your criteria is met. This is resulting in your undefined objects...
const data = [
{ "id": 1, "group": "upper-wear", "labels": [ { "type": "shirts", "quantity": "20", }, ], popular: true },
{ "id": 2, "group": "bottom-wear", "lables": [ { "type": "trousers", "quantity": "31", }, ], popular: true },
]
const newDataToInsert = [ { "type": 'blazers',"quantity": 19 }, ]
const updatedArray = data.map(datum => {
if (datum.group === 'upper-wear') datum.labels = [...datum.labels, ...newDataToInsert]
return datum
});
console.log(updatedArray);
You can use Array#find to locate the desired group and then change labels for the group found. There are two options depending on how many items you would like to insert. Use Array#push to add the desired item; use forEach for more than one item:
const searchgroup = "upper-wear";
const target = data.find(({group}) => group === searchgroup);
target.labels.push(...newDataToInsert); //For one item to insert
//newDataToInsert.forEach(label => target.labels.push( label )); //For more than one item
const data = [{"id": 1, "group": "upper-wear", "labels": [{"type": "shirts", "quantity": "20"},],popular: true }, {"id": 2, "group": "bottom-wear", "lables": [{"type": "trousers", "quantity": "31", },],popular: true}];
const newDataToInsert = [{"type": 'blazers', "quantity": 19}];
//group to find
const searchgroup = "upper-wear";
//target element in data
const target = data.find(({group}) => group === searchgroup);
//check if group was found
if( target ) {
//if there's only one product in newDataToInsert us this:
//target.labels.push(...newDataToInsert);
//if you have more than one product to be inserted use this; also works for one
newDataToInsert.forEach(label => target.labels.push( label ));
} else {
console.log( `No such group found: ${searchgroup}!` );
}
console.log( data );

filtering list by multiple conditions

there is a list of users
filterData = [
{
"position":"lawyer",
"department_positions":[],
"group_positions":[
{"group":{"id":2,"code":"234","name":"group1"},"lead":false},
{"group":{"id":1,"code":"123","name":"group12"},"lead":true}
]
},
{
"position":"director",
"department_positions":[
{"department":{"id":3,"code":"333","name":"subDep"},"lead":false}
],
"group_positions":[
{"group":{"id":2,"code":"234","name":"group1"},"lead":false},
{"group":{"id":1,"code":"123","name":"group12"},"lead":true}
]
},
{
"position":"director",
"department_positions":[],
"group_positions":[]
}
]
and list of filters
categories = {
"position":["lawyer","director"],
"group_positions":["group1","group12"],
"department_positions":["generalDep", "subDep"]
}
It is necessary to filter users taking into account the fact that several filters can be selected at the same time. For example, i want to find user with position = "director" and AND group_positions = "group1" AND department_positions = "subDep"
my code doesn't allow filtering by multiple conditions. how can i fix it?
this.filter = this.filterData.filter(item => {
for (let key in this.categories) {
if (item[key].find(el =>
this.categories[key].includes(
el.group?.name || el.department?.name
)
)) {
return true
}
}
return false
})}
This is a good place to employ an es6 class to give behavior to the object being filtered. Augment each object to determine if it matches the "category" object.
(from the example data, this assumes the OP is looking for a "product of sums" match: for all of the category keys match at least one of the category values)
class FilterMe {
constructor(item) {
Object.assign(this, item);
}
namesForKey(key) {
switch (key) {
case 'position':
return [this.position]; // always answer an array
case 'group_positions':
return this.group_positions.map(gp => gp.group.name);
case 'department_positions':
return this.department_positions.map(dp => dp.department.name);
default:
return [];
}
}
// return true if a single filter key-value pair is matched
matchesFilterKeyValue(filterKey, filterOptions) {
const myNames = this.namesForKey(filterKey);
const matches = filterOptions.filter(e => myNames.includes(e));
return matches.length > 0;
}
// return true if all filter key-values pairs are matched
matchesFilter(filter) {
return Object.entries(filter).every(keyValue => {
return this.matchesFilterKeyValue(...keyValue);
})
}
}
const filterData = [{
"position": "lawyer",
"department_positions": [],
"group_positions": [{
"group": {
"id": 2,
"code": "234",
"name": "group1"
},
"lead": false
}, {
"group": {
"id": 1,
"code": "123",
"name": "group12"
},
"lead": true
}]
},
{
"position": "director",
"department_positions": [{
"department": {
"id": 3,
"code": "333",
"name": "subDep"
},
"lead": false
}],
"group_positions": [{
"group": {
"id": 2,
"code": "234",
"name": "group1"
},
"lead": false
}, {
"group": {
"id": 1,
"code": "123",
"name": "group12"
},
"lead": true
}]
},
{
"position": "director",
"department_positions": [],
"group_positions": []
}
]
const categories = {
"position": ["lawyer", "director"],
"group_positions": ["group1", "group12"],
"department_positions": ["generalDep", "subDep"]
}
// convert the filterData to the objects and test them...
let objects = filterData.map(d => new FilterMe(d));
let matches = objects.filter(o => o.matchesFilter(categories))
console.log(matches)
You can try something like this:
let filtered = example.filter(item => {
let valid = false
if (item.includes('something')) {
valid = true
}
if (!valid) {
// check second condition
}
return valid
})
Use a temporary placeholder so you don't immediately have to return true/false.

How to calculate average from nested values in an array using javascript?

I am trying to calculate the average duration for each stage. So in the array below - I should be able to get the average duration for 'test1', which would be 2.
jobs = [
{
"build_id": 1,
"stage_executions": [
{
"name": "test1"
"duration": 1,
},
{
"name": "test2"
"duration": 16408,
},
{
"name": "test3"
"duration": 16408,
},
]
},
{
"build_id": 2,
"stage_executions": [
{
"name": "test1"
"duration": 3,
},
{
"name": "test2"
"duration": 11408,
},
{
"name": "test3"
"duration": 2408,
},
]
}
]
My failed attempt:
avgDuration: function(jobs) {
let durationSum = 0
for (let item = 0; item < this.jobs.length; item++) {
for (let i = 0; i < this.jobs[item].stage.length; item++) {
durationSum += stage.duration
}
durationAverage = durationSum/this.jobs[item].stage.length
}
return durationAverage
What am I doing wrong? I'm not sure how to accomplish this since the duration is spread out between each job.
UPDATE:
This is return a single average for all stages rateher than per stage
<template>
<div class="stages">
<h3>
Average Duration
</h3>
<table>
<tbody>
<tr v-for="item in durations">
<td>
<b>{{ item.average}} {{ item.count }}</b>
// this returns only 1 average and 177 count instead of 10
<br />
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import { calculateDuration } from "../../helpers/time.js";
import { liveDuration } from "../../helpers/time.js";
import moment from "moment";
export default {
name: "Stages",
data() {
return {
jobs: [],
durations: []
};
},
methods: {
avgDuration: function(jobs) {
var averageByName = {}; // looks like { 'name': { average: 111, count: 0 }}
for (var job of jobs) {
for(var stage of job.stage_execution) {
if (averageByName[stage.name] == null) { // we need a new object
averageByName[stage.name] = { average: 0, count: 0 };
}
// just name it so its easier to read
var averageObj = averageByName[stage.name];
// update count
averageObj.count += 1;
// Cumulative moving average
averageObj.average = averageObj.average + ( (stage.duration - averageObj.average) / averageObj.count );
console.log(averageObj.count)
}
}
return averageByName
},
},
created() {
this.JobExecEndpoint =
process.env.VUE_APP_TEST_URL +
"/api/v2/jobs/?limit=10";
fetch(this.JobExecEndpoint)
.then(response => response.json())
.then(body => {
for (let i = 0; i < body.length; i++) {
this.jobs.push({
name: body[i].job.name,
job: body[i].job,
stage_execution: body[i].stage_executions,
});
}
})
.then(() => {
this.$emit("loading", true);
})
.then(() => {
this.durations = this.avgDuration(this.jobs);
})
.catch(err => {
console.log("Error Fetching:", this.JobExecEndpoint, err);
return { failure: this.JobExecEndpoint, reason: err };
});
}
};
</script>
We can do this pretty simply and without overflow from having too many numbers by using a Cumulative moving average and a few loops.
Here is a line the relevant Wikipedia page on Moving Averages and the most relvant formula below.
I will not go into much detail with the above as there are a lot of documents describing this sort of thing. I will however say that the main reason to this over adding all the values together is that there is a far lower chance of overflow and that is why I am using it for this example.
Here is my solution with comments made in code.
var jobs = [ { "build_id": 1, "stage_executions": [ { "name": "test1", "duration": 1, }, { "name": "test2", "duration": 16408, }, { "name": "test3", "duration": 16408, }, ] }, { "build_id": 2, "stage_executions": [ { "name": "test1", "duration": 3, }, { "name": "test2", "duration": 11408, }, { "name": "test3", "duration": 2408, }, ] } ];
var averageByName = {}; // looks like { 'name': { average: 111, count: 0 }}
for (var job of jobs) {
for(var stage of job.stage_executions) {
if (averageByName[stage.name] == null) { // we need a new object
averageByName[stage.name] = { average: 0, count: 0 };
}
// just name it so its easier to read
var averageObj = averageByName[stage.name];
// update count
averageObj.count += 1;
// Cumulative moving average
averageObj.average = averageObj.average + ( (stage.duration - averageObj.average) / averageObj.count );
}
}
// print the averages
for(var name in averageByName) {
console.log(name, averageByName[name].average);
}
Let me know if you have any questions or if anything is unclear.
You could collect the values in an object for each index and map later only the averages.
var jobs = [{ build_id: 1, stage_executions: [{ name: "test1", duration: 1 }, { name: "test2", duration: 16408 }, { name: "test3", duration: 16408 }] }, { build_id: 2, stage_executions: [{ name: "test1", duration: 3 }, { name: "test2", duration: 11408 }, { name: "test3", duration: 2408 }] }],
averages = jobs
.reduce((r, { stage_executions }) => {
stage_executions.forEach(({ duration }, i) => {
r[i] = r[i] || { sum: 0, count: 0 };
r[i].sum += duration;
r[i].avg = r[i].sum / ++r[i].count;
});
return r;
}, []);
console.log(averages.map(({ avg }) => avg));
console.log(averages);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I've used Array.prototype.flatMap to flatten the jobs array into an array of {name:string,duration:number} object. Also, to make more solution a bit more dynamic the function takes in a field argument which returns the average for that specific field.
const jobs = [
{
"build_id": 1,
"stage_executions": [
{
"name": "test1",
"duration": 1,
},
{
"name": "test2",
"duration": 16408,
},
{
"name": "test3",
"duration": 16408,
},
]
},
{
"build_id": 2,
"stage_executions": [
{
"name": "test1",
"duration": 3,
},
{
"name": "test2",
"duration": 11408,
},
{
"name": "test3",
"duration": 2408,
},
]
}
];
const caller = function(jobs, field) {
const filtered = jobs
.flatMap((item) => item.stage_executions)
.filter(item => {
return item.name === field;
})
const total = filtered.reduce((prev, curr) => {
return prev + curr.duration;
}, 0)
return total / filtered.length;
}
console.log(caller(jobs, 'test1'))
console.log(caller(jobs, 'test2'))
console.log(caller(jobs, 'test3'))
In case you get the error flatMap is not a function. You can add this code snippet in your polyfill or at the top of your js file.
Array.prototype.flatMap = function(lambda) {
return Array.prototype.concat.apply([], this.map(lambda));
};
PS: for demostration, I obtained the flatMap implementation from here

javascript - reorder array of objects depending on property

I am building a select dropdown input for a webpage. I want to make a 'popular' options group which appears at the top of the dropdown.
I am working with data in the following structure.
I need to find a way to reorder the items inside the people array based on their name.
For example moving:
pogo-stick from toys[2] -> toys[0]
cards from toys[3] to toys [2]
I will have an array of popular toys such as:
popularToys: [
"cards", "pogo-stick"
]
How can I iterate through the array of objects and move them in to the new order?
Data:
{
"toys": [
{
"name": "car",
"price": "10"
},
{
"name": "duck",
"price": "25"
},
{
"name": "pogo-stick",
"price": "60"
},
{
"name": "cards",
"price": "5"
}
]
}
Use forEach() loop where you can find the index of the toy object and swap:
var popularToys = [
"cards", "pogo-stick"
]
var data = {
"toys": [
{
"name": "car",
"price": "10"
},
{
"name": "duck",
"price": "25"
},
{
"name": "pogo-stick",
"price": "60"
},
{
"name": "cards",
"price": "5"
}
]
};
popularToys.forEach(function(toy, index){
var toyObjIndex = data.toys.findIndex(x => x.name==toy);
//swap
var tempObj = data.toys[toyObjIndex];
data.toys[toyObjIndex] = data.toys[index];
data.toys[index] = tempObj;
});
console.log(data);
Using a combination of map and filter we are able to split the required logic into to methods (Maybe more readable)
Popular() returns a filtered Array of any of the toy items that have a name property that corresponds with the current name in the iteration of popular
Rest() returns a filtered Array of toys where the name property of the toy in the iteration does not exist in the Array of String in popular
const toys = [
{
name: 'car',
price: '10'
},
{
name: 'exception',
price: '999999'
},
{
name: 'duck',
price: '25'
},
{
name: 'pogo-stick',
price: '60'
},
{
name: 'cards',
price: '5'
},
{
name: 'another !exception',
price: '100000'
},
{
name: 'pogo-stick',
price: 'A MILLION POUNDS'
},
{
name: 'duck',
price: '100'
}
]
const popular = [
'cards',
'pogo-stick',
'car',
'duck'
]
const Popular = () => {
return [].concat(...popular.map(n => toys.filter(({name}) => name === n)))
}
const Rest = () => toys.filter(({name}) => popular.indexOf(name) === -1)
let ordered = [].concat(...Popular(), ...Rest())
console.log(ordered)
You could use a custom sort function
var popularToys = [
"cards", "pogo-stick"
]
var data = {
"toys": [
{
"name": "car",
"price": "10"
},
{
"name": "duck",
"price": "25"
},
{
"name": "pogo-stick",
"price": "60"
},
{
"name": "cards",
"price": "5"
}
]
};
function popularFirst(a, b) {
var aIsPopular = popularToys.indexOf(a.name) > -1;
var bIsPopular = popularToys.indexOf(b.name) > -1;
if (aIsPopular) {
// b could be popular or not popular, a still comes first
return -1;
} else if (bIsPopular) {
// a isnt popular but b is, change the order
return 1;
} else {
// no change
return 0;
}
}
console.log(data.toys.sort(popularFirst));
function compare(a,b) {
if (a.name < b.name)
return -1;
if (a.name > b.name)
return 1;
return 0;
}
toys.sort(compare);
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort

Categories