Merge and compare - javascript

I have written a logic to merge all the Batches from message2 variable. It will merge all the Batch if there is a duplicate Batch name (AA, BB) and calculate the lines.
var message2 = {
Batches: [
{Batch: "AA", Lines: 1 },
{Batch: "BB", Lines: 2 },
{Batch: "BB", Lines: 6 }
]
}
Become:
[ { Batch: 'AA', Lines: 1 }, { Batch: 'BB', Lines: 8 } ]
This is done by reduce() method.
In the forEach loop, it loops all the mergedBatches (after merged) and compares with the batch in the Worker variable. It will need to find the same batch name if the Worker line is more then mergedBatches line then set mergedBatches to match the Worker line.
var message2 = {
Batches: [
{Batch: "AA", Lines: 1 },
{Batch: "BB", Lines: 2 },
{Batch: "BB", Lines: 6 }
]
}
var Worker = {
Batches: [
{Batch: "AA", Lines: 2 },
{Batch: "BB", Lines: 3 },
]
}
var mergedBatches = message2.Batches.reduce((acc, obj)=>{
var existObj = acc.find(b => b.Batch === obj.Batch);
if(existObj) {
existObj.Lines += obj.Lines;
return acc;
}
acc.push({Batch: obj.Batch, Lines: obj.Lines});
return acc;
},[]);
mergedBatches.forEach((b) => {
var workerBatch = Worker.Batches.find(wB => wB.Batch === b.Batch);
if (b.Lines >= workerBatch.Lines) {
b.Lines = workerBatch.Lines;
}
});
console.log(mergedBatches)
Final result which is working as expected:
[ { Batch: 'AA', Lines: 1 }, { Batch: 'BB', Lines: 3 } ]
Is there a way to refactor this code to make it readable or a better way?

Here is a shorter version:
if mergedBatches should not contain references to message2.Batches entries you can use destructuring: acc.push({ ...cur });
one-line if/else should be more readable without the brackets;
null-check in the latest condition: find can return undefined.
const message2 = {
Batches: [
{Batch: "AA", Lines: 1 },
{Batch: "BB", Lines: 2 },
{Batch: "BB", Lines: 6 }
]
}
const Worker = {
Batches: [
{Batch: "AA", Lines: 2 },
{Batch: "BB", Lines: 3 },
]
}
const mergedBatches = message2.Batches.reduce((acc, cur) => {
const prev = acc.find(x => x.Batch === cur.Batch)
if (prev) prev.Lines += cur.Lines
else acc.push(cur)
return acc
}, [])
mergedBatches.forEach((mb) => {
const wb = Worker.Batches.find(x => x.Batch === mb.Batch)
if (wb && wb.Lines < mb.Lines ) mb.Lines = wb.Lines
})
console.log(mergedBatches)

This is a bit more straight forward and should be faster:
const mergeBatches = (message) => {
const obj = {};
for (let i = message.Batches.length; i--;) {
const current = message.Batches[i];
if (current.Batch in obj) {
obj[current.Batch] += current.Lines;
} else {
obj[current.Batch] = current.Lines;
}
}
const arr = [];
for (let key in obj) {
arr.push({
Batch: key,
Lines: obj[key]
})
}
return arr;
}
Its really good that you're learning functional patterns, but they aren't always the fastest.
For example, your code you have acc.find. Underneath the hood, find is iterating over the array acc every time that function executes which makes the complexity O(n * n) I think its this, someone comment if I'm wrong.
In the function I provided, you're only iterating over the Batches array once which makes this O(n).

From your current structure this would be another way to arrive to your expecting result:
const merged = {};
message2.Batches.forEach(b => {
if(merged[b.Batch]) {
merged[b.Batch].Lines += b.Lines;
} else {
merged[b.Batch] = b;
}
});
const result = [];
Worker.Batches.forEach(b => {
if (merged[b.Batch] && merged[b.Batch].Lines > b.Lines) {
merged[b.Batch].Lines = b.Lines;
}
result.push(merged[b.Batch]);
});
// Output
[{ "Batch": "AA", "Lines": 1 }, { "Batch": "BB", "Lines": 3 }]

Related

re-organizing array of objects

So I have an array of objects that contain information for different activities in different projects.
It looks something like this
const input = [
{
Project: 1,
ID: "1-1",
},
{
Project: 1,
ID: "1-2",
},
{
Project: 2,
ID: "2-1",
},
];
From this, I would like to go to this
output = [{
Project: 1,
ID1: 1 - 1,
ID2: 1 - 2,
},
{
Project: 1,
ID1: 2 - 1,
},
];
Here's what I have so far:
let currentProject = ''
let output = []
for (const e of input) {
let counter
let outputObj = {}
if (currentProject !== e.Project) {
output.push(outputObj)
counter = 1
outputObj = {}
outputObj.projectNum = e.Project
currentProject = e.Project
}
if (currentProject == e.Project) {
outputObj['ID' + counter] = e.ID
counter++
}
}
here's what I'm getting back:
output = [{
Project: 1,
ID1: 1 - 1
},
{
Project: 1,
ID1: 2 - 1
}
]
I'm not sure what the issue is, tried several times to fix it.
Could someone please help me get over the edge?
Any help will be greatly appreciated.
You can achieve this using reduce, Object.keys
const input = [{
Project: 1,
ID: "1-1",
},
{
Project: 1,
ID: "1-2",
},
{
Project: 2,
ID: "2-1",
},
];
const result = input.reduce((acc, curr) => {
const { Project, ID } = curr;
const obj = acc.find((el) => el.Project === Project);
if (obj) {
const length = Object.keys(obj).length;
obj[`ID${length}`] = ID;
} else {
acc.push({ Project, [`ID${1}`]: ID });
}
return acc;
}, []);
console.log(result);
You can try this.
const input=[{Project:1,ID:"1-1",},{Project:1,ID:"1-2",},{Project:2,ID:"2-1"}];
let temp = {};
input.map(v=>(temp[v.Project] ??= []).push(v.ID));
let output = Object.keys(temp).map(k=>{
let json = {Project:k};
temp[k].map((v,k)=>json['ID'+(Number(k+1))]=v);
return json;
});
console.log(output);
you will get the result.
[
{ Project: '1', ID1: '1-1', ID2: '1-2' },
{ Project: '2', ID1: '2-1' }
]
The way you intended to implement this assumes every project from same id comes sequentially grouped.
While #decpk answer deals with with a linear search, for performance reasons I would rather first use a dictionary and then convert to an array, AND also keep track of id quantities using a field n.
const input = [
{
Project: 1,
ID: "1-1",
},
{
Project: 1,
ID: "1-2",
},
{
Project: 2,
ID: "2-1",
},
];
const projects = {}
for (const e of input) {
let pid = e.Project
let project = projects[pid]
//First time seeing this project
if (!project) {
projects[pid] = { Project: pid, n: 1, ID1: e.ID }
}
//insert more ID
else {
project.n += 1
project[`ID${project.n}`] = e.ID
}
}
//And now converting the object to array, removing the 'n' field
const output = Object.keys(projects).map(pid => {
const obj = projects[pid]
delete obj.n
obj.Project = pid
return obj
})
You can try this way - O(n) time complexity
Using reduce to aggregate data.
Using logical nullish assignment only assigns if acc[Project] is nullish (null or undefined).
Define each additional key-value pair like:
const number = Object.keys(acc[Project]).length; // Define key format by number of existing property.
const key = `ID${number}`;
instead of using count variable.
const input=[{Project:1,ID:"1-1",},{Project:1,ID:"1-2",},{Project:2,ID:"2-1",}];
const output = input.reduce((acc, {Project, ID}) =>
{
acc[Project] ??= {Project}; // Get exist object or create new one
const number = Object.keys(acc[Project]).length; // Define key format by number of existing property.
const key = `ID${number}`;
acc[Project][key] = ID;
return acc;
}, {});
console.log(Object.values(output));
Output:
[
{
"Project": 1,
"ID1": "1-1",
"ID2": "1-2"
},
{
"Project": 2,
"ID1": "2-1"
}
]

How to stop the loop if the value is not found?

1 - I need to stop the loop and return an error if the 'grid_id' is not found in the variationsDB array
2 - If found, it takes the 'variation_id' and checks if it exists in the variations array
In this second case, I also need to stop the loop and return an error if the 'variation_id' is not found in the variations array
const grids = [{ "grid_id": 1 }, { "grid_id": 2 }]
const variationsDB = [
{
"variation_id": 3,
"grid_id": 1
},
{
"variation_id": 7,
"grid_id": 2
}
]
const variations = [{ "variation_id": 3 }, { "variation_id": 7 }]
const filter = (a, b, key) => {
const array = []
a.forEach((x) => {
b.forEach((y) => {
if (x[key] === y[key]) {
array.push(y)
}
})
})
return array
}
const filterA = filter(grids, variationsDB, 'grid_id')
const filterB = filter(filterA, variations, 'variation_id')
console.log(filterB)
// [{ "variation_id": 3 }, { "variation_id": 7 }]
Just checking if the resulting array is empty or not sounds like it'd be enough:
const filterA = filter(grids, variationsDB, 'grid_id')
if (!filterA.length) {
throw new Error('No matches found for grid_id');
}
const filterB = filter(filterA, variations, 'variation_id')
if (!filterB.length) {
throw new Error('No matches found for variation_id');
}
console.log(filterB)
Your code's computational complexity can be reduced from O(n ^ 2) to O(n) by organizing the first array of items into a Set of values first:
const grids = [{ "grid_id": 1 }, { "grid_id": 2 }]
const variationsDB = [
{
"variation_id": 3,
"grid_id": 1
},
{
"variation_id": 7,
"grid_id": 2
}
]
const variations = [{ "variation_id": 3 }, { "variation_id": 7 }]
const validate = (a, b, key) => {
const aValues = new Set(a.map(aItem => aItem[key]));
for (const bItem of b) {
if (!aValues.has(bItem[key])) {
throw new Error('No match found for', key, bItem[key]);
}
}
}
validate(grids, variationsDB, 'grid_id')
validate(variationsDB, variations, 'variation_id')
console.log(variations)

Filter only unique values from an array of object javascript

I have an array of objects i want to filter only the unique style and is not repeated .
const arrayOfObj = [ {name:'a' , style:'p'} , {name:'b' , style:'q'} , {name:'c' , style:'q'}]
result expected : [ {name:'a' , style:'p'}]
Here is a solution in O(n) time complexity. You can iterate all entries to track how often an entry occurs. And then use the filter() function to filter the ones that occur only once.
const arrayOfObj = [
{ name: "a", style: "p" },
{ name: "b", style: "q" },
{ name: "c", style: "q" },
]
const styleCount = {}
arrayOfObj.forEach((obj) => {
styleCount[obj.style] = (styleCount[obj.style] || 0) + 1
})
const res = arrayOfObj.filter((obj) => styleCount[obj.style] === 1)
console.log(res)
On of the possible solutions depending on your performance / readability needs can be:
arrayOfObj.filter(a => arrayOfObj.filter(obj => obj.style === a.style).length === 1)
Use splice when you find the existing item and remove it
const arrayOfObj = [{
name: 'a',
style: 'p'
}, {
name: 'b',
style: 'q'
}, {
name: 'c',
style: 'q'
}]
const result = arrayOfObj.reduce((acc, x) => {
const index = acc.findIndex(y => y.style === x.style);
if (index >= 0) {
acc.splice(index, 1);
} else {
acc.push(x);
}
return acc;
}, [])
console.log(result)
Here is a solution in O(n) time complexity. You can iterate all entries to track how often an entry occurs. And then use the filter() function to filter the ones that occur only once.
const arrayOfObj = [ {name:'a' , style:'p'} , {name:'b' , style:'q'} , {name:'c' , style:'q'}];
let count = {};
arrayOfObj.forEach(({style}) => {
count[style] = (count[style] || 0) + 1;
});
let result = arrayOfObj.filter(({style}) => count[style] === 1);
console.log(result);
You reduce it. Check if in the array already an element with the same style exists and remove it from the accumulator otherwise push it to the accumulator
const arr = [
{ name: "a", style: "p" },
{ name: "b", style: "q" },
{ name: "c", style: "q" }
];
let result = arr.reduce((a,v) => {
let i = a.findIndex(el => el.style === v.style);
if(i !== -1) {
a.splice(i,1);
return a;
}
a.push(v)
return a;
},[])
console.log(result);
There is a one liner answer too if you are using lodash library
(uniqBy(array, iteratee))
const arr = [
{ name: "a", style: "p" },
{ name: "b", style: "q" },
{ name: "c", style: "q" }
];
let result = _.uniqBy(arrayOfObj,'style')
console.log(result)

Nested grouping with javascript (ES5)

I have an array of objects as following :
[
{"id":1,"lib":"A","categoryID":10,"categoryTitle":"Cat10","moduleID":"2","moduleTitle":"Module 2"},
{"id":2,"lib":"B","categoryID":10,"categoryTitle":"Cat10","moduleID":"2","moduleTitle":"Module 2"},
...
{"id":110,"lib":"XXX","categoryID":90,"categoryTitle":"Cat90","moduleID":"4","moduleTitle":"Module 4"}
]
I want to group this array by (moduleID,moduleTitle) and then by (categoryID,categoryTitle).
This is what I tried :
function groupBy(data, id, text) {
return data.reduce(function (rv, x) {
var el = rv.find(function(r){
return r && r.id === x[id];
});
if (el) {
el.children.push(x);
} else {
rv.push({ id: x[id], text: x[text], children: [x] });
}
return rv;
}, []);
}
var result = groupBy(response, "moduleID", "moduleTitle");
result.forEach(function(el){
el.children = groupBy(el.children, "categoryID", "categoryTitle");
});
The above code is working as expected, but as you can see, after the first grouping I had to iterate again over the array which was grouped by the moduleId in order to group by the categoryId.
How can I modify this code so I can only call groupBy function once on the array ?
Edit:
Sorry this might be late, but I want this done by using ES5, no Shim and no Polyfill too.
Here's one possible (although may be a bit advanced) approach:
class DefaultMap extends Map {
constructor(factory, iter) {
super(iter || []);
this.factory = factory;
}
get(key) {
if (!this.has(key))
this.set(key, this.factory());
return super.get(key);
}
}
Basically, it's the a Map that invokes a factory function when a value is missing. Now, the funny part:
let grouper = new DefaultMap(() => new DefaultMap(Array));
for (let item of yourArray) {
let firstKey = item.whatever;
let secondKey = item.somethingElse;
grouper.get(firstKey).get(secondKey).push(item);
}
For each firstKey this creates a Map inside grouper, and the values of those maps are arrays grouped by the second key.
A more interesting part of your question is that you're using compound keys, which is quite tricky in JS, since it provides (almost) no immutable data structures. Consider:
items = [
{a: 'one', b: 1},
{a: 'one', b: 1},
{a: 'one', b: 2},
{a: 'two', b: 2},
]
let grouper = new DefaultMap(Array);
for (let x of items) {
let key = [x.a, x.b]; // wrong!
grouper.get(key).push(x);
}
So, we're naively grouping objects by a compound key and expecting to see two objects under ['one', 1] in our grouper (which is one level for the sake of the example). Of course, that won't work, because each key is a freshly created array and all of them are different for Map or any other keyed storage.
One possible solution is to create an immutable structure for each key. An obvious choice would be to use Symbol, e.g.
let tuple = (...args) => Symbol.for(JSON.stringify(args))
and then
for (let x of items) {
let key = tuple(x.a, x.b); // works
grouper.get(key).push(x);
}
You could extend your function by using an array for the grouping id/names.
function groupBy(data, groups) {
return data.reduce(function (rv, x) {
groups.reduce(function (level, key) {
var el;
level.some(function (r) {
if (r && r.id === x[key[0]]) {
el = r;
return true;
}
});
if (!el) {
el = { id: x[key[0]], text: x[key[1]], children: [] };
level.push(el);
}
return el.children;
}, rv).push({ id: x.id, text: x.lib });
return rv;
}, []);
}
var response = [{ id: 1, lib: "A", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Workflow" }, { id: 2, lib: "B", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Module 2" }, { id: 110, lib: "XXX", categoryID: 90, categoryTitle: "Cat90", moduleID: "4", moduleTitle: "Module 4" }],
result = groupBy(response, [["moduleID", "moduleTitle"], ["categoryID", "categoryTitle"]]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Version with path as id.
function groupBy(data, groups) {
return data.reduce(function (rv, x) {
var path = [];
var last = groups.reduce(function (level, key, i) {
path.length = i;
path[i] = key[0].slice(0, -2).toUpperCase() + ':' + x[key[0]];
var id = path.join(';'),
el = level.find(function (r) {
return r && r.id === id;
});
if (!el) {
el = { id: path.join(';'), text: x[key[1]], children: [] };
level.push(el);
}
return el.children;
}, rv);
last.push({ id: path.concat('NODE:' + x.id).join(';') });
return rv;
}, []);
}
var response = [{ id: 1, lib: "A", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Workflow" }, { id: 2, lib: "B", categoryID: 10, categoryTitle: "Cat10", moduleID: "2", moduleTitle: "Module 2" }, { id: 110, lib: "XXX", categoryID: 90, categoryTitle: "Cat90", moduleID: "4", moduleTitle: "Module 4" }];
var result = groupBy(response, [["moduleID", "moduleTitle"], ["categoryID", "categoryTitle"]]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You could do it like this:
const exit = Symbol("exit");
function groupBy(arr, ...props){
const root = {};
for(const el of arr){
const obj = props.map(key => el[key])
.reduce((obj, key) => obj[key] || (obj[key] = {}), root);
(obj[exit] || (obj[exit] = [])).push(el);
}
}
So you can access it like:
const grouped = groupBy(response, "moduleID", "moduleTitle");
console.log( grouped[2]["workflow"][exit] );
You might leave away that exit symbol, but it feels a bit wrong to mix a nested tree with arrays.

Compare 2 arrays in javascript and extract differences

Array 1 is the result of the data from a localstorage
Array 2 is, for the same IDs (329, 307, 355), the result after treatment
So i need to compare both to notify what changed
Array 1 :
[{"329":["45738","45737","45736"]},{"307":["45467","45468"]},{"355":["47921"]}]
Array 2 :
[{"355":["47921","45922"]},{"329":["45738","45737","45736"]},{"307":[]}]
I need to compare Array 2 with Array 1 and extract differences.
In this example i want to have for result
[{"355":["45922"]},{"307":[]}]
I try to adapt this code :
var compareJSON = function(obj1, obj2) {
var ret = {};
for(var i in obj2) {
if(!obj1.hasOwnProperty(i) || obj2[i] !== obj1[i]) {
ret[i] = obj2[i];
}
}
return ret;
};
Runnable:
var array1 = [{
"329": ["45738", "45737", "45736"]
}, {
"307": ["45467", "45468"]
}, {
"355": ["47921"]
}],
array2 = [{
"355": ["47921", "45922"]
}, {
"329": ["45738", "45737", "45736"]
}, {
"307": []
}]
var compareJSON = function(obj1, obj2) {
var ret = {};
for (var i in obj2) {
if (!obj1.hasOwnProperty(i) || obj2[i] !== obj1[i]) {
ret[i] = obj2[i];
}
}
return ret;
};
console.log(compareJSON(array1, array2));
But, either I have nothing or I have the whole table
your requirement(result) is not clear, but this will get you started.
var arr1 = [{ "329": ["45738", "45737", "45736"] }, { "307": ["45467", "45468"] }, { "355": ["47921"] }],
arr2 = [{ "355": ["47921", "45922"] }, { "329": ["45738", "45737", "45736"] }, { "307": [] }];
var result = [];
arr2.forEach(obj => {
var key = Object.keys(obj)[0];
var match = arr1.find(o => o.hasOwnProperty(key));
if (match) {
var newObj = {};
newObj[key] = obj[key].filter(s => match[key].indexOf(s) === -1);
if (!obj[key].length || newObj[key].length) result.push(newObj)
} else {
result.push(Object.assign({}, obj));
}
});
console.log(result);
You could use a hash tbale and delete found items. If some items remains, then an empty array is taken to the result object.
var array1 = [{ 329: ["45738", "45737", "45736"] }, { 307: ["45467", "45468"] }, { 355: ["47921"] }],
array2 = [{ 355: ["47921", "45922"] }, { 329: ["45738", "45737", "45736"] }, { 307: [] }],
hash = {},
result = [];
array1.forEach(function (o) {
Object.keys(o).forEach(function (k) {
hash[k] = hash[k] || {};
o[k].forEach(function (a) {
hash[k][a] = true;
});
});
});
array2.forEach(function (o) {
var tempObject = {};
Object.keys(o).forEach(function (k) {
var tempArray = [];
o[k].forEach(function (a) {
if (hash[k][a]) {
delete hash[k][a];
} else {
tempArray.push(a);
}
});
if (tempArray.length || Object.keys(hash[k]).length) {
tempObject[k] = tempArray;
}
});
Object.keys(tempObject).length && result.push(tempObject);
});
console.log(result);
I've used the deep-diff package in npm for this sort of thing before:
It may be more detail than you want though - here's an example from the readme of the output format:
[ { kind: 'E',
path: [ 'name' ],
lhs: 'my object',
rhs: 'updated object' },
{ kind: 'E',
path: [ 'details', 'with', 2 ],
lhs: 'elements',
rhs: 'more' },
{ kind: 'A',
path: [ 'details', 'with' ],
index: 3,
item: { kind: 'N', rhs: 'elements' } },
{ kind: 'A',
path: [ 'details', 'with' ],
index: 4,
item: { kind: 'N', rhs: { than: 'before' } } } ]
Checkout the readme on the github page linked above for details about what it all means, or try it out for yourself online using runkit
But in order for this to work you would have to do some sort of preprocessing:
Sort array based on first key of each element:
a1 = a1.sort((lhs, rhs) => {
return parseInt(Object.keys(lhs)[0]) - parseInt(Object.keys(rhs)[0]);
})
If you sort both of the arrays by the first key of each element and then pass it to the diff tool, you get the following:
[
{"kind":"A","path":[0,"307"],"index":0,"item":{"kind":"D","lhs":"45467"}},
{"kind":"A","path":[0,"307"],"index":1,"item":{"kind":"D","lhs":"45468"}},
{"kind":"A","path":[2,"355"],"index":1,"item":{"kind":"N","rhs":"45922"}}
]
If it were me I would probably merge all the array elements and diff the resulting object so you completely avoid any object order and duplicate key issues.
Alternative: merge array contents into one object
A naive merge might look like this:
a1Object = {}
a1.forEach((element) => {
Object.keys(element).forEach((key) => {
a1Object[key] = element[key];
});
})
Which produces the following diff:
[
{"kind":"A","path":["307"],"index":0,"item":{"kind":"D","lhs":"45467"}},
{"kind":"A","path":["307"],"index":1,"item":{"kind":"D","lhs":"45468"}},
{"kind":"A","path":["355"],"index":1,"item":{"kind":"N","rhs":"45922"}}
]
Interpreting the diff output
there is a change in the Array value of 307 at index 0: 45467 has been Deleted
there is a change in the Array value of 307 at index 1: 45468 has been Deleted
there is a change in the Array value of 355 at index 1: 45467 has been Newly added

Categories