How to merge two intersecting arrays via es6 - javascript

I have two arrays that have properties: minValue, maxValue, avgValue. I would like to write a very clean approach to identifying which objects match (the displayName) in the array and then assign the values of the new array to the old (minPrice, maxPrice, avgPrice).
I have this code so far
export interface Player {
displayName: string;
MinPrice: number;
MaxPrice: number;
AvgPrice: number;}
export const MERGE_PLAYER_STATS = (playerStat: Player[], auctionStat: Player[]): any => {
const reducer = (playerStat, auctionStat) => {
playerStat.MinPrice = auctionStat.minPrice,
playerStat.MaxPrice = auctionStat.maxPrice,
playerStat.AvgPrice = auctionStat.avgPrice,
playerStat.Team = auctionStat.team;
}
return reducer(playerStat, auctionStat =>
filter(auctionStat, playerStat => auctionStat.displayName.includes(playerStat.displayName)));
}
Input: two different set of player arrays that have common display Names.
playerStat: [] = [];
auctionStat: [] = [];
playerStat.push( {
displayName: "Josh Allen",
minPrice: "",
maxPrice: "",
avgPrice: ""
},
{
displayName: "No One",
minPrice: "",
maxPrice: "",
avgPrice: ""
});
auctionStat.push( {
displayName: "Josh Allen",
minPrice: 1,
maxPrice: 2,
avgPrice: 1
},
{
displayName: "No One 2",
minPrice: 1,
maxPrice: 1,
avgPrice: 2
});
The output should only have Josh Allen stats being updated from blank values to 1,2,1 respectively.
Please let me know what your clean approach this. FYI, this code is not returning what I want it to.

The simple approach would be to simply write a mapping function that copies in new objects based on matching keys.
Given an interface:
interface MyInterface {
key: string;
value: number;
}
And two arrays:
const existingArray: MyInterface[] = [
{ key: 'a', value: 1 },
{ key: 'b', value: 2 },
{ key: 'c', value: 3 }
];
const newArray: MyInterface[] = [
{ key: 'b', value: 20 },
{ key: 'c', value: 30 },
{ key: 'd', value: 40 }
];
Expected output
I want to update the existingArray with the values from newArray where the keys match.
If a value isn't found in newArray, the existing item is untouched.
If a new value is found in newArray, it isn't merged in to oldArray
All property values are to be overwritten
My expected output is:
const expected = [
{ key: 'a', value: 1 },
{ key: 'b', value: 20 },
{ key: 'c', value: 30 }
];
Solution
I would achieve this by using this merge function. It is just a javascript array map:
private merge<T>(existing: T[], updated: T[], getKey: (t: T) => string): T[] {
// start with the existing array items
return existing.map(item => {
// get the key using the callback
const key: string = getKey(item);
// find the matching item by key in the updated array
const matching: T = updated.find(x => getKey(x) === key);
if (matching) {
// if a matching item exists, copy the property values to the existing item
Object.assign(item, matching);
}
return item;
});
}
I have made it generic to allow you to use it with any type. All you need to do it provide the 2 arrays, and a callback to identify the key.
I would use it to created a merged array like this:
const merged = this.merge(existingArray, newArray, x => x.key);
DEMO: https://stackblitz.com/edit/angular-nn8hit
A word of warning
This wouldn't scale well with very large arrays. If you are using large arrays, I would optimise by creating indexes of type Map<string, T>, to keep the number of loops fixed. This implementation is out of scope of this question.
Edit:
If you only want to update specific values, you could pass in an update callback:
private merge<T>(existing: T[], updated: T[], getKey: (t: T) => string,
update: (item, matching) => void
): T[] {
return existing.map(item => {
const key: string = getKey(item);
const matching: T = updated.find(x => getKey(x) === key);
if (matching) {
update(item, matching);
}
return item;
});
}
And call it like this:
const merged = this.merge(existingArray, newArray, x => x.key,
(item, matching) => item.value = matching.value);

Use Object.assign()
Ex: oldArray = Object.assign(oldArray, newArray)

playerStat.forEach((initItem) => {
let result = auctionStat.filter((item) => (
initItem.displayName === item.displayName
))
// let me suppose the same displayName only has one
result.length && Object.assign(initItem, result[0])
})

Related

Create an array of objects based on an object if one or more properties have multiple values differentiated by a comma

i'm trying to duplicate objects based on two properties that have multiple values differentiated by a comma.
For example:
I have an object
const obj = {
id: 1
date: "2021"
tst1: "111, 222"
tst2: "AAA, BBB"
}
And I would like the result to be an array of 2 objects in this case (because there are 2 values in tst1 OR tst2, these 2 properties will always have the same nr of values differentiated by a comma)
[{
id: 1,
date: "2021",
tst1: "111",
tst2: "AAA",
},
{
id: 1,
date: "2021",
tst1: "222",
tst2: "BBB",
}]
What I tried is this:
I created a temporary object
const tempObject = {
id: obj.id,
date: obj.date,
}
And then I would split and map the property that has multiple values, like this:
cont newObj = obj.tst1.split(",").map(function(value) {
let finalObj = {}
return finalObj = {
id: tempObject.id,
date: tempObject.date,
tst1: value,
})
And now, the newObj is an array of objects and each object contains a value of tst1.
The problem is I still have to do the same for the tst2...
And I was wondering if there is a simpler method to do this...
Thank you!
Here is an example that accepts an array of duplicate keys to differentiate. It first maps them to arrays of entries by splitting on ',' and then trimming the entries, then zips them by index to create sub-arrays of each specified property, finally it returns a result of the original object spread against an Object.fromEntries of the zipped properties.
const mapDuplicateProps = (obj, props) => {
const splitProps = props.map((p) =>
obj[p].split(',').map((s) => [p, s.trim()])
);
// [ [[ 'tst1', '111' ], [ 'tst1', '222' ]], [[ 'tst2', 'AAA' ], [ 'tst2', 'BBB' ]] ]
const dupeEntries = splitProps[0].map((_, i) => splitProps.map((p) => p[i]));
// [ [[ 'tst1', '111' ], [ 'tst2', 'AAA' ]], [[ 'tst1', '222' ], [ 'tst2', 'BBB' ]] ]
return dupeEntries.map((d) => ({ ...obj, ...Object.fromEntries(d) }));
};
const obj = {
id: 1,
date: '2021',
tst1: '111, 222',
tst2: 'AAA, BBB',
};
console.log(mapDuplicateProps(obj, ['tst1', 'tst2']));
Not sure if that's what you're searching for, but I tried making a more general use of what you try to do:
const duplicateProperties = obj => {
const properties = Object.entries(obj);
let acc = [{}];
properties.forEach(([key, value]) => {
if (typeof value === 'string' && value.includes(',')) {
const values = value.split(',');
values.forEach((v, i) => {
if (!acc[i]) {
acc[i] = {};
}
acc[i][key] = v.trim();
});
} else {
acc.forEach(o => o[key] = value);
}
});
return acc;
};
const obj = {
id: 1,
date: '2021',
tst1: '111, 222',
tst2: 'AAA, BBB',
};
console.log(duplicateProperties(obj));
You could start by determining the length of the result using Math.max(), String.split() etc.
Then you'd create an Array using Array.from(), returning the correct object for each value of the output index.
const obj = {
id: 1,
date: "2021",
tst1: "111, 222",
tst2: "AAA, BBB",
}
// Determine the length of our output array...
const length = Math.max(...Object.values(obj).map(s => (s + '').split(',').length))
// Map the object using the relevant index...
const result = Array.from({ length }, (_, idx) => {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => {
const a = (value + '').split(/,\s*/);
return [key, a.length > 1 ? a[idx] : value ]
}))
})
console.log(result)
.as-console-wrapper { max-height: 100% !important; }

How to reduce a property within an array of array?

I have an array of products which in turn have an array of categories. I want to extract distinct values of the type property on category object.
Both the Lodash and native versions below do the job.
I want to make a generic function which takes the path of property and return unique values.
Essentially I am looking at something terse like
map(products, property("categories[].type") but heres the longer version(s)
import { compact, flatten, map, property, uniq } from "lodash";
export const getAllTypes1 = (products) => {
return uniq(
compact(map(flatten(map(products, property("categories"))), "type"))
);
};
export const getAllTypes2 = (products) => {
const types = [];
products.forEach((product) => {
product.categories.forEach((category) => {
if (!types.some((t) => t === category.type)) {
types.push(category.type);
}
});
});
return types;
};
Example data
const product1 = {
name: 'Wilson Orange',
price: 72.50,
categories: [{
type: 'flash sale',
discountable: false,
},{
type: 'tennis',
discountable: true,
}]
};
const product2 = {
name: 'Babolat Green',
price: 65.50,
categories: [{
type: 'tennis',
discountable: true,
}]
};
const products = [product1, product2];
Result
const result = getAllTypes2(products);
console.log(result); // ["flash sale", "tennis"]
Here's a working example
Here's a vanilla JS function that takes the path without needing [] and automatically checks arrays wherever it finds one.
How it works is:
Create an empty Set to easily remove duplicates
Turn the path string to an array of properties -> props
Call a recursive function recurse(currObj, props) which:
Checks if the currObj is an array, and if it is:
a. Recurses again with the array values as currObj
b. Use the same props since we didn't check an object in the path
Check if we're at the last prop in the path, if yes
a. Add the property's value in the current object to the set
Otherwise. recurse with currObj[currProp], and the remaining props
Convert the set to an array and return it.
const product1 = {
name: 'Wilson Orange',
price: 72.5,
categories: [
{
type: 'flash sale',
discountable: false,
},
{
type: 'tennis',
discountable: true,
},
],
};
const product2 = {
name: 'Babolat Green',
price: 65.5,
categories: [
{
type: 'tennis',
discountable: true,
},
],
};
const products = [product1, product2];
function getProperties(array, path) {
const props = path.split('.');
const values = new Set();
function recurse(currObj, props) {
const currProp = props[0]
const nextProps = props.slice(1);
if (Array.isArray(currObj)) {
for (let val of currObj) {
recurse(val, props);
}
return
}
if (nextProps.length === 0) {
values.add(currObj[currProp])
} else {
recurse(currObj[currProp], nextProps)
}
}
recurse(array, props);
return [...values];
}
console.log(getProperties(products,'categories.type'))
console.log(getProperties(products,'price'))
console.log(getProperties(products,'name'))
It's not a property path string, but it's pretty terse and expressive:
const pipe = (...fs) => fs.reduceRight(
(next, f) => x => f(x, next), x => x,
);
const getAllTypes = pipe(
(x, next) => [...new Set(x.flatMap(next))],
(x, next) => x.categories.map(next),
(x) => x.type,
);
// equivalent to
// const getAllTypes =
// x => [...new Set(x.flatMap(y => y.categories.map(z => z.type)))];
const products = [{
name: 'Wilson Orange',
price: 72.50,
categories: [{
type: 'flash sale',
discountable: false,
}, {
type: 'tennis',
discountable: true,
}]
}, {
name: 'Babolat Green',
price: 65.50,
categories: [{
type: 'tennis',
discountable: true,
}]
}];
console.log(getAllTypes(products));
The parameter x => x allows pipe to be called without any arguments and return the identity function. It also allows the last function argument of pipe to accept a next parameter for consistency, i.e. (x, next) => next(x.type) would have been equivalent to (x) => x.type.
Reference:
Array.prototype.flatMap
Array.prototype.map
Array.prototype.reduceRight
Set

JavaScript filter instead map

I have this code:
const uniform = {
zone: 'BOTTOM'
}
const sizes = [
{
_id: 'sizeId2',
zones: ['BOTTOM'],
value: '48'
},
{
_id: 'sizeId3',
zones: ['BOTTOM'],
value: '42'
},
]
sizes.map((size) => (size.zones.includes(uniform.zone) ? {
_id: size._id,
value: size.value,
} : null))
https://jsfiddle.net/pmiranda/945fsdw7/5/
// here I'm trying another way than map and the ternary
console.log(sizes.filter((size) => (size.zones.includes(uniform.zone) && {
_id: size._id,
value: size.value,
})))
I wonder, how could I replace that map with a filter? Because I think that mapping with a ternary with null could be done in a better way, plus, in the map way I'm getting that null in the end, I want to not add it to the array
I think that you'd filter then map:
sizes.filter(size => size.zones.includes(uniform.zone)).map(size => ({
_id: size._id,
value: size.value,
}));
You probably need to return the result, or assign it to a variable.
const uniform = { zone: 'BOTTOM' }
const sizes = [
{
_id: 'sizeId2',
zones: ['BOTTOM'],
value: '48'
},
{
_id: 'sizeId3',
zones: ['BOTTOM'],
value: '42'
},
]
// reduce
const newSizes = sizes.reduce((acc, size) =>
{
if(size.zones.includes(uniform.zone)) {
acc.push(
{
_id: size._id,
value: size.value
})
}
return acc;
}, []
)
console.log(newSizes);
Using either map or filter does not make sense in the cuurent usecase as you are not utilizing the return value.
If you need to just loop over the array and create new entries (containing or nnot containing null) you can use forEach or reduce.
From the "comments", reduce is more suitable for you.
Without map and filter, just create new array
let newSizes = [];
sizes.forEach(({zones, _id, value}, key) => zones.includes(uniform.zone) && (newSizes[key] = {_id, value}))

How to merge 2 array of objects by id and date in Javascript?

This 2 arrays have multiple objects that has the the same ID but different dates
const names= [
{id:'1',name:'a',date:'1604616214'},
{id:'1',name:'Angel',date:'1604616215'},
{id:'2',name:'b',date:'2004616214'},
{id:'2',name:'Karen',date:'2004616215'},
{id:'3',name:'a',date:'3004616220'},
{id:'3',name:'Erik',date:'3004616221'}
]
const lastnames= [
{id:'1',lastname:'a',date:'4004616220'},
{id:'1',lastname:'Ferguson',date:'4004616221'},
{id:'2',lastname:'b',date:'5004616220'},
{id:'2',lastname:'Nixon',date:'5004616221'},
{id:'3',lastname:'a',date:'6004616222'},
{id:'3',lastname:'Richard',date:'6004616223'}
]
The data is in moment().unix() to create a number "easy to compare"
I want to create a Third array that merge the 2 arrays and create objects with the same id and the last updated date object.
The output should be something like this
const third = [
{id:'1',name:'Angel',lastname:'Ferguson'},
{id:'2',name:'Karen',lastname:'Nixon'},
{id:'3',name:'Erik',lastname:'Richard'}
]
This is what i got so far, if i updated the arrays it duplicates and i need to have only the last updated object
const third = names.map(t1 => ({...t1, ...lastnames.find(t2 => t2.id === t1.id)}))
I'm going to assume since you have the spread operator and Array.find in your example that you can use ES6, which includes for of and Object.values as you see below.
An object and simple looping is used to reduce the amount of times you're iterating. In your example, for every element in names you're iterating over last names to find one with the same ID. Not only is that not ideal for performance, but it doesn't work because every time you're finding the same element with that ID (the first one with that ID in the array).
const names = [
{ id: "1", name: "a", date: "1604616214" },
{ id: "1", name: "Angel", date: "1604616215" },
{ id: "2", name: "b", date: "2004616214" },
{ id: "2", name: "Karen", date: "2004616215" },
{ id: "3", name: "a", date: "3004616220" },
{ id: "3", name: "Erik", date: "3004616221" },
];
const lastnames = [
{ id: "1", lastname: "a", date: "4004616220" },
{ id: "1", lastname: "Ferguson", date: "4004616221" },
{ id: "2", lastname: "b", date: "5004616220" },
{ id: "2", lastname: "Nixon", date: "5004616221" },
{ id: "3", lastname: "a", date: "6004616222" },
{ id: "3", lastname: "Richard", date: "6004616223" },
];
const profiles = {};
function addToProfiles(arr, profiles) {
for (let obj of arr) {
if (obj.id != null) {
// Inits to an empty object if it's not in the profiles objects
const profile = profiles[obj.id] || {};
profiles[obj.id] = { ...profile, ...obj };
}
}
}
addToProfiles(names, profiles);
addToProfiles(lastnames, profiles);
const third = Object.values(profiles);
The idea is to group the objects by their ids, then merge each group according to the rules, maximizing date for each type of record (name and lastname)
// the input data
const names= [
{id:'1',name:'a',date:'1604616214'},
{id:'1',name:'Angel',date:'1604616215'},
{id:'2',name:'b',date:'2004616214'},
{id:'2',name:'Karen',date:'2004616215'},
{id:'3',name:'a',date:'3004616220'},
{id:'3',name:'Erik',date:'3004616221'}
]
const lastnames= [
{id:'1',lastname:'a',date:'4004616220'},
{id:'1',lastname:'Ferguson',date:'4004616221'},
{id:'2',lastname:'b',date:'5004616220'},
{id:'2',lastname:'Nixon',date:'5004616221'},
{id:'3',lastname:'a',date:'6004616222'},
{id:'3',lastname:'Richard',date:'6004616223'}
]
// make one long array
let allNames = [...names, ...lastnames]
// a simple version of lodash _.groupBy, return an object like this:
// { '1': [ { objects with id==1 }, '2': [ ... and so on ] }
function groupById(array) {
return array.reduce((acc, obj) => {
let id = obj.id
acc[id] = acc[id] || [];
acc[id].push(obj);
return acc;
}, {});
}
// this takes an array of objects and merges according to the OP rule
// pick the maximum date name object and maximum date lastname object
// this sorts and searches twice, which is fine for small groups
function mergeGroup(id, group) {
let sorted = group.slice().sort((a, b) => +a.date < +b.date)
let name = sorted.find(a => a.name).name
let lastname = sorted.find(a => a.lastname).lastname
return {
id,
name,
lastname
}
}
// first group, then merge
let grouped = groupById(allNames)
let ids = Object.keys(grouped)
let results = ids.map(id => {
return mergeGroup(id, grouped[id])
})
console.log(results)
I tried to come up with a solution using filter functions. End result contains the format you wanted. check it out.
const names= [
{id:'1',name:'a',date:'1604616214'},
{id:'1',name:'Angel',date:'1604616215'},
{id:'2',name:'b',date:'2004616214'},
{id:'2',name:'Karen',date:'2004616215'},
{id:'3',name:'a',date:'3004616220'},
{id:'3',name:'Erik',date:'3004616221'}
]
const lastnames= [
{id:'1',lastname:'a',date:'4004616220'},
{id:'1',lastname:'Ferguson',date:'4004616221'},
{id:'2',lastname:'b',date:'5004616220'},
{id:'2',lastname:'Nixon',date:'5004616221'},
{id:'3',lastname:'a',date:'6004616222'},
{id:'3',lastname:'Richard',date:'6004616223'}
]
// filter out last updated objects from both arrays
var lastUpdatednames = names.filter(filterLastUpdate,names);
console.log(lastUpdatednames);
var lastUpdatedsurnames = lastnames.filter(filterLastUpdate,lastnames);
console.log(lastUpdatedsurnames);
// combine the properties of objects from both arrays within filter function.
const third = lastUpdatednames.filter(Combine,lastUpdatedsurnames);
console.log(third);
function filterLastUpdate(arrayElement)
{
var max = this.filter( i => arrayElement.id==i.id ).reduce(
function(prev, current)
{
return (prev.date > current.date) ? prev : current
}
)
return max.date == arrayElement.date ;
}
function Combine(firstArray)
{
var subList= this.filter( i => firstArray.id==i.id );
//console.log(subList);
//console.log(subList[0]);
if (subList)
{
firstArray.lastname = subList[0].lastname;
return true;
}
return false ;
}
Here is last output:
[…]
0: {…}
date: "1604616215"
id: "1"
lastname: "Ferguson"
name: "Angel"
1: {…}
date: "2004616215"
id: "2"
lastname: "Nixon"
name: "Karen"
2: {…}
date: "3004616221"
id: "3"
lastname: "Richard"
name: "Erik"

Building adjacency matrix from an array of objects

I have a plain JavaScript array of objects, say e.g.
const drawings = [
{
name: "Foo",
category: "widget"
},
{
name: "Bar",
category: "widget"
},
{
name: "Bar",
category: "fidget"
},
]
etc, where both the name and category have duplicates. What I want to end up with is essentially a list of objects (this is to meet the interface for a 3rd party library), where each object represents a name, and then for each category there is a property that is either true or false, depending on the original list. So for the example the output would be:
const output = [
{
name: "Foo",
widget: true,
fidget: false
},
{
{
name: "Bar",
widget: true,
fidget: true
},
]
I would first go through and make an object of your categories with the categories as keys and default values as false.
Then you can assign this to each object and set the correct keys to true as you go through.
const drawings = [{name: "Foo",category: "widget"},{name: "Bar",category: "widget"},{name: "Bar",category: "fidget"},]
// make category object where everything is false
let category_obj = drawings.reduce((a, item) => (a[item.category] = false, a), {})
let output = drawings.reduce((a, {name, category}) => {
// assign cat
if (!a.hasOwnProperty(name)) a[name] = Object.assign({}, {name}, category_obj)
// set to true if the correct category
a[name][category] = true
return a
}, {})
// the above makes an object, but you only want the array of values
console.log(Object.values(output))
If you already know the categories or if you have infered them as you suggested, you could use Array.reduce() like such:
drawings.reduce(function(acc, curr) {
if (!acc.some(elt => elt.name === curr.name)) {
acc.push({name: curr.name, widget: false, fidget: false})
}
const i = acc.findIndex(elt => elt.name === curr.name)
acc[i][curr.category] = true
return acc
}, [])

Categories