Javascript Array Sorting Headache - javascript

I have an array of objects that looks similar to this:
[
{
id : 1,
position: false
},
{
id: 2,
position: false
},
{
id: 3,
position: 4
},
{
id: 4,
position: false
},
{
id: 5,
position: 2
},
{
id: 6,
position: 5
},
{
id: 7,
position: 3
},
{
id: 8,
position: 1
},
]
I would like to sort this array using the position property (ascending) of each object so that the array looks like this:
[
{
id: 8,
position: 1
},
{
id: 5,
position: 2
},
{
id: 7,
position: 3
},
{
id: 3,
position: 4
},
{
id: 6,
position: 5
},
{
id: 1,
position: false
},
{
id: 2,
position: false
},
{
id: 4,
position: false
}
]
The idea is only to move the objects with a position that isnt false to the top without affecting the order of the rest of the objects with a position set to false.
Seems easy enough, right? I've tried to approach this problem in so many ways but nothing seems to work reliably. Hopefully someone can help me get my head around how to properly go about this.
Help me Stack Overflow, you're my only hope.

I tried to add a bunch of comments to help you follow my ideas.
This is implemented in an imperative style, and (I hope) can help with illustrating some ideas for how to approach similar problems.
I used your sample data set, and reproduced the result you were looking for.
I hope this is helpful and that it can be modified to suit whatever data you require.
I also pasted the text here.
var deepSort = function(arr){
var i;
var hasPositionKey = [];
var noPositionKey = [];
var nestedObj;
var positions = [];
var loggingPositions = {};
var positionSortedObjects = [];
// iterate through the array of objects
for(i = 0; i < arr.length; i++){
// set a variable for the individual object being considered
nestedObj = arr[i];
// if that object has a position of false
if(nestedObj.position === false){
// push false object into the array of false position objects
noPositionKey.push(nestedObj);
}
// if the object has a numbered position
if(typeof nestedObj.position === 'number'){
// push numbered position to the array of numbered position objects
hasPositionKey.push(nestedObj);
}
}
// iterate through the array of objects with positions
for(i = 0; i < hasPositionKey.length; i++){
// set a variable for the individual object being considered
nestedObj = hasPositionKey[i];
// push only the position number into an array to track positions
positions.push(nestedObj.position);
// add a key value pair of position:id to an object
loggingPositions[nestedObj.position] = nestedObj.id;
}
// sort the array that stores the positions of the non-false objects
positions.sort();
// iterate through that array and construct a final array of objects by sorted position
for(i = 0; i < positions.length; i++){
// set a variable for the positions, as encountered in order:
var num = positions[i];
// set a key for the object to be constructed
var key = loggingPositions[num];
// set a value for the object to be constructed
var value = num;
// construct a result object with object literal notation:
var result = {
id: key,
position: value
};
// push the constructed objects into the positionSortedObjects array
positionSortedObjects.push( result );
}
// return a concatenated array of the positionSortedObjects + the noPositionKey objects
return (positionSortedObjects.concat(noPositionKey));
};
var sample = [
{
id : 1,
position: false
},
{
id: 2,
position: false
},
{
id: 3,
position: 4
},
{
id: 4,
position: false
},
{
id: 5,
position: 2
},
{
id: 6,
position: 5
},
{
id: 7,
position: 3
},
{
id: 8,
position: 1
}
];
deepSort(sample);

The easy way to sort your data would be to use the use the Array.sort() function. First check if the position of object 2 is false, if so return -1 to float object 1 to top. Do the opposite for object 1. If both are not false, do an integer compare.
var data = [
{ id: 1, position: false },
{ id: 2, position: false },
{ id: 3, position: 4 },
{ id: 4, position: false },
{ id: 5, position: 2 },
{ id: 6, position: 5 },
{ id: 7, position: 3 },
{ id: 8, position: 1 }
];
data.sort(function (a, b) {
posa = a.position;
posb = b.position;
if (!posb) return -1;
if (!posa) return 1;
return posa - posb;
});
console.log(data);
But this solution may not be the best because Array.sort() does not require a standard according to ECMA International:
Excerpt below taken from ECMAScript Language Specification, 2011, p.129
15.4.4.11 Array.prototype.sort (comparefn)
The elements of this array are sorted. The sort is not necessarily stable (that is, elements that compare equal do not necessarily remain in their original order). If comparefn is not undefined, it should be a function that accepts two arguments x and y and returns a negative value if x < y, zero if x = y, or a positive value if x > y.
A work around for this would be for you to devise your own sorting algorithm. Preferably a merge or quick-sort algorithm. Below is an implementation of the quick-sort algorithm used to sort the data as intended. Note: There is a link to JSFiddle at the bottom of this response.
/**
* Add a swap function to the Array object.
* #param a Object a.
* #param b Object b.
*/
Array.prototype.swap = function (a, b) {
var tmp = this[a];
this[a] = this[b];
this[b] = tmp;
};
/**
* A utility function to print out HTML to the screen in case the user's
* console is not enabled or present.
* #param message The message to print to the screen.
*/
var trace = function (message) {
id = 'trace';
t = $('#' + id);
if (t.length < 1) t = $('<div>', {
'id': id
}).appendTo($('body'));
t.append($('<p>', {
'html': message
}));
};
/**
* An implementation of the quick-sort algorithm that allows the user to
* use a custom comparator with the following signature
* {#code function(a, b) } which will return an integer value to determine
* ordering.
* #param array The array to be sorted.
* #param comparator The custom comparator function.
*/
var quickSort = function (array, comparator) {
var __internal_compare__ = function compare(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
};
comparator = comparator || __internal_compare__;
var qsort = function (array, begin, end, comparator) {
if (end - 1 > begin) {
var pivot = begin + Math.floor(Math.random() * (end - begin));
pivot = partition(array, begin, end, pivot, comparator);
qsort(array, begin, pivot, comparator);
qsort(array, pivot + 1, end, comparator);
}
};
var partition = function (array, begin, end, pivot, comparator) {
var piv = array[pivot];
array.swap(pivot, end - 1);
var store = begin;
var ix;
for (ix = begin; ix < end - 1; ++ix) {
if (piv != undefined && comparator(piv, array[ix]) < 0) {
array.swap(store, ix);
++store;
}
}
array.swap(end - 1, store);
return store;
};
qsort(array, 0, array.length, comparator);
};
// The custom compare function to be used on the array of data below
// in the quick-sort function.
var compare = function (a, b) {
var isBoolAndFalse = function (val) {
return typeof val === 'boolean' && !val;
};
if (a == b) return 0;
var posa = a.position;
var posb = b.position;
var cona = isBoolAndFalse(posb);
var conb = isBoolAndFalse(posa);
if (cona && conb) {
var ida = a.id;
var idb = b.id;
return idb - ida;
}
if (conb) return -1;
if (cona) return 1;
return posb - posa;
};
/**
* Main procedure follows:
*/
var data = [
{ id: 1, position: false },
{ id: 2, position: false },
{ id: 3, position: 4 },
{ id: 4, position: false },
{ id: 5, position: 2 },
{ id: 6, position: 5 },
{ id: 7, position: 3 },
{ id: 8, position: 1 }
];
quickSort(data, compare);
trace(JSON.stringify(data, undefined, 2));
Output:
[
{
"id": 8,
"position": 1
},
{
"id": 5,
"position": 2
},
{
"id": 7,
"position": 3
},
{
"id": 3,
"position": 4
},
{
"id": 6,
"position": 5
},
{
"id": 1,
"position": false
},
{
"id": 2,
"position": false
},
{
"id": 4,
"position": false
}
]
You can test the above code out by using the following JSFiddle Demo. I hope this clears thing up for you.

Related

Loop through an array of objects, find duplicate objects by id, add their values in a single object, react js

I have a function inside of a react class component which generates properties. Since I want to be able to have duplicate properties, I've done it in a way that it is possible. However, I want those duplicate properties to be combined as a single value so that it can be displayed in the render function as a single property with a bigger value instead of 2 properties with smaller values. How can I achieve this based on the below code?
changePropertyState = () => {
let rngProperties = []
let maxProp = this.state.rarity.maxProperties;
let minProp = this.state.rarity.minProperties;
let rngCurrentPropAmount = Math.floor(Math.random() * (maxProp - minProp + 1) + minProp);
// the actual properties for an item based on the array of properties
for (let i = 0; i < rngCurrentPropAmount; i++) {
let rngNum = Math.floor(Math.random() * (itemProperties.length))
rngProperties.push(itemProperties[rngNum])
}
let proxyProperties = []
// setting the values for each property based on the min and max of that property
for (let j = 0; j < rngProperties.length; j++) {
let rngValue = this.getRandomNumber(rngProperties[j].min, rngProperties[j].max);
rngProperties[j].value = rngValue;
// creating a proxy to store unique values for each property,
let obj = {
id: rngProperties[j].id,
name: rngProperties[j].name,
min: rngProperties[j].min,
max: rngProperties[j].max,
value: rngProperties[j].value
}
proxyProperties.push(obj);
}
//setState() has an inbuilt functionality for a callback function, in which we can console.log the newly changed state
this.setState({
properties: proxyProperties
}, () => {
// console.log('propF', this.state)
});
}
An expected output of the above code is the below picture.
What I want to do is combine the 2 properties called (in this case) "Area Damage" so that only 1 property is listed but the value is 25 (again, in this case).
The itemProperties is an array of objects that have the following structure:
id: 1,
name: "intelligence",
min: 1,
max: 10,
value: 0
The rngCurrentPropAmount can be replaced with any integer for testing purposes. This is the amount of properties to be added.
The logic is to first group the array by name then merge them using reduce & summing the value. Bit tricky but working. Hope this is what was needed. The initial array has 4 elements & the final one has two. value is summed up.
const arr = [
{
id: 1, name: "intelligence", min: 1, max: 10, value: 11
},
{
id: 1, name: "intelligence", min: 1, max: 10, value: 4
},
{
id: 2, name: "dexterity", min: 1, max: 10, value: 3
},
{
id: 2, name: "dexterity", min: 1, max: 10, value: 8
}
];
//group an array by property
function groupBy(arr, property) {
return arr.reduce(function(memo, x) {
if (!memo[x[property]]) {
memo[x[property]] = [];
}
memo[x[property]].push(x);
return memo;
}, {});
}
//group by name
const grouped = groupBy(arr, "name");
const keys = Object.keys(grouped);
var output = [];
//loop keys
keys.forEach(key => {
//merge using reduce
const out = grouped[key].reduce((acc, current) => {
return {
id: current.id,
name: current.name,
min: current.min,
max: current.max,
value: acc.value + current.value
}
});
output.push(out);
});
console.log(output);

problems with for loop inside another for loop Javascript

I have problems in going through these two for loops, I need to get the same elements from the first array within the cycle, but the values ​​are being repeated. I know that they are repeated depending on the data of the second array.
I tried to make comparisons but I could not get the result I want.
var array = [
{
grouper: 1
},
{
grouper: 2
},
{
grouper: 3
},
{
grouper: 4
},
];
var array2 = [
{
value: 1,
grouper: 1,
status: 100
},
{
value: 2,
grouper: 2,
status: 100
},
{
value: 3,
grouper: 3,
status: 100
}
];
for(var i = 0; i<array.length; i++){
for(var j = 0; j<array2.length; j++){
if(array2[j].grouper == array[i].grouper){
console.log(array[i].grouper+'-'+array2[j].value);
}
}
}
This is the result I want, I need all the groupers from the first array and the values from the second array:
1-1
2-2
3-3
4-
The grouper 4, does not have value, but I need to show it.
I need the second array because I'm going to compare with the data from the second array
I do not know if I am doing the process wrong. I hope you can help me.
You could simply track if there was a match (variable shown), and if there were not any, display a "half" line:
var array = [{grouper: 1},{grouper: 2},{grouper: 3},{grouper: 4},];
var array2 = [
{value: 1, grouper: 1, status: 100},
{value: 2, grouper: 2, status: 100},
{value: 3, grouper: 3, status: 100}
];
for(var i = 0; i<array.length; i++){
var shown=false;
for(var j = 0; j<array2.length; j++){
if(array2[j].grouper == array[i].grouper){
console.log(array[i].grouper+'-'+array2[j].value);
shown=true;
}
}
if(!shown){
console.log(array[i].grouper+"-");
}
}
First of all, with the example you provided I believe you want to get back:
1,2,3
There is no 4th object inside of array2, so your conditional (array2[j].grouper == array[i].grouper will never evaluate to true.
The question here is whether you are always comparing the same indexes? In this example, you're comparing array[0] to array2[0] to see if grouper in array equals grouper in array2... that's it????
In that case you just do one loop:
for (var i = 0; i < array.length; i++) {
if (array[i].grouper == array2[i].grouper) {
console.log(array[i].grouper+'-'+array2[j].value);
}
}
#FabianSierra ... with your provided example one just needs to handle the not fulfilled if clause/condition in the most inner loop.
A more generic approach additionally might take into account changing field names (keys). Thus a function and Array.reduce / Array.find based approach provides better code reuse. An example implementation then might look similar to that ...
var array = [{ // in order.
grouper: 1
}, {
grouper: 2
}, {
grouper: 3
}, {
grouper: 4
}];
var array2 = [{ // not in the order similar to `array`.
value: 22,
grouper: 2,
status: 200
}, {
value: 33,
grouper: 3,
status: 300
}, {
value: 11,
grouper: 1,
status: 100
}];
function collectRelatedItemValuesByKeys(collector, item) {
var sourceKey = collector.sourceKey;
var targetKey = collector.targetKey;
var targetList = collector.targetList;
var resultList = collector.result;
var sourceValue = item[sourceKey];
var targetValue;
var relatedItem = targetList.find(function (targetItem) {
return (targetItem[sourceKey] === sourceValue);
});
if (typeof relatedItem !== 'undefined') {
targetValue = relatedItem[targetKey];
} else if (typeof targetValue === 'undefined') {
targetValue = ''; // `relatedItem` does not exist.
}
resultList.push([sourceValue, targetValue].join('-'));
return collector;
}
var resultList = array.reduce(collectRelatedItemValuesByKeys, {
sourceKey: 'grouper',
targetKey: 'value',
targetList: array2,
result: []
}).result;
console.log('resultList : ', resultList);
resultList = array.reduce(collectRelatedItemValuesByKeys, {
sourceKey: 'grouper',
targetKey: 'status',
targetList: array2,
result: []
}).result;
console.log('resultList : ', resultList);
.as-console-wrapper { max-height: 100%!important; top: 0; }

create element at index position if index does not exist in array

I have an array objects that hold an id and a name
const stages = [{
id: 1,
name: ''
}, {
id: 2,
name: ''
}, {
id: 3,
name: ''
}, {
id: 4,
name: ''
}, {
id: 5,
name: ''
}, {
id: 6,
name: ''
}, {
id: 7,
name: ''
}, {
id: 8,
name: ''
}];
Further I have an array that holds numbers.
const indexPositions = [0, 1, 2, 2, 2, 3, 2, 0];
I want to create a third array that holds arrays. Each number in distances represents the index of the current array within the array.
If the current array does not exist yet I want to create it first. Obviously I have to create new arrays until I get to this index position.
Example:
My array is empty at start. The first index position is 0 so I have to create a new array for this. The next index position is 3 so I have to create more arrays until I have 4 arrays.
All I want to do is to push the stage to its correct level index position. The result of this example would be
const levels = [
[stage1, stage8],
[stage2],
[stage3, stage4, stage5, stage7],
[stage6]
];
Currently my code looks this
$(document).ready(() => {
const levels = []; // the array containing the arrays
stages.forEach((stage, stageIndex) => {
const indexPosition = indexPositions[stageIndex];
const positionDifference = indexPosition - levels.length;
if (positionDifference > 0) {
for (let i = 0; i < positionDifference; i++) { // fill up with empty arrays
levels.push([]);
}
}
levels[indexPosition].push(stage);
});
});
I get this error Uncaught TypeError: Cannot read property 'push' of undefined and this happens because the indexPosition is out of bounds. If the positionDifference is 0 no array gets created but in the beginning the array is empty.
I tried setting levels.length to -1 if it is 0 but I still get the error if the difference is 1, I create one array at position 0 and want to access position 1.
How can I create an empty array if it does not exist?
While I do not fully understand what you want to do, checking existence of an array element is simple, one way of doing that is coercing it to boolean:
const thing=[];
function addElem(where,what){
if(!thing[where]) // <- here
thing[where]=[];
thing[where].push(what);
}
addElem(2,1);
addElem(2,2);
addElem(2,3);
addElem(5,1);
console.log(thing);
(The indices are deliberately non-continuous, because that does not matter: JavaScript arrays are sparse)
You could use a single loop and add an array for the index if not exists. Then push the wanted value.
var stages = [{ id: 1, name: '' }, { id: 2, name: '' }, { id: 3, name: '' }, { id: 4, name: '' }, { id: 5, name: '' }, { id: 6, name: '' }, { id: 7, name: '' }, { id: 8, name: '' }],
indexPositions = [0, 1, 2, 2, 2, 3, 2, 0],
result = stages.reduce((r, o, i) => {
var index = indexPositions[i];
r[index] = r[index] || []; // take default value for falsy value
r[index].push('stage' + o.id); // instead of string take object
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You actually were very close! You have a very small issue in your code.
$(document).ready(() => {
const levels = []; // the array containing the arrays
stages.forEach((stage, stageIndex) => {
const indexPosition = indexPositions[stageIndex];
const positionDifference = indexPosition - levels.length + 1; //YOU DID NOT ADD 1 HERE
if (positionDifference > 0) {
for (let i = 0; i < positionDifference; i++) { // fill up with empty arrays
levels.push([]);
}
}
levels[indexPosition].push(stage);
});
});
When you were calculating the positionDifference, you did not add 1 causing the problem when indexPosition equaled 0 and the for loop did not run and no new arrays were pushed. Just adding one fixed the problem :-)

Merge two arrays with objects

I plan to merge two objects:
var c = {
name: "doo",
arr: [
{
id: 1,
ver: 1
},
{
id: 3,
ver: 3
}
]
};
var b = {
name: "moo",
arr: [
{
id: 1,
ver: 0
},
{
id: 2,
ver: 0
}
]
};
When using Object.assign({},b,c) what happens is, that the b.arr is simply being replaced with c.arr.
My question is, how do I preserve objects inside the b.arr that are not in c.arr but still merge objects from that array when they match b.arr[0].id === c.arr[0].id. The desired outcome would look like:
{
name: "doo",
arr: [
{
id: 1,
ver: 1
},
{
id: 2,
ver: 0
},
{
id: 3,
ver: 3
}
]
}
Thanks.
You could have a look at ArrayUtils.addAll() from the apache commons
As soon as you use lodash - you may use a combination of lodash's functions. It may look a bit complex but it's not:
_.assign({}, b, c, function(objectValue, sourceValue, key, object, source) {
//merging array - custom logic
if (_.isArray(sourceValue)) {
//if the property isn't set yet - copy sourceValue
if (typeof objectValue == 'undefined') {
return sourceValue.slice();
} else if (_.isArray(objectValue)) {
//if array already exists - merge 2 arrays
_.forEach(sourceValue, function(sourceArrayItem) {
//find object with the same ID's
var objectArrayItem = _.find(objectValue, {id: sourceArrayItem.id});
if (objectArrayItem) {
//merge objects
_.assign(objectArrayItem, sourceArrayItem);
} else {
objectValue.push(sourceArrayItem);
}
});
return objectValue;
}
}
//if sourceValue isn't array - simply use it
return sourceValue;
});
See the full demo here.
Try this function:
function mergeArrayObjects (a, b) {
var tmp, // Temporary array that will be returned
// Cache values
i = 0,
max = 0;
// Check if a is an array
if ( typeof a !== 'object' || typeof a.indexOf === 'undefined')
return false;
// Check if b is an array
if ( typeof b !== 'object' || typeof b.indexOf === 'undefined')
return false;
// Populate tmp with a
tmp = a;
// For each item in b, check if a already has it. If not, add it.
for (i = 0, max = b.length; i < max; i++) {
if (tmp.indexOf(b[i]) === -1)
tmp.push(b[i]);
}
// Return the array
return tmp;
}
JsFiddle here
Note: Because I'm anal, I decided to see if this function is faster than the alternative proposed. It is.
Using lodash, I would do something like this:
var first = {
name: 'doo',
arr: [
{ id: 1, ver: 1 },
{ id: 3, ver: 3 }
]
};
var second = {
name: 'moo',
arr: [
{ id: 1, ver: 0 },
{ id: 2, ver: 0 }
]
};
_.merge(first, second, function(a, b) {
if (_.isArray(a)) {
return _.uniq(_.union(a, b), 'id');
} else {
return a;
}
});
// →
// {
// name: 'doo',
// arr: [
// { id: 1, ver: 1 },
// { id: 2, ver: 0 },
// { id: 3, ver: 3 }
// ]
// }
The merge() function let's you specify a customizer callback for things like arrays. So we just need to check it it's an array we're dealing with, and if so, use the uniq() and union() functions to find the unique values by the id property.

How to find elements in array2 that are not in array1?

I have two arrays:
var a1 = [ { ID: 2, N:0 }, { ID: 1, N:0 } ];
var a2 = [ { ID: 1, N:0 }, { ID: 2, N:0 }, { ID: 3, N:0 } ];
I need to get all elements that are on a2 but not in a1. An element here is distinct of another only by the property ID, the other properties should be ignored. And I cannot guarantee the order of the elements on the arrays. Meaning the result for this example should be:
var result = [ { ID: 3, N:0 } ]; // result for the example above
How can I do this in an efficient way? (I will be comparing arrays from 500 to 5,000 length)
To do this efficiently, you need to build an index of the items that are already in a1 so you can cycle through a2 and compare each one to the index to see if it's already been seen or not. One can use a javascript object for an index. Cycle through a1 and put all its IDs into the index. Then cycle through a2 and collect any items whose ID does not appear in the index.
function findUniques(testItems, baseItems) {
var index = {}, i;
var result = [];
// put baseItems id values into the index
for (i = 0; i < baseItems.length; i++) {
index[baseItems[i].ID] = true;
}
// now go through the testItems and collect the items in it
// that are not in the index
for (i = 0; i < testItems.length; i++) {
if (!(testItems[i].ID in index)) {
result.push(testItems[i]);
}
}
return(result);
}
var a1 = [ { ID: 2, N:0 }, { ID: 1, N:0 } ];
var a2 = [ { ID: 1, N:0 }, { ID: 2, N:0 }, { ID: 3, N:0 } ];
var result = findUniques(a2, a1);
// [{"ID":3,"N":0}]
Working demo: http://jsfiddle.net/jfriend00/uDEtg/
The same question has been posted a few times, have a look here:
JavaScript array difference
Most solutions are given through 'native' javascript however. I sometimes prefer to use underscore.js, since I build a lot of things using backbone.js and underscore is a dependency for Backbone. So I can use its awesome utilities. You might consider loading them in:
http://documentcloud.github.com/underscore/
var a1 = [ { ID: 2, N:0 }, { ID: 1, N:0 } ];
var a2 = [ { ID: 1, N:0 }, { ID: 2, N:0 }, { ID: 3, N:0 } ];
var from, to;
if(a1 > a2){
from = a1
to = a2
} else {
from = a2
to = a1
}
var a3 = _.filter(from, function(obj){
var compare = _.find(to, function(obj2){ return obj.ID === obj2.ID });
return compare === undefined
});
console.log(a3);
I first determined the longest array, I do this because I want to compare as many objects as possible to the shorter list. Otherwise we'd 'forget' some.
Then I simply use filter and find in the underscore.js library to return the objects that aren't in the shorter array, but ARE in the longer array.
If both arrays are of equal length it is fine too, because then we'd compare all of the items to all of the others.

Categories

Resources