Using array reduce to reduce two properties of an array of objects - javascript

I want to store all duplicates and unique values of suits and values of an array of Card objects. Each card has a suit and value property. The datastructure looks like this:
cards = [
{
suit: 'spades',
value : 4
},
{
suit: 'hearts',
value : 4
},
{
suit: 'spades',
value : 11
},
{
suit: 'spades',
value : 12
}
etc...
]
I'm trying to use array.reduce to check and store duplicates and unique values and suits for both, but am having trouble structuring the code.
Rules:
3-of-a-kind with different suits
4-card-run (with incrementing values) with same suits
Basically... I need to check each card's value and suits... and count the duplicates and uniques of values and suits. I'm struggling passing in an array of objects and using reduce on it.
Output: something like
melds : {
values: [4, 4, 4]
suits: [spades, hearts, diamonds]
},
runs : {
values: [11, 12, 13],
suits: ['spades', 'spades', 'spades']
}
Code:
function calculate(cards) {
var my_array = cards.reduce(function(prev_array, curr, index, array){
if (prev_array.duplicates.values.indexOf(curr) !== -1 || array.lastIndexOf(curr) !== index) {
prev_array.duplicates.values.push(curr);
} else {
prev_array.uniques.values.push(curr);
}
if (prev_array.duplicates.suits.indexOf(curr) !== -1 || array.lastIndexOf(curr) !== index) {
prev_array.uniques.suits.push(curr);
} else {
prev_array.duplicates.suits.push(curr);
}
return prev_array;
},
{
duplicates : {
values : [],
suits : []
},
uniques : {
values : [],
suits : []
}
}
);
return my_array;
}
Edit:
var values = [2, 3, 4, 5, 6, 6, 6];
var suits = ['spades', 'spades', 'spades', 'spades', 'diamonds', 'clubs', 'hearts'];
var test_values = potentialRunsAndMelds(values);
var test_suits = potentialRunsAndMelds(suits);
function potentialRunsAndMelds(array) {
var my_array = array.reduce(function(prev_array, curr, index, array){
if (prev_array.duplicates.indexOf(curr) !== -1 || array.lastIndexOf(curr) !== index) {
prev_array.duplicates.push(curr);
} else {
prev_array.uniques.push(curr);
}
return prev_array;
},
{
uniques : [],
duplicates : []
}
);
return my_array;
}
var values = [2, 3, 4, 5, 6, 6, 6];
var suits = ['spades', 'hearts', 'spades', 'spades', 'diamonds', 'clubs', 'spades'];
EDIT 2:
var runs = Object.keys(groups.suits).map(function (suit) {
var values = groups.suits[suit].sort();
var run = [];
for (var i = 0; i < values.length; i++) {
if (values[i+1] - values[i] === 1) {
if (run.indexOf(values[i+1]) === -1) {
run.push(values[i+1]);
}
if (run.indexOf(values[i]) === -1) {
run.push(values[i]);
}
}
}
if (run.length >= 4) return run;
});
Returns : [Array[4], undefined, undefined, undefined]
Where Array[4] is [2, 3, 4, 5]
How can I not return undefined?
I suppose I can just do:
runs = runs.filter(function (run) {
return run.length;
});

You might be trying to do too much in one reduce function; maybe try breaking this into steps? Anyway, having your reducer group by value and suit would simplify things.
var groups = cards.reduce(function (accumulator, card) {
// Group by value.
accumulator.values[card.value] = accumulator.values[card.value] || [];
accumulator.values[card.value].push(card.suit);
// Group by suit.
accumulator.suits[card.suit] = accumulator.suits[card.suit] || [];
accumulator.suits[card.suit].push(card.value);
return accumulator;
}, {values: {}, suits: {}});
Once you've done that, it's much easier to find melds and runs.
// Melds
var meldValues = Object.keys(groups.values).filter(function (value) {
// Check for duplicates in this array, if so inclined.
return groups.values[value].length >= 3;
});
// Runs
var runs = Object.keys(groups.suits).map(function (suit) {
var values = groups.suits[suit].sort();
// (1) iterate over values
// (2) append each value to current 'run' as long as it's consecutive
// (3) if not consecutive, start a new run. if duplicate, discard.
// (4) return all runs with length >= 4
});

OK, I don't gamble and maybe I quite don't understood what you want to achieve, but it seem to me, that you forgot to get property value from cards array.
You are passing the curr
if (prev_array.duplicates.values.indexOf(curr) !== -1 ...
But curr is card object.
cards = [
{
suit: 'spades',
value : 4
},
You should target suit and value like curr.suit and cur.value.
And because in JS, Objects cannot be easily compared so lastIndexOf(object) === indexOf(object) equals in all cases. and you need to check uniques Array if the value is'nt already there, because you cannot rely on lastIndexOf().
Also, because Objects cannot be easily compared, testing duplicity with Object is bad idea.

Related

Grouping items with more that one indexes into sub arrays

Trying to create a function that groups repeated items in an array into sub arrays, and also grouping strings (should there be any) into another subarray.
I tried using the findIndex method to define i and then iterate it and push in into an [], using reduce
let roughArray = [1, 2, 4, 591, 392, 391, 2, 5, 10, 2, 1, 1, 1, 20, 20];
function sortArray() {
roughArray.map(num => {
if (num[i] > 1) {
roughArray.reduce((acc, num) => {
return acc.concat(num)
}, [])
}
})
sortArray()
I also tried:
const cleanArray = roughArray.reduce((acc, num) => {
let i = acc.findIndex(num);
if (i) {
return acc.concat(num);
}
}, [])
cleanArray();
I expect this in case of only numbers
[[1,1,1,1],[2,2,2], 4,5,10,[20,20], 391, 392,591]
And this in case of some included strings:
[[1,2], ["2", "3"]]
let roughArray = [1, 2, 4, 591, 392, 391, 2, 5, 10, 2, 1, 1, 1, 20, 20];
let results = {}
roughArray.map( num => {
if(results[num])
results[num].push(num)
else
results[num] = [num]
})
let bigArray = []
bigArray = Object.values(results)
let final_result = []
bigArray.map(array => {
if(array.length == 1)
final_result.push(array[0])
else
final_result.push(array)
})
console.log(final_result)
You could declare some callbacks for the various types of grouping and get the wanted type by checking the array and take an object for the grouped values.
function sortOf(array) {
const
mixed = (r, v) => {
var key = typeof v;
r[key] = r[key] || [];
r[key].push(v);
return r;
},
numbers = (r, v) => {
if (v in r) r[v] = [].concat(r[v], v);
else r[v] = v;
return r;
};
return Object.values(array.reduce(array.some(v => typeof v === 'string')
? mixed
: numbers,
Object.create(null)
));
}
console.log(sortOf([1, '1', '2', 3]));
console.log(sortOf([5, 2, 3, 3, 4, 5, 5, 1]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
First separate the strings from the rest. Sort the numbers, group them, then add the strings back in at the end.
If you want to, you can then map all the single item arrays into just single items, but that seems like it would make the output confusing.
let start = [1, 2, 4, 591, 392, 391, 2, 5, 10, 2, 1, 1, 1, 20, 20, '2', '3'];
let strings = start.filter(v => typeof(v) === 'string');
let notStrings = start.filter(v => typeof(v) !== 'string');
let sortedNotStrings = notStrings.sort((a,b) => a > b);
let grouped = sortedNotStrings.reduce((acc, value) =>
{
if(acc.length > 0)
{
if(acc[0][0] === value)
{
acc[0].push(value);
}
else
{
acc.unshift([value]);
}
}
else
{
acc.unshift([value]);
}
return acc;
}, []);
let sortedGrouped = grouped.sort((g1, g2) => g1[0] > g2[0]);
let withStrings = [sortedGrouped, strings];
console.log(withStrings);
let lonelySingleItems = sortedGrouped.map(arr => arr.length > 1 ? arr : arr[0]);
console.log([lonelySingleItems, strings]);
Regarding the if statement:
if(acc.length > 0)
{
if(acc[0][0] === value)
{
acc[0].push(value);
}
else
{
acc.unshift([value]);
}
}
else
{
acc.unshift([value]);
}
What I'm doing with the reduce function is passing in a default value [], so if we're at the start (i.e. the result is empty) then we put the first item in the sortedNotStrings array into the accumulating result (acc). This is what is happening in the outermost else.
If this isn't the beginning (i.e. acc is not empty) then we need to check if the value is the same as the last value added. If it is the same, put it into the array, otherwise start a new array in acc.
acc is an array of arrays, which is why [value] is being unshifted to start, rather than value.
In order to not have to access the last array of acc, I'm using unshift to put things on the front of the array. This is just to make the code look cleaner, by not using of acc[acc.length-1]. On the other hand you can do acc[acc.length-1].push([value]), and that means the grouped.sort is unnecessary, because the values won't be back to front.
If you have a really large array, eliminating the second sort is probably preferable to not having to type acc.length - 1.
Here I use an object literal {} as the accumulator for Array.prototype.reduce, in case of strings in the array I used str as the key of the object {} accumulator and added the strings as the value. So if a string is encountered the accumulator will be {str: "23"}.
In case of numbers I checked if the value is repeated or not, if repeated I created an array and added the new duplicate number to it with the key being the number itself e.g. {1: [1,1]}
Finally when the accumulator object is constructed I just take the values part of the accumulator object using Object.values which I return:
let roughArray = [1, 2, 4, 591, 392, 391, 2, 5, 10, 2, 1, 1, 1, 20, 20, "23", "23", "34"];
function group(roughArray) {
return Object.values(roughArray.reduce((r, e) => {
if(r['str'] && typeof e === "string"){ r['str'] = Array.isArray(r['str']) ? [...r['str']].concat(e): [r['str'], e]; }
else if(typeof e === "string"){r['str'] = [e]}
else if(r[e]){r[e] = Array.isArray(r[e]) ? [...r[e]].concat(e): [r[e], e];}
else{ r[e] = e}
return r;
}, {}));
}
console.log(group(roughArray));
Note: Array.isArray(r['str']) checks whether the value of the str key is an array if so I can use the es6 spread operator ... to get the old values of the array and also append the new one to the existing array.

Custom implementation of comparing two non ordered array of object in Javascript

I want to compare two array of objects, they should be considered equal if their elements are same but not in same order. e.g. [{a:1}, {b:2}] and [{b:2}, {a:1}]
I am using lodash-v3's isEqual which can compare two values but it only gives true if the order in array is same, so I've implement one function that compares elements recursively.
function deepEqual(data1, data2) {
data1 = _.cloneDeep(data1);
data2 = _.cloneDeep(data2);
function isDeepEqual(val1, val2) {
if (_.isArray(val1) && _.isArray(val2)) {
if (val1.length === val2.length) {
for (let id1 in val1) {
let hasMatch = false;
let matchOnIndex = -1;
for (let id2 in val2) {
if (isDeepEqual(val1[id1], val2[id2])) {
hasMatch = true;
matchOnIndex = id2;
break;
}
}
if (hasMatch) {
val2.splice(matchOnIndex, 1);
} else {
return false;
}
}
return true;
} else {
return false;
}
}
if (_.isPlainObject(val1) && _.isPlainObject(val2)) {
if (Object.keys(val1).length === Object.keys(val2).length) {
for (let temp1 in val1) {
if (!isDeepEqual(val1[temp1], val2[temp1])) {
return false;
}
}
return true;
} else {
return false;
}
}
return _.isEqual(val1, val2);
}
return isDeepEqual(data1, data2);
}
Above function works, but how can i improve it performance wise?
If there is any simple implementation with lodash3 that works for me as well.
Link to above function's fiddle.
EDIT:
The two array of objects can be nested,
e.g.
[{
a:1,
b:[{
c: [1, 2]
},
{
d: [3, 4]
}]
},{
e:1,
f:[{
g: [5, 6]
},
{
h: [7, 8]
}]
}]
and
[{
e:1,
f:[{
h: [8, 7]
},{
g: [6, 5]
}]
},{
a:1,
b:[{
d: [4, 3]
},{
c: [2, 1]
}]
}]
Arrays can also not have unique values(as users are creating this arrays).
This might be possible with _.isEqualWith as #Koushik and #tokland suggested. Unfortunately it's available from lodashv4 so I can't use it.
Similar solution is also mentioned in this comment.
Really sorry for not clearly specifying the examples. The fiddle has all different type of cases.
i think this is what you want
var first = [{ a: 1 }, { b: 2 }];
var second = [{ b: 2 }, { a: 1 }];
function comparer(otherArray){
return function(current){
return otherArray.filter(function(other){
return other.a == current.a && other.b == current.b
}).length == 0;
}
}
var onlyInA = first.filter(comparer(second));
var onlyInB = second.filter(comparer(first));
result = (onlyInA.concat(onlyInB)).length===0;
console.log(result);
how about simply check each Object of one array presents in another array and having same length (both array)?
let isEqualArray = (arr1, arr2) => (
arr1 === arr2 ||
arr1.length === arr2.length &&
!arr1.some(a=> !arr2.find(b=>_.isEqual(a,b)))
)
let a1 = [{a:1}, {b:2}],
a2 = [{b:2}, {a:1}];
let isEqualArray = (arr1, arr2) => (
arr1 === arr2 ||
arr1.length === arr2.length &&
!arr1.some(a=> !arr2.find(b=>_.isEqual(a,b)))
)
console.log(isEqualArray(a1,a2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
Usage of _.isEqual or _.isEqualWith with a customizer depending on how you want to compare two object!
Note: this will not work 100% when you have duplicate items in your array, but it will atleast confirm that all the items of one array is present on another array. You can do a reverse check, or apply unique to perform this compare for duplicate arrays.
Function _.isEqualWith should be the starting point. Now, there are many ways you can implement it. You could make it pretty efficient by defining an ordering for the items in the arrays. An example using id as a key:
function isEqualConsideringArraysAsSets(obj1, obj2, key) {
const customizer = (objValue, othValue) => {
if (_(objValue).isArray() && _(othValue).isArray()) {
return _.size(objValue) === _.size(othValue) &&
_.zip(_.sortBy(objValue, key), _.sortBy(othValue, key))
.every(([val1, val2]) => isEqualConsideringArraysAsSets(val1, val2));
}
};
return _.isEqualWith(obj1, obj2, customizer);
}
console.log(isEqualConsideringArraysAsSets([{id: 1}, {id: 2}], [{id: 2}, {id: 1}], "id"))

I want to order items returned from an object based on the property key value and then assign a ranking to each item in the interface (1,2,3...etc)

Newb here with a basic question. Here is my object:
{TestOne: 12, TestTwo: 6, TestThree: 4, TestFour: 2}
I've looped through it using a for-in loop and grabbed the properties and displayed it in my UI like so:
TestOne: 12
TestTwo: 6
TestThree: 4
TestFour: 2
I need to be able to display this by giving each item a numerical ranking (1,2,3,4...etc) and then displaying them by that ranking (corresponding to their actual order). In other words, what my users need to see on the screen is:
TestOne: 4
TestTwo: 3
TestThree: 2
TestFour: 1
Not 12,6,4,2, etc. This is all new to me but I've been trying to figure out the best way to implement this and have not found anything that I understand to this point.
Here is my code. I feel like this should be easy but it is super frustrating!
var rank = "";
var title = objArrayTwo[i].Title;
var summary ={};
summary = groupBy(objArrayTwo);
for (var prop in summary) {
if (summary.hasOwnProperty(prop)) {
if(title == `${prop}`){
rank = `${summary[prop]}`;
}
}
}
function groupBy(items){
var result= {};
var sum;
$.each(items, function(index, item) {
sum = result[item.RequestName] || 0;
result[item.RequestName] = sum + parseInt(item.Rank);
});
return result;
}
var obj = {TestOne: 12, TestTwo: 6, TestThree: 4, TestFour: 2};
// sort the array
var arr = Object.entries(obj);
arr.sort((a, b) => a[1] - b[1]);
// enumerate the array the index would be the rank
var arr_with_rank = arr.map((data, index) => [data[0], index+1]).reverse()
arr_with_rank.forEach(x => console.log(x[0] + ": " + x[1]));
ES5 solution
var resultDataObject = {
TestOne: 12,
TestTwo: 6,
TestThree: 4,
TestFour: 2
};
var descendingSort = function(a, b) {
return resultDataObject[b] - resultDataObject[a]
};
var sortedResultKeys = Object.keys(resultDataObject).sort(descendingSort);
var resultWithRank = sortedResultKeys.map(function(k, i) {
return {
title: k,
score: resultDataObject[k],
rank: i + 1
};
})
console.log(resultWithRank)

Iterating JS array of objects and returning matched objects as well removing it from the original array?

I am trying to find a better way in iterating over a array of objects in Javascript where i also want to do two more functionalities
Removing the matched element from the original array
As well returning it.
For Example,
var originalArray = [{id:1},{id:2},{id:3},{id:4},{id:5}];
var matchedArray = originalArray.filter(function(n, i, arr) {
return (n.id == 1 || n.id == 2 || n.id == 5) && arr.splice(i, 1)
});
The output im looking for
original array as : [{id:3},{id:4}]
matched array as : [{id:1},{id:2},{id:5}]
Here's a generic function partition: given an array and a predicate (=boolean) function, it splits the array into "falsy" and "truthy" parts:
function partition (ary, predicate) {
var rs = [[], []];
ary.forEach(function(x) {
rs[predicate(x) ? 1 : 0].push(x);
});
return rs;
};
///
var originalArray = [{id:1},{id:2},{id:3},{id:4},{id:5}];
var parts = partition(originalArray, function(n) {
return n.id == 1 || n.id == 2 || n.id == 5
});
console.log(JSON.stringify(parts[0]));
console.log(JSON.stringify(parts[1]))
If you are removed an element then on the next iteration the actual element index and the index in the callback would be different and this result unexpected output.
Use a while loop to iterate in reverse order and generate the new array and remove the value from the main array. Although you can avoid the multiple conditions by using an array and checking value present in the array using Array#includes method(for older browser support use Array#indexOf method).
var matchedArray = [],
i = originalArray.length;
while (i--) {
if ([1, 2, 5].includes(originalArray[i].id))
matchedArray.push(originalArray.splice(i, 1)[0])
}
var originalArray = [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}];
var matchedArray = [],
i = originalArray.length;
while (i--) {
if ([1, 2, 5].includes(originalArray[i].id))
matchedArray.push(originalArray.splice(i, 1)[0])
}
console.log(matchedArray);
console.log(originalArray);
UPDATE 1: An alternate method using Array#reduceRight method with the same logic.
var matchedArray = originalArray.reduceRight(function(arr, o, i, orig) {
if ([1, 2, 5].includes(o.id))
arr.push(orig.splice(i, 1)[0])
return arr;
}, [])
var originalArray = [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}];
var matchedArray = originalArray.reduceRight(function(arr, o, i, orig) {
if ([1, 2, 5].includes(o.id))
arr.push(orig.splice(i, 1)[0])
return arr;
}, [])
console.log(matchedArray);
console.log(originalArray);
UPDATE 2: Although you can do it with the Array#filter method with an additional variable holds the count of removed elements which helps to calculate the new index on updated array.
var c = 0;
var matchedArray = originalArray.filter(function(n, i, arr) {
return [1, 2, 5].includes(n.id) && arr.splice(i - c--, 1)
});
var originalArray = [{
id: 1
}, {
id: 2
}, {
id: 3
}, {
id: 4
}, {
id: 5
}];
var c = 0;
var matchedArray = originalArray.filter(function(n, i, arr) {
return [1, 2, 5].includes(n.id) && arr.splice(i - c--, 1)
});
console.log(matchedArray);
console.log(originalArray);
One of the possible solutions. You can adjust the ids array and specify which id's you want to filter.
var originalArray = [{id:1},{id:2},{id:3},{id:4},{id:5}];
var ids = [1,2,5];
var filtered = [];
var notFiltered = [];
filtered = originalArray.filter(v => ids.some(c => c == v.id));
notFiltered = originalArray.filter(v => filtered.every(c => c.id != v.id));
console.log(filtered);
console.log(notFiltered);
You can use _.remove function from Lodash which solves this elegantly.
let originalArray = [{id:1},{id:2},{id:3},{id:4},{id:5}];
const matchedArray = _.remove(originalArray, function(n) {
return (n.id == 1 || n.id == 2 || n.id == 5);
});
console.log(originalArray);
console.log(matchedArray);
<script src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js"></script>
Using underscoreJs for example:
var originalArray = [{id:1},{id:2},{id:3},{id:4},{id:5}];
var matchedArray = _.filter(originalArray,function(n){return (n.id == 1 || n.id == 2 || n.id == 5);});
originalArray = _.difference(originalArray,matchedArray);

How can I change the index order of an array?

I have a button that has a function called clickNext(). Whenever that button is clicked, it increments the index position (scope.selected) on an array called 'arr1'.
<button type="button" class="right-btn col-xs-6" role="menuitem" ng-click="clickNext()">Next</button>
.
function clickNext()
{
scope.selected = (scope.selected + 1) % length;
}
arr1 = [
{apple: 1 , tango},
{banana: 3, kappa},
{orange:5, alpha},
{apple: 8 , beta},
{grape: 10 , sigma}
]
Problem
I have an identical array to arr1 called 'arr2'. What I'm trying to do is have the clickNext() increment to the next index position based on the arr2 array instead of the arr1 array.
Right now, the clickNext function still increments in the order of the arr1 array. For example, if I were to click the button, it would start on orange:5 then move to apple 8, etc.
arr2 = [
{orange:5, alpha},
{apple: 8 , beta},
{banana: 3, kappa},
{grape: 10 , sigma},
{apple: 1 , tango}
]
What I have tried
My though process to accomplish this is to use the findIndex() function and match the arr2 item to the arr1 item. That doesn't work, but maybe I'm structuring it wrong?
clickNext(){
var oldIndex = filteredToXs(scope.selected);
scope.selected = oldIndex + 1;}
function filteredToXs( filteredIndex ) {
var firstArr = scope.arr1[ filteredIndex ];
var xsIndex = scope.arr2.findIndex( function(item) {
return item.trackingNumber === firstArr.trackingNumber;
} );
if( xsIndex >= 0 ) return xsIndex;
if( xsIndex === -1 ) return 0; // Default value
}
I hope I understood your question correctly. Please read my comments in the code sections as well.
I had to modify your source so I was able to create a fiddle for you.
HTML: I changed the click event and removed a css class that's not available
<button type="button" role="menuitem" onclick="clickNext();">Next</button>
Sampe Arrays:
They were containing invalid objects: I changed alpha, beta, tango, .. to a property. You can also define them as values.. this shouldn't matter:
var arr1 = [
{ apple: 1, tango: '' },
{ banana: 3, kappa: '' },
{ orange: 5, alpha: '' },
{ apple: 8, beta: '' },
{ grape: 10, sigma: '' }];
var arr2 = [
{ orange: 5, alpha: '' },
{ apple: 8, beta: '' },
{ banana: 3, kappa: '' },
{ grape: 10, sigma: '' },
{ apple: 1, tango: '' }];
Code:
var idx = 0; //save current index of array 2
function clickNext() {
idx++;
//I'm comparing the array objects using a string compare- this only works because you said 'I have an identical array'
//this may cause issues if you're objects are cross-referenced
var find = function(array, obj) { //lookup value from arr2 in arr1
for (var i=0, l=array.length;i<l;i++)
if (JSON.stringify(array[i]) == JSON.stringify(obj)) //adjust this line to your needs
return array[i];
}
var result = find(arr1, arr2[idx])
if (!result)
throw new Error('not found- your arrays are not identical or you run out of index');
console.log(result);
}
fiddle: https://jsfiddle.net/k50y8pp5/

Categories

Resources