Zipping two collections with possible null values in Javascript (lodash available) - javascript

I have two arrays of objects. Each collection object contains similar properties. I'm trying to zip together the two collections based on a specific property. It's possible, however, that a particular object in either array may not necessarily have a object with a matching property value in the other array. In these cases I'd like to have null values. Here's an example of what I'd like to do:
var arr1 = [
{ id: '1', text: 'arr1 - one' },
{ id: '2', text: 'arr1 - two' },
{ id: '3', text: 'arr1 - three' }
];
var arr2 = [
{ id: '1', text: 'arr2 - one' },
{ id: '2', text: 'arr2 - two' },
{ id: '4', text: 'arr2 - four' }
];
result:
{
'1': [
{ id: '1', text: 'arr1 - one' },
{ id: '1', text: 'arr2 - one' }
]
'2': [
{ id: '2', text: 'arr1 - two' },
{ id: '2', text: 'arr2 - two' }
],
'3': [
{ id: '3', text: 'arr1 - three' },
null
],
'4': [
null,
{ id: '4', text: 'arr2 - four' }
]
I do already have a lodash 4 dependency in the project, so answers using that library are welcome.

_.chain(arr1)
.concat(arr2)
.map('id')
.uniq() //get all possible ids without dublicates
.reduce(function (result, id) {
result[id] = _.map([arr1, arr2], function (arr) { //find object in each array by id
return _.find(arr, {id: id}) || null; //if not found set null
});
return result;
}, {})
.value();

function zip(a, b, propName) {
const result = new Map();
a.forEach(i=> result.set(i[propName], [i, null]));
b.forEach(i=> {
let item = result.get(i[propName]);
item ? (item[1] = i) : (result.set(i[propName], [null, i]));
});
return result;
}
var arr1 = [
{ id: '1', text: 'arr1 - one' },
{ id: '2', text: 'arr1 - two' },
{ id: '3', text: 'arr1 - three' }
];
var arr2 = [
{ id: '1', text: 'arr2 - one' },
{ id: '2', text: 'arr2 - two' },
{ id: '4', text: 'arr2 - four' }
];
console.log(JSON.stringify([...zip(arr1, arr2, 'id')], null, ' '));

According to the requirements listed, below should work (using Ramda, I believe the functions used should have according one in lodash/fp)
var arr1 = [
{ id: '3', text: 'arr1 - one' },
{ id: '2', text: 'arr1 - two' },
{ id: '1', text: 'arr1 - three' }
];
var arr2 = [
{ id: '4', text: 'arr2 - one' },
{ id: '2', text: 'arr2 - two' },
{ id: '1', text: 'arr2 - four' }
];
var idList = R.pipe(
R.concat,
R.map(R.prop('id')),
R.uniq,
R.sortBy(R.identity)
)(arr1, arr2);
var findById = (id) => R.find(R.propEq('id', id));
return R.map(
(id) => [
findById(id)(arr1),
findById(id)(arr2)
]
)(idList);
Here's the link. It's returning undefined instead of null, but you can map over it to change it if it matters.
However, depending on what the result would be used, if null value is not required at all, there is simpler version with groupWith
return R.pipe(
R.concat,
R.sortBy(R.prop('id')),
R.groupWith(R.eqProps('id'))
)(arr1, arr2);
link.

The thing that makes this a bit difficult is the need for null values in each grouping, which match the order in which they were grouped. For example, if you did not need the nulls, this problem would be as simple as:
_.groupBy(arr1.concat(arr2), 'id')
However, to group and maintain nulls, you need to add a bit of redundancy that groupBy does not come pre-baked with. You can write your own group function as such:
function group(...arrs) { // extensible to any number of arrays
// construct an object with empty arrays for all available ids
let rslt = _.chain(arrs)
.flatten()
.map(el => el.id)
.uniq()
.reduce((memo, el) => {
memo[el] = []
return memo
}, {})
.value()
// as we iterate, first set resulting object's bucket value to null
// Replace this value if it exists while iterating
_.each(arrs, (arr, i) => {
_.each(rslt, v => {
v.push(null)
})
_.each(arr, el => {
rslt[el.id][i] = el
})
})
return rslt
}
var arr1 = [{
id: '3',
text: 'arr1 - one'
}, {
id: '2',
text: 'arr1 - two'
}, {
id: '1',
text: 'arr1 - three'
}];
var arr2 = [{
id: '4',
text: 'arr2 - one'
}, {
id: '2',
text: 'arr2 - two'
}, {
id: '1',
text: 'arr2 - four'
}];
console.log(group(arr1, arr2))
<script src="https://cdn.jsdelivr.net/lodash/4.17.2/lodash.min.js"></script>
^ Run the snippet to see a log of the result.

You can do this with _.groupBy() and _.mapValues():
const zipByKey = (arr1, arr2, zipKey) => {
// group arr2 by the zipKey
const group2 = _.groupBy(arr2, zipKey);
return _(arr1)
// group arr1 by the zipKey
.groupBy(zipKey)
// merge group1 with group2, if object is not in group1 substitute with null
.mergeWith(group2, (objValue, srcValue) => (objValue || [null]).concat(srcValue))
// map the groups, if object is not in group2 substitute with null
.mapValues((group, zipKey) => group2[zipKey] ? group : group.concat(null))
.value();
}
const arr1 = [{"id":"1","text":"arr1 - one"},{"id":"2","text":"arr1 - two"},{"id":"3","text":"arr1 - three"}], arr2 = [{"id":"1","text":"arr2 - one"},{"id":"2","text":"arr2 - two"},{"id":"4","text":"arr2 - four"}];
const result = zipByKey(arr1, arr2, 'id');
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js"></script>

Related

Get the branch of collection without the sibling elements, searching by property

I have the object with the next structure:
let array = [
{
name: 'Name1',
items: [
{
name: 'Name1.1',
items: [
{ id: '1', name: 'Name1.1.1' },
{ id: '2', name: 'Name1.1.2' },
{ id: '3', name: 'Name1.1.3' },
...
],
},
{
name: 'Name1.2',
items: [
{ id: '4', name: 'Name1.2.1' },
{ id: '5', name: 'Name1.2.2' },
],
},
],
},
{
name: 'Name2',
items: [
{
name: 'Name2.1',
items: [
{ id: '6', name: 'Name2.1.1' },
{ id: '7', name: 'Name2.1.2' },
],
},
],
},
];
I want to get the branch without the sibling elements, searching by id. The desired result is the next structure by id = '4':
let array = [
{
name: 'Name1',
items: [
{
name: 'Name1.2',
items: [
{ id: '4', name: 'Name1.2.1' },
],
},
],
}
];
I could find only the end element of the tree ({ id: '4', name: 'Name1.2.1' }). But I don't understand how to get intermediate structures of the tree.
const test = (data, id) => {
if (!data || !data.length) return null;
for (var j = 0; j < data.length; j++) {
var result = data[j].items
? test(data[j].items, id)
: data[j].id
? data[j].id === id
? data[j]
: undefined
: undefined;
if (result !== undefined) {
return result;
}
}
return undefined;
};
test(array, '4');
You should indeed take a recursive approach, but your function currently can only return an id value (a string) or null or undefined. It never returns an array, yet that is what you expect to get.
When a solution is found as a base case, you need to wrap that solution in an array and plain object, each time you get out of the recursion tree.
Here is a working solution:
function getPath(forest, targetid) {
for (let root of forest) {
if (root.id === targetid) return [root]; // base case
let items = root.items && getPath(root.items, targetid);
if (items) return [{ ...root, items }]; // wrap!
}
}
// Example run:
let array = [{name: 'Name1',items: [{name: 'Name1.1',items: [{ id: '1', name: 'Name1.1.1' },{ id: '2', name: 'Name1.1.2' },{ id: '3', name: 'Name1.1.3' },],},{name: 'Name1.2',items: [{ id: '4', name: 'Name1.2.1' },{ id: '5', name: 'Name1.2.2' },],},],},{name: 'Name2',items: [{name: 'Name2.1',items: [{ id: '6', name: 'Name2.1.1' },{ id: '7', name: 'Name2.1.2' },],},],},];
console.log(getPath(array, '4'));

How do I correctly spread 2 arrays overwriting the old values that matches the object id

My goal is to combine the values in both arrays, overwriting the objects that matches all the _id. Is this possible?
oldState = [
{ _id: 1, label: 'a' },
{ _id: 2, label: 'b' },
{ _id: 3, label: 'c' }
]
newState = [
{ _id: 2, label: 'updated b' },
{ _id: 3, label: 'updated c' }
]
Expected Result:
[
{ _id: 1, label: 'a' },
{ _id: 2, label: 'updated b' },
{ _id: 3, label: 'updated c' }
]
Doing this:
[...oldState, ...newState]
Results to this:
[
{ _id: 1, label: 'a' },
{ _id: 2, label: 'b' },
{ _id: 3, label: 'c' },
{ _id: 2, label: 'updated b' },
{ _id: 3, label: 'updated c' }
]
You must note that spread syntax cannot in itself overrider objects with specific key, you need to do that yourself.
You can make use of Array.prototype.reduce and group the object based on _id and overwrite objects with the same id.
var oldState = [
{ _id: 1, label: 'a' },
{ _id: 2, label: 'b' },
{ _id: 3, label: 'c' }
]
var newState = [
{ _id: 2, label: 'updated b' },
{ _id: 3, label: 'updated c' }
]
const res = oldState.concat(newState).reduce((acc, item) => {
acc[item._id] = {...(acc[item._id] || {}), ...item};
return acc;
}, {})
const sol = Object.values(res);
console.log(sol);
I would use Array.reduce() with a Map as the accumulator. The code is a bit cleaner, and the Map also preserves the original order of the items (an object would order them by the numeric value of _id):
const oldState = [{"_id":1,"label":"a"},{"_id":2,"label":"b"},{"_id":3,"label":"c"}]
const newState = [{"_id":2,"label":"updated b"},{"_id":3,"label":"updated c"}]
const result = Array.from(
[...oldState, ...newState]
.reduce((acc, item) => acc.set(item._id, item), new Map())
.values()
)
console.log(result);
I'd reduce all items to an object and then get the object's values:
const oldState = [
{ _id: 1, label: 'a' },
{ _id: 2, label: 'b' },
{ _id: 3, label: 'c' }
];
const newState = [
{ _id: 2, label: 'updated b' },
{ _id: 3, label: 'updated c' }
];
const merge = (oldState, newState) => Object.values(
[...oldState, ...newState].reduce((a, v) => (a[v._id] = v, a), {})
);
console.log(merge(oldState, newState));
You can create a Map from your newState with the keys being the object ids and the values being the objects themselves. Then you can map your oldState and replace any old objects with the new objects in from the Map if its id exists:
const oldState = [{ _id: 1, label: 'a' }, { _id: 2, label: 'b' }, { _id: 3, label: 'c' } ];
const newState = [{ _id: 2, label: 'updated b' }, { _id: 3, label: 'updated c' } ];
const newStateMap = new Map(newState.map(o => [o._id, o]));
const res = oldState.map(o => newStateMap.get(o._id) || o);
console.log(res);
You could build objects and spred it into a new object and ge the values from it.
const
getObject = (array, key) => Object.fromEntries(array.map(o => [o[key], o])),
oldState = [{ _id: 1, label: 'a' }, { _id: 2, label: 'b' }, { _id: 3, label: 'c' }],
newState = [{ _id: 2, label: 'updated b' }, { _id: 3, label: 'updated c' }],
result = Object.values({
...getObject(oldState, '_id'),
...getObject(newState, '_id')
});
console.log(result);
It is possible to create an object which will contain unique items and then use map method to create merged object:
const oldState = [
{ _id: 1, label: 'a' },
{ _id: 2, label: 'b' },
{ _id: 3, label: 'c' }
];
const newState = [
{ _id: 2, label: 'updated b' },
{ _id: 3, label: 'updated c' }
];
const mapping = new Map(newState.map(s => [s._id, s]))
const result = oldState.map((s, i)=> ({ ...mapping.get(s._id) || s}));
console.log(result);

how to sort array object based on another object

it possible to sort and rearrange an array that looks like this:
items:[{
id: '5',
name: 'wa'
},{
id: '3',
name: 'ads'
},{
id: '1',
name: 'fdf'
}]
to match the arrangement of this object:
Item_sequence: {
"5": {index: 1},
"1": { index: 0 }
}
Here is the output I’m looking for:
items:[{
id: '1',
name: 'fdf'
},{
id: '5',
name: 'wa'
},{
id: '3',
name: 'ads'
}]
You could check if the index is supplied and if not take a lage value for sorting by delta of two items.
var data = { items: [{ id: '5', name: 'wa' }, { id: '3', name: 'ads' }, { id: '1', name: 'fdf' }] },
sequence = { 5: { index: 1 }, 1: { index: 0 } };
data.items.sort(({ id: a }, { id: b }) =>
(a in sequence ? sequence[a].index : Number.MAX_VALUE) -
(b in sequence ? sequence[b].index : Number.MAX_VALUE)
);
console.log(data.items);
.as-console-wrapper { max-height: 100% !important; top: 0; }
JavaScript specifically, First you have to apply loop to your array "items":
`
let newArr = [];
items.map(obj=>{
//obj will be the element of items array, here it is an object.
if(Item_sequence.obj[id] !== undefined) {
/*this condition will be satisfied when id from items array will be present as a
key in Item_sequence array*/
insertAt(newArr, Item_sequence.obj[id] , obj)
}
else{
newArr.push(obj);
}
})
//After checking on whole array here you assign a newArr value to items array.
items=newArr;
Hope that it will help you.

Pick unique sub-objects from list of objects and transform them with Ramda

With the following data structure...
const branches = [
{ id: '1', brand: { id: 'A' }},
{ id: '2', brand: { id: 'B' }},
{ id: '3', brand: { id: 'A' }}
]
I want to pick unique brands... that's easy enough.
const pickBrands = RM.pipe(
RM.map(RM.prop('brand')),
RM.uniqBy(RM.prop('id'))
)
But ultimately, I need to transform a whole thing like this...
const brands = [
{ id: 'A', branches: ['1', '3'] },
{ id: 'B', branches: ['2'] },
]
I am kinda confused how could I approach that considering that after the first map, I am losing information about a branch.
Final solution:
https://runkit.com/fredyc/5d2e1bf1df8aec001aff7f64
This might help too:
const group = R.pipe(
R.groupBy(R.path(['brand', 'id'])),
R.values,
R.map(
R.applySpec({
id: R.path([0, 'brand', 'id']),
branches: R.pluck('id'),
}),
),
);
const branches = [
{ id: '1', brand: { id: 'A' }},
{ id: '2', brand: { id: 'B' }},
{ id: '3', brand: { id: 'A' }}
];
console.log('group', group(branches));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
You can use R.groupBy with R.path to group by brand.id, and then use R.toPairs and R.map with R.zipObject to generate the new object from the groups.
Example (annotated by #ScottSauyet):
const { pipe, groupBy, path, map, pluck, toPairs, zipObj } = R
const fn = pipe(
groupBy(path(['brand', 'id'])), //=> {"A": [{brand: {id: "A"}, id: "1"}, {brand: {id: "A"}, id: "3"}], B: [{brand: {id: "B"}, id: "2"}]}
map(pluck('id')), //=> {A: ["1", "3"], B: ["2"]}
toPairs, //=> [["A", ["1", "3"]], ["B", ["2"]]]
map(zipObj(['id', 'brand']) ) //=> [{id: "A", brand: ["1", "3"]}, {id: "B", brand: ["2"]}]
)
const branches = [
{ id: '1', brand: { id: 'A' } },
{ id: '2', brand: { id: 'B' } },
{ id: '3', brand: { id: 'A' } }
]
const result = fn(branches)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
Note: My original solution used R.applySpec({ id: head, branches: last }), but it seems that I'm the only one to find it more readable.
You can use plain old JS to achieve your outcome, use reduce with findIndex to check if the id already exists, if it does, push the id to the existing object, otherwise, push the new object:
const branches = [
{ id: '1', brand: { id: 'A' }},
{ id: '2', brand: { id: 'B' }},
{ id: '3', brand: { id: 'A' }}
]
console.log(branches.reduce((a, {id, brand}) => {
const i = a.findIndex(o => o.id === brand.id)
i + 1 ? a[i].branches.id.push(id) : a.push({id: brand.id, branches: { id: [id] }})
return a
}, []))
You could take a Map for collecting brands.
const
branches = [{ id: '1', brand: { id: 'A' } }, { id: '2', brand: { id: 'B' } }, { id: '3', brand: { id: 'A' } }],
brands = Array.from(
branches.reduce((m, { id: branche, brand: { id } }) =>
m.set(id, [...(m.get(id) || []), branche]), new Map),
([id, branches]) => ({ id, branches: { id: branches }})
);
console.log(brands);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Here's a solution using values and reduceBy.
First let's define an empty template for brands:
const brandTmpl =
{ id: null,
branches: [] };
Then let's define a function that returns the brand id for a given branch:
const brandId = path(['brand', 'id']);
brandId({ id: '1', brand: { id: 'A' }});
//=> 'A'
Then given a brand and a branch, let's define a function that "adds" a branch to a brand:
const brand = (brd, bch) => (
{ id: brandId(bch),
branches: append(bch.id, brd.branches) });
brand(brandTmpl, { id: '1', brand: { id: 'A' }});
//=> {id: 'A', branches: ['1']}
Now let's use all of that to merge branches by brand:
const brands = reduceBy(brand, brandTmpl, brandId);
brands([
{ id: '1', brand: { id: 'A' }},
{ id: '2', brand: { id: 'B' }},
{ id: '3', brand: { id: 'A' }}]);
//=> { A: { id: "A", branches: ["1", "3"]},
//=> B: { id: "B", branches: ["2"]} }
Finally we can simply extract the values:
const branches = [
{ id: '1', brand: { id: 'A' }},
{ id: '2', brand: { id: 'B' }},
{ id: '3', brand: { id: 'A' }} ];
const brandId = path(['brand', 'id']);
const brandTmpl =
{ id: null,
branches: [] };
const brand = (brd, bch) => (
{ id: brandId(bch),
branches: append(bch.id, brd.branches) });
const brands = reduceBy(brand, brandTmpl, brandId);
const pickBrands = compose(values, brands);
console.log(
pickBrands(branches)
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {path, append, reduceBy, compose, values} = R;</script>
try this:
const branches = [
{ id: '1', brand: { id: 'A' }},
{ id: '2', brand: { id: 'B' }},
{ id: '3', brand: { id: 'A' }}
]
const groupBy = (branches) => branches.reduce((acc, ele)=>( (acc[ele.brand.id] = acc[ele.brand.id] || []).push(ele), acc),{})
const reformat = ([k, v]) =>({id: k, branches: {id:v.map(({id})=>id)}})
const result = Object.entries(groupBy(branches)).map(ele => reformat(ele))
console.log(result);

Find object in array ids

I've list of id's in array and list of article in other array.
I would like filter my article array by ids find in id's array.
Exemple :
const ids = [ '1', '2', '3' ];
const articles = [
{ id: '1', title: 'blua' },
{ id: '10', title: 'blua' }
...
];
I've try this :
ids.map((id) => {
return audits.find((audit) => {
return id === audit.id;
});
});
But return underfined :/
I think it's not a good methode ^^
Anyone can help me ?
Thank you !
Use array.prototype.filter and array.prototype.includes:
const ids = [ '1', '2', '3' ];
const articles = [ { id: '1', title: 'blua' },{ id: '10', title: 'blua' } ];
const filtered = articles.filter(a => ids.includes(a.id));
console.log(filtered);
const ids = [ '1', '2', '3' ];
const articles = [
{ id: '1', title: 'blua' },
{ id: '10', title: 'blua' }
...
];
let results = articles.filter( (a) => ids.indexOf(a.id) !== -1);

Categories

Resources