Javascript Array - Remove duplicates based on another property - javascript

What is the most efficient method to dedupe a complex array based on the value of another property? I have found many examples that will dedupe an array or complex array but not this specific use case.
I am trying to find records with unique values in column 3 (state) with the highest number in column 2 (license)
var arrayWithDuplicates = [
["Boat", 1, "NV"],
["Car", 7, "CA"],
["Boat", 3, "NV"],
["Boat", 4, "CA"],
["Car", 5, "OR"],
["Boat", 6, "CA"],
];
Desired outcome
var outputArray = [
["Car", 7, "CA"],
["Boat", 3, "NV"],
["Car", 5, "OR"]
];
This works but not sure if with large datasets
var arrayWithDuplicates = [
["Boat", 1, "NV"],
["Car", 7, "CA"],
["Boat", 3, "NV"],
["Boat", 4, "CA"],
["Car", 5, "OR"],
["Boat", 6, "CA"],
];
let arr= arrayWithDuplicates;
let unique = []
for (let i = 0; i < arr.length; i++) {
let found = false;
for (let j = 0; j < unique.length; j++) {
if (arr[i][2] === unique[j][2]) {
found = true;
if (arr[i][1] > unique[j][1]) {
unique[j] = arr[i];
}
break;
}
}
if (!found) {
unique.push(arr[i])
}
}
console.log(unique);
[["Boat", 3, "NV"], ["Boat", 7, "CA"], ["Car", 5, "OR"]]
You can see the performance of the proposed solutions: https://jsbench.me/eskxxcwnhn/1

One way to solve your problem would be to use a Map instance to hold the most relevant value. Then replace it if you encounter another. Then, when you are done iterating, you take the values that are present in the Map instance.
const arrayWithDuplicates = [
["Boat", 1, "NV"],
["Car", 7, "CA"],
["Boat", 3, "NV"],
["Boat", 4, "CA"],
["Car", 5, "OR"],
["Boat", 6, "CA"],
];
const lookup = new Map();
for (const record of arrayWithDuplicates) {
const key = record[2];
if (!lookup.has(key)) {
lookup.set(key, record);
continue;
}
const other = lookup.get(key);
if (record[1] > other[1]) {
// Iteration order is based on insertion order. By removing the
// current value first, the new value will be placed at the end.
// If you don't care about the order, deletion can be omitted.
lookup.delete(key);
lookup.set(key, record);
}
}
const result = Array.from(lookup.values());
console.log(result);
Note that the following code sequence can be a fairly heavy operation:
lookup.delete(key);
lookup.set(key, record);
Due to the fact that it rearranges the iteration order of the Map contents. This is only done get the result in the order you are looking for. If the order of the resulting items is irrelevant, then you should remove the lookup.delete(key) call to improve the execution speed.
Although using Map instances might produce some execution overhead for small collections. The improved lookup speed really shines when collections get larger.

Using Sort and Reduce
const arrayWithDuplicates = [ ["Boat", 1, "NV"], ["Car", 7, "CA"], ["Boat", 3, "NV"], ["Boat", 4, "CA"], ["Car", 5, "OR"], ["Boat", 6, "CA"], ];
let deduped = [...arrayWithDuplicates]; // take a copy
deduped.sort((a, b) => { // sorts in place
if (a[0] < b[0]) return 1; // sort on names
if (a[0] > b[0]) return -1;
return b[1] - a[1]; // sort on second element
})
// reduce into an object keyed on state
deduped = Object.values( // take only the values from the object
deduped.reduce((acc, cur) => {
const state = cur[2];
if (!acc[state]) acc[state] = cur;
return acc;
},{}))
console.log(deduped)

Related

Why is my JS recursion function returning a list populated with the same entry?

I've been learning Python at school, and I am learning JavaScript on my own time and tackling some JS projects. I can't figure out why my recursion function is only a list with the same entry.
Function description:
The function takes in a list of course Objects, with key-value pairs "courseCode": string and "possibleCombos": list[number]. I want my recursive function to output another list of Objects, with the course Object's "courseCode" value as its keys, and one element of the "possibleCombos" as its value. The returned list will have all the possible permutations of the Objects with course-combo pairs. The function also takes in an Object parameter, for recursion purposes.
Example data:
const dummyObject1 = {
'courseCode': 'BLUE',
'possibleCombos': [1, 2, 3, 4, 5]
}
const dummyObject2 = {
'courseCode': 'RED',
'possibleCombos': [11, 22, 33, 44]
}
const dummyObject3 = {
'courseCode': 'PURPLE',
'possibleCombos': [111, 222, 333, 444, 555, 666]
}
const dummyList = [dummyObject1, dummyObject2, dummyObject3]```
I ideally want:
let dummySchedules = recursionFunction(dummyList, {})
console.log(dummySchedules)
//ideal console output
[
{'BLUE': 1, 'RED': 11, 'PURPLE': 111},
{'BLUE': 1, 'RED': 11, 'PURPLE': 222},
{'BLUE': 1, 'RED': 11, 'PURPLE': 333},
... //and so on.
]
However, the list output I get, is just 120 entries of the same Object.
Here is my code:
function recursiveFunction(listOfCourses, dictSoFar) {
//base case, checks if listOfCourses is empty
if (!listOfCourses.length) {
return [dictSoFar]
} else {
//recursive step
var arraySoFar = [] //accumulator
//iterate through each element of listOfCourses[0]['possibleCombos']
for (let combo of listOfCourses[0]['possibleCombos']) {
//update dictSoFar entry.
dictSoFar[listOfCourses[0]['courseCode']] = combo
//filter out the course we just entered into dictSoFar.
let course = listOfCourses[0]
var cloneListOfCourses = listOfCourses.filter(item => item !== course)
//recursive call, this time with the filtered out list. If we keep following the
//the recursive call down, it should reach the point where listOfCourses is empty,
//triggering the base case. At that point, dictSoFar already has all course: combo
//pairs. This should traverse through all possible course: combo pairs.
var result = recursiveFunction(cloneListOfCourses, dictSoFar)
//update the accumulator
arraySoFar.push(...result)
}
return arraySoFar;
}
}
What is happening? On theory I think the logic makes sense, and I can't tell where its going wrong.
you can do something like this
if you need some explanation fell free to ask
const dummyObject1 = {
'courseCode': 'BLUE',
'possibleCombos': [1, 2, 3, 4, 5]
}
const dummyObject2 = {
'courseCode': 'RED',
'possibleCombos': [11, 22, 33, 44]
}
const dummyObject3 = {
'courseCode': 'PURPLE',
'possibleCombos': [111, 222, 333, 444, 555, 666]
}
const dummyList = [dummyObject1, dummyObject2, dummyObject3]
function recursiveFunction(listOfCourses) {
const loop = (data, acc) => {
if (!data.length) { // if listOfCourses is falsy
return acc
}
const [next, ...rest] = data
if(acc.length === 0){
return loop(rest, next)
}
return loop(rest, next.flatMap(n => acc.flatMap(a => Object.assign({}, a, n))))
}
const courseCombo = listOfCourses.map(({
courseCode,
possibleCombos
}) => possibleCombos.map(c => ({
[courseCode]: c
})))
return loop(courseCombo, [])
}
console.log(recursiveFunction(dummyList))
I came out with a simpler solution that doesn't involve recursion at all
it's divided in two steps:
the first transformation map you dummy object in an array of elements with this form
[{ BLUE : 1}, { BLUE : 2},{ BLUE : 3}, { BLUE : 4}, { BLUE : 5}]
then using reduce it merges all combination of the three arrays together
const dummyObject1 = {
'courseCode': 'BLUE',
'possibleCombos': [1, 2, 3, 4, 5]
}
const dummyObject2 = {
'courseCode': 'RED',
'possibleCombos': [11, 22, 33, 44]
}
const dummyObject3 = {
'courseCode': 'PURPLE',
'possibleCombos': [111, 222, 333, 444, 555, 666]
}
const dummyList = [dummyObject1, dummyObject2, dummyObject3]
const result = dummyList
.map(({courseCode, possibleCombos}) => possibleCombos.map(c => ({[courseCode]: c})))
.reduce((res, item) => res.flatMap(r => item.flatMap(i => Object.assign({}, r, i))))
console.log(result)
What you're looking for is usually called the Cartesian Product of the lists. With a little fiddling, we can turn your inputs into arrays like [{BLUE: 1}, {BLUE: 2}, /*...,*/ {BLUE: 5}], then do a cartesian product of your collection of these to get something like [[{BLUE: 1}, {RED: 11}, {PURPLE: 111}], [{BLUE: 1}, {RED: 11}, {PURPLE: 222}, /...,*/ [{BLUE: 5}, {RED: 44}, {PURPLE: 666}]]. Then we can just call Object.assign on each of these arrays to get your final result.
The code ends up fairly simple.
const cartesian = ([xs, ...xss]) =>
xs == undefined ? [[]] : xs .flatMap (x => cartesian (xss) .map (ys => [x, ...ys]))
const spreadCombos = ({courseCode, possibleCombos}) =>
possibleCombos .map (v => ({[courseCode]: v}))
const combine = (os) =>
cartesian (os .map (spreadCombos)) .map (xs => Object .assign ({}, ... xs))
const dummyObject1 = {courseCode: 'BLUE', possibleCombos: [1, 2, 3, 4, 5]}, dummyObject2 = {courseCode: 'RED', possibleCombos: [11, 22, 33, 44]}, dummyObject3 = {courseCode: 'PURPLE', possibleCombos: [111, 222, 333, 444, 555, 666]}
const dummyList = [dummyObject1, dummyObject2, dummyObject3]
console .log (combine (dummyList))
.as-console-wrapper {max-height: 100% !important; top: 0}
cartesian does the cartesian product of an array of arrays.
spreadCombos does that first transformation from your input into [{BLUE: 1}, {BLUE: 2}, /*...,*/ {BLUE: 5}]
And our main function combine first calls spreadCombos on each input element, calls cartesian, and then for each resulting array, calls Object.assign.
Note that we have to start our Object .assign calls with an empty object. In the intermediate format, the instances of, say, {BLUE: 1} are all references to the same object. If we simply spread our array as the only parameters to Object .assign, then we'd be modifying the same reference each time.
This also helps explain what's wrong with your function. You pass through dictSoFar as a reference to an object, and so continually update that same object. You can fix this by passing a clone of the object in your recursive call. For this purpose, we can make do with the shallow clone {...dictSoFar}, although other circumstances might require a deeper clone. So this patch should fix your approach:
- var result = recursiveFunction(cloneListOfCourses, dictSoFar)
+ var result = recursiveFunction(cloneListOfCourses, {...dictSoFar})

Basic Javascript, How can i filter an array by numeric value?

this is my first question on this community and i'm a novice programmer with JavaScript.
I have something like this:
let dog = [["extra small", 2], ["small", 5], ["medium", 7], ["big", 9], ["extra big", 12]];
Taking the data of the previous array, i want to create a new array just with the numeric values, for example:
ages = [2, 5, 7, 9, 12]
I tried to use "filter", but i don't know how to properly use it, also i tried to search some references to make it work but i couldn't.
Thanks in advance (and sorry for my poor english, but i suppose you get the idea).
You can first use Array#map to get just the numbers and then Array#sort to sort the numbers
let dog = [
["extra small", 2],
["small", 5],
["medium", 7],
["big", 9],
["extra big", 12]
];
let ages = dog.map(([size, age]) => age).sort((a, b) => a - b);
console.log(ages);
Here are my thoughts on how to achieve this.
Using Array#Map and Array#filter.
Basically, mapping each element of the array, then checking for the numeric values in the subArray using JavaScript#isNaN() and
returning the numbers.
isNaN() checks if the value type is not a number. !isNaN() reverses that response.
flat() is used to flatten the final result to a single array. Alternatively, you can change map() to flatMap()
// map(), isNaN(), filter(), flat()
let newArr = dog.map((arr) => arr.filter((val) => !isNaN(val))).flat();
console.log(newArr); // [ 2, 5, 7, 9, 12 ]
// flatMap(), isNaN(), filter()
let newArr = dog.flatMap((arr) => arr.filter((val) => !isNaN(val)));
console.log(newArr); // [ 2, 5, 7, 9, 12 ]
Using function.
This is similar to the first, however, instead of using map() we use a Array#forEach to loop through the array.
function getNumeric(array) {
let result = [];
array.forEach((arr) => {
let res = arr.filter((a) => !isNaN(a));
result.push(...(res));
});
return result;
}
let newArr = getNumeric(dog);
console.log(newArr); // [ 2, 5, 7, 9, 12 ]
let dog = [
["extra small", 2],
["small", 5],
["medium", 7],
["big", 9],
["extra big", 12]
];
const newArr = dog.map(item => {
return item[1]
})
console.log(newArr);

Filter array of objects against another array of objects. Remove items with array values that matches those inside another array of objects

I've used .filter successfully in the past, but I can't figure out this use case.
I want to return a clone of the array chordLibrary (presumably using .filter). But I want to remove any items/objects from this new array where any array value of the property name notesInChord happens to match any of the array values of badNotes.keyIndex.
To clarify, I will compare each item in chordLibrary against every item in badNotes and remove an item from chordLibrary if its array values matches any of the array values in any of the items items in badNotes.
In the following example, you can see that the first item in chordLibrary includes the array value 5, and so that item is removed in the result.
const chordLibrary = [
{ notesInChord: [5, 8], chordName: 'Major' },
{ notesInChord: [4, 8], chordName: 'Minor' },
{ notesInChord: [8], chordName: '5' }
];
const badNotes = [
{"keyIndex":[1],"keyName":"C#"},
{"keyIndex":[3],"keyName":"D#"},
{"keyIndex":[5],"keyName":"E"}
];
// example result: "
const newChordLibrary = [
{ notesInChord: [4, 8], chordName: 'Minor' },
{ notesInChord: [8], chordName: '5' }
];
I assume I need to nest or use a for loop or forEach to do this, but I can't figure it out.
ES6 solutions are ok.
Thanks!
In the filter you can use a custom method that searches in the notesInChord if any of them are found in badNotes using find as follows:
const chordLibrary = [
{ notesInChord: [5, 8], chordName: 'Major' },
{ notesInChord: [4, 8], chordName: 'Minor' },
{ notesInChord: [8], chordName: '5' }
];
const badNotes = [
{"keyIndex":[1],"keyName":"C#"},
{"keyIndex":[3],"keyName":"D#"},
{"keyIndex":[5],"keyName":"E"}
];
function getGoodNotes(chordList){
return chordList.filter((chord)=>{
if(!containsBadNote(chord.notesInChord))
return chord;
});
}
function containsBadNote(notesInChord){
for(let i = 0; i < notesInChord.length; i++){
var note = notesInChord[i];
if(badNotes.find((n)=> n.keyIndex[0]==note)!=null)
return true;
}
return false;
}
console.log( getGoodNotes(chordLibrary) );

Javascript: What is the easiest way to place the same item in an array more than once?

I'm a Javascript beginner (more or less).
I've created a new array:
var genres = [
"metal",
"rockroll",
"funk",
"punk",
"country",
];
However, I'd like to put each genre in the array a specific number of times, not just once. I know I can just repeat each line as many times as I need, but I'm sure there's a better way.
It would be great if I could do something like this:
var genres = [
"metal" * 3,
"rockroll" * 5,
"funk" * 1,
"punk" * 0,
"country" * 4,
];
...but of course I've tried that, and it doesn't work.
Can anyone help me out? I wasn't able to find anything by googling.
Thanks!
You can build an array like this with reduce() if you start with some data structure that holds your counts and categories:
let cats = [[3, "metal"], [5, "rockroll"], [1, "funk"], [0, "punk"], [4, "country"] ]
// etc..
let arr = cats.reduce((arr, [n, cat]) => arr.concat(Array(n).fill(cat)), [])
console.log(arr)
let item = [
{
genres: "metal",
count: 3
},
{
genres: "rockroll",
count: 5
},
{
genres: "funk",
count: 1
},
{
genres: "punk",
count: 0
}
];
console.log(item);
item.map(i => {
for(let n = 0; n < i.count; n++){
console.log(i.genres);
}
});
How do you think about using the Object?
There's no built-in way to do this, but you could easily write a function to do it. For instance:
function addMultiples (input) {
const output = []
for (let key in input) {
for (let i = 0; i < input[key]; i++) {
output.push(key)
}
}
return output
}
Then you would pass in your values as an object:
console.log(addMultiples({
"metal": 3,
"rockroll": 5,
"funk": 1,
"punk": 0,
"country": 4
}).join(", "))
// prints "metal, metal, metal, rockroll, rockroll, rockroll, rockroll, rockroll, funk, country, country, country, country"
You can also use Array.from and keep your sub arrays filled. And only spread them when needed:
let cats = [[3, "metal"], [5, "rockroll"], [1, "funk"], [0, "punk"], [4, "country"]]
const filled = Array.from(cats, ([v,k]) => new Array(v).fill(k)) // fill arrays
console.log(filled.reduce((r,c) => [...r, ...c])) // spread for output

Tournament pairing algorhitm

I need to find ideal pairing amongst tournament players based on following rules:
players with equal points score or similar should be matched
two players can have only one mutual match in tournament
all players must have a match in a round
Its basically a simplified Swiss tournament system.
I have followings standings:
[{
"playerIndex": 0,
"points": 0,
"opponents": [3, 2, 4]
}, {
"playerIndex": 1,
"points": 3,
"opponents": [4, 5, 2]
}, {
"playerIndex": 2,
"points": 3,
"opponents": [5, 0, 1]
}, {
"playerIndex": 3,
"points": 4,
"opponents": [0, 4, 5]
}, {
"playerIndex": 4,
"points": 6,
"opponents": [1, 3, 0]
}, {
"playerIndex": 5,
"points": 2,
"opponents": [2, 1, 3]
}]
Read as: first array item is player number (index) 0 that already played with players number (index) 3, 2 and 4 and gained 0 points, each item for one of six players in a tournament.
And I need to pick three matches for the fourth match. Following a rule that no two players can play a mutual match more than once I choose from following matches:
[ [ 0, 1 ], [ 0, 5 ], [ 1, 3 ], [ 2, 3 ], [ 2, 4 ], [ 4, 5 ] ]
Each of these six possible matches has a points difference between the two players as follows:
[3, 2, 1, 1, 2, 4]
So ideal pairing for the fourth round that gives each player a match in a round with lowest points difference between paired players is:
[ [0, 5], [1, 3], [2, 4] ]
Is there any way of finding these ideal pairings in real time? It is impossible to try all the possible combinations, because there can be more than 100 people in a tournament and the calculations would take forever.
I have been advised to use https://en.wikipedia.org/wiki/Edmonds%27_algorithm or https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm (both available in JS: https://www.npmjs.com/package/edmonds-blossom and https://github.com/sfentress/edmunds-karp). But I am not sure how to read the results.
Can somebody help please?
Edit: Hungarian algorithm fails if there is too many possible solutions. In my case after first round when there is a lot of players with same amount of points.
Edmond Blossoms algorithm performs much better (found this JS implementation available via NPM https://github.com/mattkrick/EdmondsBlossom).
Just had trouble understanding how to use it. The main difference is that you need to feed it with pairs and the points difference between pairs is higher for the pairs that should be preferred. So I use zero difference for pairs that already played before.
My final (hopefully) solution:
var maxDiff = (roundIndex + 1) * this.config.pointsWin
var possiblePairs = []
availablePlayers.forEach(player => {
availablePlayers.forEach(opponent => {
if (
player.playerIndex !== opponent.playerIndex // &&
// player.opponents.indexOf(opponent.playerIndex) === -1
) {
var match = [player.playerIndex, opponent.playerIndex]
match.sort(function(a, b) {
return a - b;
})
if (player.opponents.indexOf(opponent.playerIndex) === -1) {
match.push(maxDiff - Math.abs(player.points - opponent.points))
}
else {
match.push(0)
}
if (this.searchForArray(possiblePairs, match) === -1) {
possiblePairs.push(match)
}
}
})
})
var rawPairing = edmondsBlossom(possiblePairs)
rawPairing.forEach((match, index) => {
if (match !== -1 && match < index) {
round.matches.push({
home: match,
home_score: '',
away: index,
away_score: '',
referee: -1
})
}
})
First I count max possible points difference amongst players (expecting someone could gain zero points and someone else all of them). Then create all possible combinations between players and mark them with MAX POSSIBLE POINTS - PLAYERS POINTS DIFFERENCE or ZERO for players that matched before.
Then feed the array to EdmondsBlossom function that returns array of integers...
[6,8,14,5,15,3,0,10,1,12,7,13,9,11,2,4]
...read as follows: player index 0 should be paired with player 6, player 1 vs 8, player 2 vs 14, player 3 vs 5... player 5 vs 3 (duplicates). Sometimes there is -1 value in the output that is simply skipped.
Here is my solution (deprecated):
Thanks to #VedPrakash's comment I found the Hungarian algorithm that solves my problem. Luckily there is also a JS implementation available on NPM https://github.com/addaleax/munkres-js.
The Munkers function needs a matrix as input. In my case it is players points difference on intersections of my matrix (see below). The pairs that already played each other have higher value that cant be achieved (9 in my case).
Input matrix:
[ [ 9, 4, 9, 9, 9, 3 ],
[ 4, 9, 9, 2, 9, 9 ],
[ 9, 9, 9, 2, 4, 9 ],
[ 9, 2, 2, 9, 9, 9 ],
[ 9, 9, 4, 9, 9, 5 ],
[ 3, 9, 9, 9, 5, 9 ] ]
Output:
[ [ 0, 5 ], [ 1, 3 ], [ 2, 4 ], [ 3, 1 ], [ 4, 2 ], [ 5, 0 ] ]
The last thing to take care of is filter the Munkers output (that contains duplicates - both pairs 0vs1 and 1vs0) so i filter them simply by comparing first and second index.
My implementation:
var maxDiff = (roundIndex + 1) * this.config.pointsWin
// prepare matrix
var matrix = [];
for (var i = 0; i < availablePlayers.length; i++) {
matrix[i] = new Array(availablePlayers.length);
matrix[i].fill(0)
}
// fill matrix with players points diff
for (var y = 0; y < availablePlayers.length; y++) {
var playerY = availablePlayers[y]
for (var x = 0; x < availablePlayers.length; x++) {
var playerX = availablePlayers[x]
if (x === y) {
matrix[x][y] = maxDiff
}
else if (playerY.opponents.indexOf(x) !== -1) {
matrix[x][y] = maxDiff
matrix[y][x] = maxDiff
}
else {
var value = Math.abs(playerX.points - playerY.points)
matrix[x][y] = value
matrix[y][x] = value
}
}
}
// console.table(matrix)
// console.table(computeMunkres(matrix))
// return
// make pairing
var rawPairing = computeMunkres(matrix)
rawPairing.forEach(pairing => {
if (pairing[0] < pairing[1]) {
round.matches.push({
home: pairing[0],
home_score: '',
away: pairing[1],
away_score: '',
referee: -1
})
}
})

Categories