Order array of objects based on dependencies list? - javascript

I need to order an array of objects, composed by a name and a dependencies list (made out of names).
An example of this array could be:
[
{ name: 'a', requires: ['b', 'c'] },
{ name: 'b', requires: ['c'] },
{ name: 'c', requires: [] },
]
I'd like this array to be sorted so that the items which require a specific set of dependencies will be positioned after its required dependencies.
The array could actually contain more items, I'm okay if the sorting function throws an error in case of circular dependencies.
Example output:
[
{ name: 'c', requires: [] }, // first, no dependencies, and required by both the others
{ name: 'b', requires: ['c'] }, // second, because it needs `c` first
{ name: 'a', requires: ['b', 'c'] }, // last, because requires both the others
]
What's the most concise way to do it?

You can try following (changed test case to support more possible combinations)
var arr = [
{ name: 'd', requires: ['a', 'c'] },
{ name: 'a', requires: ['b', 'c'] },
{ name: 'b', requires: ['c'] },
{ name: 'e', requires: ['d'] },
{ name: 'c', requires: [] },
];
var map = {}; // Creates key value pair of name and object
var result = []; // the result array
var visited = {}; // takes a note of the traversed dependency
arr.forEach(function(obj){ // build the map
map[obj.name] = obj;
});
arr.forEach(function(obj){ // Traverse array
if(!visited[obj.name]) { // check for visited object
sort_util(obj);
}
});
// On visiting object, check for its dependencies and visit them recursively
function sort_util(obj){
visited[obj.name] = true;
obj.requires.forEach(function(dep){
if(!visited[dep]) {
sort_util(map[dep]);
}
});
result.push(obj);
}
console.log(result);

Update: thanks to Nina Scholz, I updated the code so that sort should work
This might do the job.
The idea behind is, to user the sort and check if element a is in the requires of element b. If so, we can assume, that ashould be before b.
But I´m not 100% sure, I just checked against your example and the example of #nikhilagw. I might have forgotten something. Please let me know if it worked!
For every element, I additionally inherit all dependencies.
const list = [
{ name: 'b', requires: ['c'] },
{ name: 'e', requires: ['d'] },
{ name: 'd', requires: ['a', 'c'] },
{ name: 'c', requires: [] },
{ name: 'a', requires: ['b', 'c'] },
];
// indexed by name
const mapped = list.reduce((mem, i) => {
mem[i.name] = i;
return mem;
}, {});
// inherit all dependencies for a given name
const inherited = i => {
return mapped[i].requires.reduce((mem, i) => {
return [ ...mem, i, ...inherited(i) ];
}, []);
}
// order ...
const ordered = list.sort((a, b) => {
return !!~inherited(b.name).indexOf(a.name) ? -1 : 1;
})
console.log(ordered);

This proposal looks for previous elements and checks if the actual element has the wanted requirements sorted before.
If all requirements are found the object is spliced to the index.
function order(array) {
var i = 0,
j,
temp;
while (i < array.length) {
temp = array.slice(0, i);
for (j = i; j < array.length; j++) {
if (array[j].requires.every(n => temp.some(({ name }) => n === name))) {
array.splice(i++, 0, array.splice(j, 1)[0]);
break;
}
}
}
return array;
}
var array = [{ name: 'd', requires: ['a', 'c'] }, { name: 'a', requires: ['b', 'c'] }, { name: 'b', requires: ['c'] }, { name: 'e', requires: ['d'] }, { name: 'c', requires: [] }];
console.log(order(array));
.as-console-wrapper { max-height: 100% !important; top: 0; }

After several years I found this super short solution to the problem, a friend of mine shared it with me, I don't take credits.
elements.sort((a, b) =>
a.requires.includes(b.name) ? 1 : b.requires.includes(a.name) ? -1 : 0
);

Your data can be represented via graph. So you could use topological sort for this problem.

Related

Remove duplicated combination in array based on index

I have the following data array:
const data = [
{
value: [
'a',
'b',
'a',
'a'
]
},
{
value: [
'c',
'c',
'd',
'c'
]
}
];
So there's is 4 combination here based on index:
combination 1 : a - c (index 0 in each value arrays)
combination 2 : b - c (index 1 in each value arrays)
combination 3 : a - d (index 2 in each value arrays)
combination 4 : a - c (index 3 in each value arrays)
As you can see the first and the last combinations are the same, so i want to remove the second occurrence from each array, the result should be:
[
{
value: [
'a',
'b',
'a'
]
},
{
value: [
'c',
'c',
'd'
]
}
]
You can zip the values arrays from both objects to form an array which looks like:
["a-c", "b-c", ...]
As these are now strings, you can turn this array into a Set using new Set(), which will remove all duplicate occurrences. You can then turn this set back into an array which you can then use .reduce() on to build you array of objects from. For each value you can obtain the list of values by using .split() on the '-', and from that, populate your reduced array.
See example below:
const data = [{ value: [ 'a', 'b', 'a', 'a' ] }, { value: [ 'c', 'c', 'd', 'c' ] } ];
const unq = [...new Set(
data[0].value.map((_,c)=> data.map(({value})=>value[c]).join('-'))
)];
const res = unq.reduce((acc, str) => {
const values = str.split('-');
values.forEach((value, i) => acc[i].value.push(value));
return acc;
}, Array.from({length: data.length}, _ => ({value: []})))
console.log(res);
Limitations of the above method assume that you won't have a - character as your string value. If this is an issue, you can consider using a different delimiter, or find unique values within your array using .filter() instead of a Set.
You could save a lookup object for unique pairs of value based with index
Given your input is, below solution could help you
const data = [
{
value: ["a", "b", "a", "a"],
},
{
value: ["c", "c", "d", "c"],
},
]
const lookup = {}
data[0].value.forEach((_, index) => {
lookup[`${data[0].value[index]}-${data[1].value[index]}`] = true
})
const res = Object.keys(lookup).reduce(
(acc, key) => {
const [val1, val2] = key.split("-")
acc[0].value.push(val1)
acc[1].value.push(val2)
return acc
},
[{ value: [] }, { value: [] }]
)
console.log(res)
Below is a two step solution with a generator function and a single pass.
const data = [ { value: [ 'a', 'b', 'a', 'a' ] }, { value: [ 'c', 'c', 'd', 'c', ] } ];
const zipDataValues = function* (data) {
const iterators = data.map(item => item.value[Symbol.iterator]())
let iterations = iterators.map(iter => iter.next())
while (iterations.some(iteration => !iteration.done)) {
yield iterations.map(iteration => iteration.value)
iterations = iterators.map(iter => iter.next())
}
}
const filterOutDuplicateCombos = function (values) {
const combosSet = new Set(),
resultData = [{ value: [] }, { value: [] }]
for (const [valueA, valueB] of values) {
const setKey = [valueA, valueB].join('')
if (combosSet.has(setKey)) {
continue
}
combosSet.add(setKey)
resultData[0].value.push(valueA)
resultData[1].value.push(valueB)
}
return resultData
}
console.log(
filterOutDuplicateCombos(zipDataValues(data))
) // [ { value: [ 'a', 'b', 'a' ] }, { value: [ 'c', 'c', 'd' ] } ]
Here is a reference on generators and iterators
Filter combinations + sorting by the first occurrence:
const data = [{
value: ['a', 'b', 'a', 'a']
},{
value: ['c', 'c', 'd', 'c']
}];
var res = {}, i, t;
for (i = 0; i < data[0].value.length; ++i) {
res[data[0].value[i]] = res[data[0].value[i]] || {};
res[data[0].value[i]][data[1].value[i]] = true;
}
data[0].value = [];
data[1].value = [];
for (i in res) {
for (t in res[i]) {
data[0].value[data[0].value.length] = i;
data[1].value[data[1].value.length] = t;
}
}
console.log(data);

sorting array manually using a reference array [duplicate]

This question already has answers here:
How to sort in specific order - array prototype(not by value or by name)
(4 answers)
Closed 3 years ago.
const order = ['b', 'c', 'a'];
const objects = [
{ name: 'a' },
{ name: 'b' },
{ name: 'c' },
];
Trying to figure out the most efficient way to sort the objects array by name using the manual order array.
Here is a quick use of sort plus indexOf.
const order = ['b', 'c', 'a'];
const objects = [
{ name: 'a' },
{ name: 'b' },
{ name: 'c' },
];
const sortedObjects = objects.sort((o1, o2) => order.indexOf(o1.name) - order.indexOf(o2.name));
console.log(sortedObjects);
With cached indices:
const order = ['b', 'c', 'a'].reduce((acc, elt, index) => (acc[elt] = index, acc), {});
const objects = [
{ name: 'a' },
{ name: 'b' },
{ name: 'c' },
];
const sortedObjects = objects.sort((o1, o2) => order[o1.name] - order[o2.name]);
console.log(sortedObjects);
You can cache the indices using Object.entries() and Object.fromEntries() to re-arrange the order object into a lookup table:
const order = ['b', 'c', 'a'];
const objects = [
{ name: 'a' },
{ name: 'b' },
{ name: 'c' },
];
const lut = Object.fromEntries(
Object.entries(order).map(entry => entry.reverse())
);
objects.sort((a, b) => lut[a.name] - lut[b.name]);
console.log(objects);

JavaScript | AngularJS: Filter Collection by Nested Value

Filter Collection by Nested Item in Array
Say there's a collection:
[ { key: '', param: '', array: ['a', 'b', 'c'] },... ]
Using [Angular] $filter('filter'), how can I return a collection of objects whose .array contains the specified value 'c'?
For instance, one could imagine writing the following.
var token = 'c';
var query = { array: token };
var anyWithC = filter(collection, query, true);
This doesn't seem to work so I've tried...
var query = { array: [token] };
I don't think this is working either.
Could someone set me straight on this one? Is this possible to filter this way? Must I use a function instead of a query object?
Use Underscore library's _.filter(list, predicate);
Working demo
var sample = [{
name: 'test',
lastName: 'test',
address: ['a', 'b', 'c']
}, {
name: 'test1',
lastName: 'test1',
address: ['d', 'e', 'f']
}, {
name: 'test2',
lastName: 'test2',
address: ['g', 'h', 'i']
}];
//key is the objecy key from which the value will be extracted to compare with the token.
function filterByKey(list, key, token) {
var filterArray = _.filter(list, function(value) {
//If Undefined value then should return false
if (_.isUndefined(value[key])) {
return false;
} else if (_.isArray(value[key])) {
//Checks the extracted value is an Array
return _.contains(value[key], token);
}else {
return value[key] === token;
}
});
return filterArray;
}
console.log('Output ------------- ', filterByKey(sample, 'address', 'h'));
You can use lodash
https://lodash.com/docs
Have created a JSFiddle using angular and lodash:
https://jsfiddle.net/41rvv8o8/
This is based on the data you provided, and this demo is not case sensitive (upper and lower cases).
HTML
<div ng-app="app">
<example-directive>
</example-directive>
</div>
Javascript
var app = angular.module('app',[]);
app.directive('exampleDirective', function() {
return {
template:'<h1>Collections with array containing c</h1><ul><li ng-repeat="collection in matchcollections">key: {{collection.key}}, param: {{collection.param}}, <div ng-repeat="item in collection.array">{{item}}</div></li></ul>',
controller: function($scope){
$scope.collections = [ { key: 'A', param: 'A1', array: ['a', 'b', 'c'] },{ key: 'B', param: 'B1', array: ['x', 'h', 'c'] },{ key: 'C', param: 'C', array: ['t', 'a', 'k'] }];
$scope.matchcollections = _.filter($scope.collections, function(c){
return _.includes(c.array,'c')
});
console.log("Collection with match: ", $scope.matchcollections);
}
}
});
Hope this helps!

IndexBy nested object value of type array

I am wanting to perform the below transformation, but am having trouble with how to do it, just wondering if anyone has any pointers:
//Source
[
{ types: ['a', 'b'] },
{ types: ['b'] },
{ types: ['a', 'c'] }
]
//Transformation
{
'a': [ { types: ['a', 'b'] }, { types: ['a', 'c'] } ],
'b': [ { types: ['a', 'b'] }, { types: ['b'] } ],
'c': [ { types: ['a', 'c'] } ]
}
Use reduce() with forEach()
var data = [{
types: ['a', 'b']
}, {
types: ['b']
}, {
types: ['a', 'c']
}];
var res = data.reduce(function(a, b) {
b.types.forEach(function(v) { // iterate over inner array
a[v] = a[v] || []; // define the property if not defined
a[v].push(b); // push the object refence
});
return a;
}, {});
document.write('<pre>' + JSON.stringify(res, 0, 3) + '</pre>');
For older browser check polyfill options of forEch and reduce methods.
We can use .reduce of Array & iterate
var test = [
{ types: ['a', 'b'] },
{ types: ['b'] },
{ types: ['a', 'c'] }
]
test.reduce(function(res,obj,index){
obj.types.forEach(function(x){
res[x] = res[x] || [];
res[x].push(obj)
});
return res;
},{});
var data = [{
types: ['a', 'b']
}, {
types: ['b']
}, {
types: ['a', 'c']
}];
var transform = function(records) {
var obj = {};
records.forEach(function(record){
record.types.forEach(function(value){
obj[value] = obj[value] || []
obj[value].push(record);
});
});
return obj;
};
document.write('<pre>' + JSON.stringify(transform(data)) + '</pre>');

group array by groups and sort by position

I have an array. I need to group this array by groups and sort by position. I tied to create a new array with group names as keys and values as sorted array grouped by group, but didn't work well. How can I do this?
a = [
{id:1,name:'qw'group:'C',name:'hite',position:'1'},
{id:2,name:'qwe'group:'B',name:'ite',position:'2'},
{id:3,name:'qwer'group:'A',name:'ite',position:'3'},
{id:4,name:'qer'group:'D',name:'te',position:'4'},
{id:5,name:'wer'group:'C',name:'whit',position:'5'},
{id:6,name:'er'group:'B',name:'whi',position:'6'},
]
function groupDo(array){
var groups = [];
for (var i in array){
groups[array[i].group] = array[i].group;
}
for (var i in array){
if (groups[array[i].group] == array[i].group){
groups[array[i].group] = array[i];
}
}
}
Here's a simple straight forward answer:
var sortByPosition = function(obj1, obj2) {
return obj1.position - obj2.position;
};
var arr = [
{ id: 1, name: 'qw', group: 'C', name: 'hite', position: '1' },
{ id: 2, name: 'qwe', group: 'B', name: 'ite', position: '2' },
{ id: 3, name: 'qwer', group: 'A', name: 'ite', position: '3' },
{ id: 4, name: 'qer', group: 'D', name: 'te', position: '4' },
{ id: 5, name: 'wer', group: 'C', name: 'whit', position: '5' },
{ id: 6, name: 'er', group: 'B', name: 'whi', position: '6' },
];
var grouped = {};
for (var i = 0; i < arr.length; i += 1) {
if(!grouped[arr[i].group]) {
grouped[arr[i].group] = [];
}
grouped[arr[i].group].push(arr[i]);
}
for (var group in grouped) {
grouped[group] = grouped[group].sort(sortByPosition);
}
console.log(grouped);
When you want to do stuff like this though, it's usually recommended to use a utility library like lodash or underscore.js, so that you don't have to "reinvent the wheel". Here's how it would look like using one of these libraries:
var arr = [
{ id: 1, name: 'qw', group: 'C', name: 'hite', position: '1' },
{ id: 2, name: 'qwe', group: 'B', name: 'ite', position: '2' },
{ id: 3, name: 'qwer', group: 'A', name: 'ite', position: '3' },
{ id: 4, name: 'qer', group: 'D', name: 'te', position: '4' },
{ id: 5, name: 'wer', group: 'C', name: 'whit', position: '5' },
{ id: 6, name: 'er', group: 'B', name: 'whi', position: '6' },
];
var grouped = _.groupBy(arr, 'group');
for (var group in grouped) {
_.sortBy(grouped[group], 'position');
}
console.log(grouped);
Here ya go!
a = [
{id:1,name:'qw',group:'C',name:'hite',position:'1'},
{id:2,name:'qwe',group:'B',name:'ite',position:'2'},
{id:3,name:'qwer',group:'A',name:'ite',position:'3'},
{id:4,name:'qer',group:'D',name:'te',position:'4'},
{id:5,name:'wer',group:'C',name:'whit',position:'5'},
{id:6,name:'er',group:'B',name:'whi',position:'6'},
]
function groupAndSort(array, groupField, sortField) {
var groups = {}; // This object will end being keyed by groups, and elements will be arrays of the rows within the given array, which have been sorted by the sortField
// Put all the rows into groups
for (var i = 0; i < array.length; i++) {
var row = array[i];
var groupValue = row[groupField];
groups[groupValue] = groups[groupValue] || [];
groups[groupValue].push(row);
}
// Sort each group
for (var groupValue in groups) {
groups[groupValue] = groups[groupValue].sort(function(a, b) {
return a[sortField] - b[sortField];
});
}
// Return the results
return groups;
}
var groupedAndSorted = groupAndSort(a, "group", "position");
If you want to group objects, first think about what the resulting data would look like. Maybe something like this?
var grouped = {
A : [
{id:3,name:'qwer', group:'A',name:'ite',position:'3'}
],
B : [],
C : [],
D : []
};
And so on. To transform a list into an object, consider using .reduce().
.reduce() takes a function as its first argument, and a resulting object as the second. The function iterates through each element of the array and reduces it into the given object.
var data = [
{id:1,name:'qw', group:'C',name:'hite',position:'1'},
{id:2,name:'qwe', group:'B',name:'ite',position:'2'},
{id:3,name:'qwer', group:'A',name:'ite',position:'3'},
{id:4,name:'qer', group:'D',name:'te',position:'4'},
{id:5,name:'wer', group:'C',name:'whit',position:'5'},
{id:6,name:'er', group:'B',name:'whi',position:'6'},
]
// acc is the accumulated object, x is each element of the array
data.reduce(function(acc, x) {
// first check if the given group is in the object
acc[x.group] = acc[x.group] ? acc[x.group].concat(x) : [x];
return acc;
}, {}); // this is the resulting object
Now all you need to do is use the built in sort to order the resulting arrays. You could do this by iterating through the keys of the resulting object and applying .sort() to each array. .sort() takes a function as an argument which accesses the data and provides a comparison function.
// a and b are elements of the array
array.sort(function(a, b) {
if (a.position > b.position) {
return -1;
} else if (b.position > a.position) {
return 1;
} else {
return 0;
}
});
And you would implement it like so
var result = Object.keys(data).map(function(d){
return d.sort(f); // f is the function above
});

Categories