I have two arrays that need merging in Javascript. They are arranged as follows:
arrayA = [town1A, town2A, town3A];
arrayB = [town3B, town5B];
Each town
is an object with a townName: 'town1'
(matching the object variable name). Each town
also has an array of occupants: [{}, {}]
which each have their own personName
, and a status: 'dead' or 'alive'
.
My goal, is that after merging, the new array will contain every unique town according to townName
(town3B
and town3A
both have townName : 'town3'
).
arrayC = [town1, town2, town3, town5]
Any new towns in arrayB
(i.e., town5
) should be added directly to the list. Any towns with the same name (i.e., town3
) should combine their lists of occupants, but remove any "dead" people. ArrayB has priority over ArrayA when determining status
, as it is "overwriting" the old data. For example:
arrayA.town3.occupants = [{name: 'Bob', status: 'alive'}, {name: 'Joe', status: 'alive'}];
arrayB.town3.occupants = [{name: 'Bob', status: 'dead'}, {name: 'Alice', status: 'alive'}];
arrayC.town3.occupants = [{name: 'Joe', status: 'alive'}, {name: 'Alice', status: 'alive'}];
I'm just struggling with the logic sequence process here and need a nudge to figure out what tools to use. Currently I'm trying to work with Lodash's _.merge
and _.union
in some combination. It seems I can use _.mergeWith
or _.unionBy
to "nest" the merging steps without resorting to manually looping over the arrays, but their usage is going over my head. If a solution exists that uses one of those, I would like to see an example to learn better how they work.
Edit: I was asked for the entire contents of an example arrayA and arrayB:
arrayA = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
{name: 'Jim', status: 'dead'}
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'alive'},
{name: 'Joe', status: 'alive'}
]
}
];
arrayB = [
{
townName: 'town3',
occupants: [
{name: 'Bob', status: 'dead'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Sam', status: 'dead'},
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
The output I expect is:
arrayC = [
{
townName: 'town1',
occupants: [
{name: 'Charlie', status: 'alive'},
]
},
{
townName: 'town2',
occupants: [
{name: 'Rachel', status: 'alive'},
]
},
{
townName: 'town3',
occupants: [
{name: 'Joe', status: 'alive'},
{name: 'Alice', status: 'alive'}
]
},
{
townName: 'town5',
occupants: [
{name: 'Ray', status: 'alive'},
{name: 'Bob', status: 'alive'},
]
}
];
CodePudding user response:
You can define a function for merging arrays with a mapper like this:
const union = (a1, a2, id, merge) => {
const dict = _.fromPairs(a1.map((v, p) => [id(v), p]))
return a2.reduce((a1, v) => {
const i = dict[id(v)]
if (i === undefined) return [...a1, v]
return Object.assign([...a1], { [i]: merge(a1[i], v) })
}, a1)
}
and use it like this:
union(
arrayA,
arrayB,
town => town.townName,
(town1, town2) => ({
...town1,
occupants: union(
town1.occupants,
town2.occupants,
occupant => occupant.name,
(occupant1, occupant2) => occupant1.status === 'alive' ? occupant1 : occupant2
).filter(occupant => occupant.status === 'alive')
})
)
CodePudding user response:
The complexity with this problem is that you want to merge on 2 different layers:
- you want to merge two arrays of towns, so you need to decide what to do with towns common to the two arrays;
- when handling two towns with common name, you want to merge their occupants.
Now, both _.merge
and _.mergeWith
are good candidates to accomplish the task, except that they are for operating on objects (or associative maps, if you like), whereas you have vectors of pairs (well, not really pairs, but objects with two elements with fixed keys; name
/status
and townName
/occupants
are fundamentally key
/value
) at both layers mentioned above.
One function that can be useful in this case is one that turns an array of pairs into an object. Here's such a utility:
arrOfPairs2Obj = (k, v) => (arr) => _.zipObject(..._.unzip(_.map(arr, _.over([k, v]))));
Try executing the following
townArr2townMap = arrOfPairs2Obj('townName', 'occupants');
mapA = townArr2townMap(arrayA);
mapB = townArr2townMap(arrayB);
to see what it does.
Now you can merge mapA
and mapB
more easily…
_.mergeWith(mapA, mapB, (a, b) => {
// … well, not that easily
})
Again, a
and b
are arrays of "pairs" name
/status
, so we can reuse the abstraction I showed above, defining
personArr2personMap = arrOfPairs2Obj('name', 'status');
and using it on a
and b
.
But still, there are some problems. I thought that the (a, b) => { … }
I wrote above would be called by _.mergeWith
only for elements which have the same key across mapA
and mapB
, but that doesn't seem to be the case, as you can verify by running this line
_.mergeWith({a: 1, b: 3}, {b:2, c:4, d: 6}, (x, y) => [x, y])
which results in
{
a: 1
b: [3, 2]
c: [undefined, 4]
d: [undefined, 6]
}
revealing that the working lambda is called for the "clashing" keys (in the case above just b
), and also for the keys which are absent in the first object (in the case above c
and d
), but not for those absent in the second object (in the case above a
).
This is a bit unfortunate, because, while you could filter dead people out of towns which are only in arrayB
, and you could also filter out those people which are dead in arrayB
while alive in arrayA
, you'd still have no place to filter dead people out of towns which are only in arrayA
.
But let's see how far we can get. _.merge
doc reads
Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
So we can at least handle the merging of towns common across the array in a more straightforward way. Using _.merge
means that if a person is common in the two arrays, we'll always pick the one from arrayB
, whether that's (still) alive or (just) dead.
Indeed, a strategy like this doesn't give you the precise solution you want, but not even one too far from it,
notSoGoodResult = _.mergeWith(mapA, mapB, (a, b) => {
return _.merge(personArr2personMap(a), personArr2personMap(b));
})
its result being the following
{
town1: [
{name: "Charlie", status: "alive"},
{name: "Jim", status: "dead"}
],
town2: [
{name: "Rachel", status: "alive"}
],
town3:
Alice: "alive"
Bob: "dead"
Joe: "alive"
},
town5: {
Bob: "alive"
Ray: "alive"
Sam: "dead"
}
}
As you can see
Bob
intown3
is correctlydead
,- we've not forgotten
Alice
intown3
, - nor have we forogtten about
Joe
intown3
.
What is left to do is
- "reshaping"
town3
andtown5
to look liketown1
andtown2
(or alternatively doing the opposite), - filtering away all
dead
people (there's no more people appearing with both thedead
andalive
status, so you don't risk zombies).
Now I don't have time to finish up this, but I guess the above should help you in the right direction.
The bottom line, however, in my opinion, is that JavaScript, even with the power of Lodash, is not exactly the best tool for functional programming. _.mergeWith
disappointed me, for the reason explained above.