The following code produces the desired result but is there a way to refactor it to eliminate the accumulator variable?
const data = [
{ id: "428", date: "2017-01-24" },
{ id: "526", date: "2022-01-01" },
{ id: "428", name: "George" },
{ id: "526", name: "Sam" },
{ id: "827", name: "Dan" }
];
const accumulator = {};
data.forEach(o => {
accumulator[o.id] = { ...accumulator[o.id] , ...o } || o;
});
console.log(accumulator);
The built-in method reduce already does this. Pass the accumulator as the second argument. The first argument in the callback is the accumulated value. The second is the current object in this iteration. Return value of callback is passed to next iteration as the accumulated value.
const data = [
{ id: "428", date: "2017-01-24" },
{ id: "526", date: "2022-01-01" },
{ id: "428", name: "George" },
{ id: "526", name: "Sam" },
{ id: "827", name: "Dan" },
];
const result = data.reduce((accumulator, o) => {
accumulator[o.id] = { ...accumulator[o.id] , ...o } || o;
return accumulator;
}, {});
console.log(result);
The || o part doesn’t make sense (objects are always truthy), and the repeated spread can become a serious performance trap. It’s also good practice to start with Object.create(null) when using an object as a map to avoid keys colliding with things on Object.prototype (even though it looks like you have numeric keys for the moment).
So, let’s restart from this more efficient and reliable version:
const data = [
{ id: "428", date: "2017-01-24" },
{ id: "526", date: "2022-01-01" },
{ id: "428", name: "George" },
{ id: "526", name: "Sam" },
{ id: "827", name: "Dan" }
];
const accumulator = Object.create(null);
data.forEach(o => {
Object.assign(accumulator[o.id] ??= {}, o);
});
console.log(accumulator);
With that in mind, here’s how you would use Array.prototype.reduce:
const data = [
{ id: "428", date: "2017-01-24" },
{ id: "526", date: "2022-01-01" },
{ id: "428", name: "George" },
{ id: "526", name: "Sam" },
{ id: "827", name: "Dan" }
];
const accumulator = data.reduce(
(accumulator, o) => {
Object.assign(accumulator[o.id] ??= {}, o);
return accumulator;
},
Object.create(null)
);
console.log(accumulator);
You can reduce the dataset using Array.prototype.reduce. Just find the previous object by the id key in the accumulator (acc) or use a new object, and spread the current object over it.
const data = [
{ id: "428", date: "2017-01-24" },
{ id: "526", date: "2022-01-01" },
{ id: "428", name: "George" },
{ id: "526", name: "Sam" },
{ id: "827", name: "Dan" }
];
const reduceBy = (data, predicate) =>
data.reduce((acc, obj) =>
(key => ({
...acc,
[key]: {
...(acc[key] ?? {}),
...obj
}
}))
(typeof predicate === 'string'
? obj[predicate]
: predicate(obj)
), {});
const reducedData = reduceBy(data, ({ id }) => id); // or reduceBy(data, 'id')
console.log(reducedData);
.as-console-wrapper { top: 0; max-height: 100% !important; }
Related
I have an two arrays of objects. My goal is to replace an object from the second array into the first one based upon 'id'. I have a working solution, but would like to extend it by adding the object to the first array if a value isnt found. Please advice.
function mergeById(arr) {
return {
with: function(arr2) {
return _.map(arr, item => {
return _.find(arr2, obj => obj.id === item.id) || item
})
}
}
}
var result = mergeById([{
id: '124',
name: 'qqq'
},
{
id: '589',
name: 'www'
},
{
id: '567',
name: 'rrr'
}
])
.with([{
id: '124',
name: 'ttt'
}, {
id: '45',
name: 'yyy'
}])
console.log(result);
/**
[
{
"id": "124",
"name": "ttt"
},
{
"id": "589",
"name": "www"
},
{
"id": "567",
"name": "rrr"
},
{
id: '45',
name: 'yyy'
}
]
**/
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.20/lodash.min.js"></script>
Please advice.
You need to filter the second array and add the values who have no common id.
function mergeById(arr) {
return {
with: function(arr2) {
return [
..._.map(arr, item => _.find(arr2, obj => obj.id === item.id) || item),
..._.filter(arr2, item => !_.some(arr, obj => obj.id === item.id))
];
}
}
}
var result = mergeById([{ id: '124', name: 'qqq' }, { id: '589', name: 'www' }, { id: '567', name: 'rrr' } ])
.with([{ id: '124', name: 'ttt' }, { id: '45', name: 'yyy' }]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.20/lodash.min.js"></script>
A shorter approach with a Map and single loops for every array.
function mergeById(array) {
const
add2map = (m, o) => m.set(o.id, o),
map = array.reduce(add2map, new Map);
return {
with: function(array2) {
return Array.from(array2
.reduce(add2map, map)
.values()
);
}
}
}
var result = mergeById([{ id: '124', name: 'qqq' }, { id: '589', name: 'www' }, { id: '567', name: 'rrr' } ])
.with([{ id: '124', name: 'ttt' }, { id: '45', name: 'yyy' }]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Use _.differenceBy(arr2, arr, 'id') to find all items that appear in arr2 that doesn't have a counterpart in arr by id, and concat them to the results of the _.map() action.
Note: instead using _.find() (O(n)) on each iteration, iterate arr2 once with _.keyBy() (O(n)) to create a dictionary { [id]: item }, and then get the items in O(1).
const mergeById = arr => ({
with(arr2) {
const arr2Dict = _.keyBy(arr2, 'id')
return _.map(arr, item => arr2Dict[item.id] || item)
.concat(_.differenceBy(arr2, arr, 'id'))
}
})
const result = mergeById([{ id: '124', name: 'qqq' }, { id: '589', name: 'www' }, { id: '567', name: 'rrr' } ])
.with([{ id: '124', name: 'ttt' }, { id: '45', name: 'yyy' }])
console.log(result)
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.20/lodash.min.js"></script>
You can replace/add in a single loop by concating both arrays, reducing to a Map, and just adding the items by id to the Map:
const mergeById = arr => ({
with(arr2) {
return Array.from(
[...arr, ...arr2]
.reduce((r, o) => r.set(o.id, o), new Map)
.values()
)
}
})
const result = mergeById([{ id: '124', name: 'qqq' }, { id: '589', name: 'www' }, { id: '567', name: 'rrr' } ])
.with([{ id: '124', name: 'ttt' }, { id: '45', name: 'yyy' }])
console.log(result)
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.20/lodash.min.js"></script>
I have an array of objects like this:
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK'
},
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1236,
name: 'bar',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
]
and I need to filter it, keeping only one with the same name, and it should be the one with the highest id.
const expectedOutput = [
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
]
I've been strugling but I can't find the best solution. Any idea?
Keep track of maxes in an object mapping names to objects:
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK'
},
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1236,
name: 'bar',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
];
const maxes = {};
for (const ele of myArray) {
if (!(ele.name in maxes) || ele.id > maxes[ele.name].id) {
maxes[ele.name] = ele;
}
}
const filtered = Object.values(maxes);
console.log(filtered);
.as-console-wrapper {min-height: 100%;}
You can use reduce like the following. This way it will work for both sorted and unsorted array.
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK'
},
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1236,
name: 'bar',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
];
const ret = myArray.reduce((acc, curr) => {
const index = acc.findIndex(item => item.name === curr.name);
if(index> -1 && acc[index].id < curr.id) {
acc[index] = curr;
} else {
acc.push(curr);
}
return acc;
}, []);
console.log(ret);
Although this will work pretty good as you have to loop through the array only once. But if you use for loop instead of reduce. It will be much faster as for loops are usually faster than map, filter, reduce etc. You can do the following for fastest result,
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK'
},
{
id: 1235,
name: 'foo',
status: 'KO'
},
{
id: 1236,
name: 'bar',
status: 'KO'
},
{
id: 1237,
name: 'bar',
status: 'OK'
},
{
id: 1238,
name: 'baz',
status: 'KO'
}
];
let ret = [];
for(let i =0;i<myArray.length; i++) {
const index = ret.findIndex(item => item.name === myArray[i].name);
if(index > -1 && ret[index].id < myArray[i].id) {
ret[index]=myArray[i];
} else {
ret.push(myArray[i]);
}
}
console.log(ret);
Since the array is already sorted by id you could use a Map object and just set each value using the name as key. Overriding the previous value if present. Note that this only matches the requirements as long as the last element with a certain name has also the highest value.
const myArray = [{id:1234,name:'foo',status:'OK'},{id:1235,name:'foo',status:'KO'},{id:1236,name:'bar',status:'KO'},{id:1237,name:'bar',status:'OK'},{id:1238,name:'baz',status:'KO'}];
const lookup = new Map();
myArray.forEach(item => lookup.set(item.name, item));
const result = Array.from(lookup.values());
console.log(result);
The order of the resulting elements is based on insertion order into the Map object. The first key inserted will be the first element of the resulting array. The second key inserted will be the second element, etc.
You could do it using Map Object.
First, create a new Map Object
Traverse the array using forEach() method.
Put name as a key into a variable named key
Check if key exists by using has(key) method in the Map Object named map
If key does not exist then set it into the Map Object by calling the set(key, value) method. In this solution, key is name and value is object.
If Key exists then get the object using get(key) method, get max id using Math.max() method, then update the object and set it into the Map Object.
const myArray = [
{
id: 1234,
name: 'foo',
status: 'OK',
},
{
id: 1235,
name: 'foo',
status: 'KO',
},
{
id: 1236,
name: 'bar',
status: 'KO',
},
{
id: 1237,
name: 'bar',
status: 'OK',
},
{
id: 1238,
name: 'baz',
status: 'KO',
},
];
const map = new Map();
myArray.forEach((x) => {
const key = x.name;
if (map.has(key))
map.set(key, { ...map.get(key), id: Math.max(map.get(key).id, x.id) });
else map.set(key, { ...x });
});
const ret = [...map.values()];
console.log(ret);
I have the following array of deeply nested objects:
const data = [
{
name: "foo",
children:[
{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [
{
count: 3,
name: "C",
children: [
{
count: 4,
name: "D"
}
]
}
]
}
]
The way I'd like to transform this would be such as:
const expectedStructure = [
{
count: 1,
name: "A",
label: "foo = A"
},
{
count: 2,
name: "B",
label: "foo = B"
},
{
count: 3,
name: "C",
label: "bar = C"
},
{
count: 4,
name: "D",
label: "bar = D"
}
]
I created recursive function that transforms nested array into array of flat objects.
Here's my code:
function getChildren(array, result=[]) {
array.forEach(({children, ...rest}) => {
result.push(rest);
if(children) {
getChildren(children, result);
}
});
return result;
}
And here's output I get:
[ { name: 'foo' },
{ count: 1, name: 'A' },
{ count: 2, name: 'B' },
{ name: 'bar' },
{ count: 3, name: 'C' },
{ count: 4, name: 'D' } ]
The problem is that I need to add label field to every object in my output array, and I can't find a solution without iterating multiple times through the final array to make desired transformation. How to properly insert label field without hugely augmenting complexity of the function?
Check each iteration whether the current item is a "parent" item, and reassign label if it is.
const data = [{name:"foo",children:[{count:1,name:"A"},{count:2,name:"B"}]},{name:"bar",children:[{count:3,name:"C",children:[{count:4,name:"D"}]}]}];
function getChildren(array, result = [], label = "") {
array.forEach(({ children, name, count }) => {
if (!label || name[1]) {
label = `${name} = `;
}
if (count) {
result.push({ count, name, label: label + name });
}
if (children) {
getChildren(children, result, label);
}
});
return result;
}
const res = getChildren(data);
console.log(res);
You can use a different function for the nested levels, so you can pass the top-level name properties down through all those recursion levels.
function getTopChildren(array, result = []) {
array.forEach(({
name,
children
}) => {
if (children) {
getChildren(children, name, result);
}
});
return result;
}
function getChildren(array, name, result) {
array.forEach(({
children,
...rest
}) => {
rest.label = `${name} = ${rest.name}`;
result.push(rest);
if (children) {
getChildren(children, name, result);
}
});
}
const data = [{
name: "foo",
children: [{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [{
count: 3,
name: "C",
children: [{
count: 4,
name: "D"
}]
}]
}
]
console.log(getTopChildren(data));
You can also do this recursively with flatMap based on whether or not a parent has been passed into the recursive call :
const data = [{
name: "foo",
children: [{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [{
count: 3,
name: "C",
children: [{
count: 4,
name: "D"
}]
}]
}
];
function flatten(arr, parent = null) {
return parent
? arr.flatMap(({name, count, children}) => [
{name, count, label: `${parent} = ${name}`},
...flatten(children || [], parent)
])
: arr.flatMap(({name, children}) => flatten(children || [], name));
}
console.log(flatten(data));
Sometimes it's a little easier to reason about the code and write it clearly using generators. You can yield* from the recursive calls:
const data = [{name: "foo",children:[{count: 1,name: "A"},{ count: 2,name: "B"}]},{name: "bar",children: [{count: 3,name: "C",children: [{count: 4,name: "D"}]}]}]
function* flat(input, n){
if (!input) return
if (Array.isArray(input)) {
for (let item of input)
yield* flat(item, n)
}
let _name = n || input.name
if ('count' in input) {
yield { count:input.count, name:input.name, label:`${_name} = ${input.name}`}
}
yield* flat(input.children, _name)
}
let g = [...flat(data)]
console.log(g)
The function returns a generator, so you need to spread it into a list [...flat(data)] if you want a list or iterate over it if you don't need to store the list.
Suppose there are two objects.
const a = [
{ id: '1-1-1', name: 'a111' },
{ id: '1-1-2', name: 'a112' },
{ id: '1-2-1', name: 'a121' },
{ id: '1-2-2', name: 'a122' },
{ id: '2-1-1', name: 'a211' },
{ id: '2-1-2', name: 'a212' }
]
const b = ['1-1', '1-2', '2-1']
and the result
{
'1-1':[
{ id: '1-1-1', name: 'a111' },
{ id: '1-1-2', name: 'a112' },
],
'1-2':[
{ id: '1-2-1', name: 'a121' },
{ id: '1-2-2', name: 'a122' },
],
'2-1':[
{ id: '2-1-1', name: 'a211' },
{ id: '2-1-2', name: 'a212' },
]
}
Basically, I want to group the data.
I use includes to check if the item from b to match the id from a. Then construct the new array.
This is my attempt(fiddle):
return b.map(item => a.map(jtem => {
if(jtem.id.includes(item)){
return {
[item]: jtem
}
}
}))
For somehow, it doesn't work.
and, is there a clever way to avoid the nested for loop or map function?
You can do that in following steps:
Apply reduce() on the array b
During each iteration use filter() on the the array a
Get all the items from a which starts with item of b using String.prototype.startsWith()
At last set it as property of the ac and return ac
const a = [
{ id: '1-1-1', name: 'a111' },
{ id: '1-1-2', name: 'a112' },
{ id: '1-2-1', name: 'a121' },
{ id: '1-2-2', name: 'a122' },
{ id: '2-1-1', name: 'a211' },
{ id: '2-1-2', name: 'a212' }
]
const b = ['1-1', '1-2', '2-1']
let res = b.reduce((ac,b) => {
ac[b] = a.filter(x => x.id.startsWith(b));
return ac;
},{})
console.log(res)
As suggested by #Falco is the comments that It would be better to scan over the a once as its large. So here is that version.Actually its better regarding performance
const a = [
{ id: '1-1-1', name: 'a111' },
{ id: '1-1-2', name: 'a112' },
{ id: '1-2-1', name: 'a121' },
{ id: '1-2-2', name: 'a122' },
{ id: '2-1-1', name: 'a211' },
{ id: '2-1-2', name: 'a212' }
]
const b = ['1-1', '1-2', '2-1']
let res = a.reduce((ac,x) => {
let temp = b.find(y => x.id.startsWith(y))
if(!ac[temp]) ac[temp] = [];
ac[temp].push(x);
return ac;
},{})
console.log(res)
Note: startsWith is not supported by I.E. So you can create polyfill using indexOf
if(!String.prototype.startWith){
String.prototype.startsWith = function(str){
return this.indexOf(str) === 0
}
}
I'm trying to strip the duplicate array values from my current array. And I'd like to store the fresh list (list without duplicates) into a new variable.
var names = ["Daniel","Lucas","Gwen","Henry","Jasper","Lucas","Daniel"];
const uniqueNames = [];
const namesArr = names.filter((val, id) => {
names.indexOf(val) == id; // this just returns true
});
How can I remove the duplicated names and place the non-duplicates into a new variable?
ie: uniqueNames would return...
["Daniel","Lucas","Gwen","Henry","Jasper"]
(I'm using react jsx) Thank you!
You can do it in a one-liner
const uniqueNames = Array.from(new Set(names));
// it will return a collection of unique items
Note that #Wild Widow pointed out one of your mistake - you did not use the return statement. (it sucks when we forget, but it happens!)
I will add to that that you code could be simplified and the callback could be more reusable if you take into account the third argument of the filter(a,b,c) function - where c is the array being traversed. With that said you could refactor your code as follow:
const uniqueNames = names.filter((val, id, array) => {
return array.indexOf(val) == id;
});
Also, you won't even need a return statement if you use es6
const uniqueNames = names.filter((val,id,array) => array.indexOf(val) == id);
If you want to remove duplicate values which contains same "id", You can use this.
const arr = [
{ id: 2, name: "sumit" },
{ id: 1, name: "amit" },
{ id: 3, name: "rahul" },
{ id: 4, name: "jay" },
{ id: 2, name: "ra one" },
{ id: 3, name: "alex" },
{ id: 1, name: "devid" },
{ id: 7, name: "sam" },
];
function getUnique(arr, index) {
const unique = arr
.map(e => e[index])
// store the keys of the unique objects
.map((e, i, final) => final.indexOf(e) === i && i)
// eliminate the dead keys & store unique objects
.filter(e => arr[e]).map(e => arr[e]);
return unique;
}
console.log(getUnique(arr,'id'))
Result :
[
{ id: 2, name: "sumit" },
{ id: 1, name: "amit" },
{ id: 3, name: "rahul" },
{ id: 4, name: "jay" },
{ id: 7, name: "sam" }
]
you forgot to use return statement in the filter call
const namesArr = duplicatesArray.filter(function(elem, pos) {
return duplicatesArray.indexOf(elem) == pos;
});
Since I found the code of #Infaz 's answer used somewhere and it confused me greatly, I thought I would share the refactored function.
function getUnique(array, key) {
if (typeof key !== 'function') {
const property = key;
key = function(item) { return item[property]; };
}
return Array.from(array.reduce(function(map, item) {
const k = key(item);
if (!map.has(k)) map.set(k, item);
return map;
}, new Map()).values());
}
// Example
const items = [
{ id: 2, name: "sumit" },
{ id: 1, name: "amit" },
{ id: 3, name: "rahul" },
{ id: 4, name: "jay" },
{ id: 2, name: "ra one" },
{ id: 3, name: "alex" },
{ id: 1, name: "devid" },
{ id: 7, name: "sam" },
];
console.log(getUnique(items, 'id'));
/*Output:
[
{ id: 2, name: "sumit" },
{ id: 1, name: "amit" },
{ id: 3, name: "rahul" },
{ id: 4, name: "jay" },
{ id: 7, name: "sam" }
]
*/
Also you can do this
{Array.from(new Set(yourArray.map((j) => j.location))).map((location) => (
<option value={`${location}`}>{location}</option>
))}