2

I want to sort an array based on "before" and "after" conditions.

Example:

  • C should be before B: Example: {key: 'C', condition: {$before: 'B'}
  • B should be after A: Example: {key: 'B', condition: {$after: 'A'}
  • A: Example {key: 'A'}.

The resulting sorted list would be: A, C, B.

I need this so I can sort a list of middleware in a pipeline. Each middleware has a condition and I should come up with an order in which all conditions from all middleware are met. This is similar to what MS Project does to organize tasks in a Gantt given what are the requirements for a task.

Question: What is the easiest way to achieve it? Even by using external libraries like underscore? Bonus: Does this kind of sorting have a name better than "conditional sorting"? That would help on my Google searches.

EDIT: The input should be an array of items with their conditions. I can't hardcode the sort method.

EDIT 2: As @Berdi stated. This is called typological sorting. Yes, depending on the items and conditions, there might be no combination that meet all the conditions and my algorithm should trigger an exception.

EDIT 3: The way I'm thinking of implementing this is calculating all the possible combinations and then look for the first one that meet all the conditions. This may not be terribly slow for me because, in my case, I can do this just once when the app starts and I'm not going to have more than 50 items in the array. But anyway, for the science, it would be good to know a more optimized solution.

EDIT 4: I would accept a solution that works with after conditions only. Like MS Project.

Andre Pena
  • 56,650
  • 48
  • 196
  • 243

4 Answers4

2

Bonus: Does this kind of sorting have a name better than "conditional sorting"?

It's called topological sort. And depending on your exact input format and content, it might not even be well-defined.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thanks @Bergi. I didn't know that. And yes! Depending on my items and conditions... The sort method should trigger an exception. – Andre Pena Dec 06 '15 at 14:20
  • 2
    @andrerpena: Can you show us the input format in which your "rules" come? Only with that we can devise a proper and complete answer. – Bergi Dec 06 '15 at 14:24
  • I would accept an answer that works with `$after` conditions only.. If it's too hard to get both – Andre Pena Dec 06 '15 at 14:42
  • 1
    @andrerpena: `x y` means `y x`, so that's trivial to transform. – Bergi Dec 06 '15 at 15:11
2

I would use: a hash table, a pointer for the start and then reassembling the array.

(This answer is part of my answer from this question: Ordering array of objects that have a BeforeID and AfterID on each object)

function chain(array) {
    var o = {}, pointer;
    array.forEach(function (a) {
        o[a.id] = a;
        if (a.beforeId === null) {
            pointer = a.id;
        }
    });
    array = [];
    do {
        array.push(o[pointer]);
        pointer = o[pointer].afterId;
    } while (pointer !== null);
    return array;
}

var unsorted = [{ id: 7, beforeId: 6, afterId: 8 }, { id: 11, beforeId: 10, afterId: null }, { id: 0, beforeId: null, afterId: 1 }, { id: 1, beforeId: 0, afterId: 2 }, { id: 4, beforeId: 3, afterId: 5 }, { id: 8, beforeId: 7, afterId: 9 }, { id: 2, beforeId: 1, afterId: 3 }, { id: 9, beforeId: 8, afterId: 10 }, { id: 10, beforeId: 9, afterId: 11 }, { id: 3, beforeId: 2, afterId: 4 }, { id: 5, beforeId: 4, afterId: 6 }, { id: 6, beforeId: 5, afterId: 7 }];
document.write('<pre>' + JSON.stringify(chain(unsorted), 0, 4) + '</pre>');

A Solution for EDIT 4, after conditions only.

function chain(array) {
    var n = {}, o = {}, pointer;
    array.forEach(function (a) {
        o[a.id] = a;
        n[a.after] = a.id;
        if (a.after === null) {
            pointer = a.id;
        }
    });
    // rewind pointer.
    // i took push for the array. otherwise the array could be mounted
    // from the other end with unshift but push is more efficient.
    do {
        pointer = n[pointer];
    } while (pointer in n);
    array = [];
    do {
        array.push(o[pointer]);
        pointer = o[pointer].after;
    } while (pointer !== null);
    return array;
}

var unsorted = [{ id: 7, after: 8 }, { id: 11, after: null }, { id: 0, after: 1 }, { id: 1, after: 2 }, { id: 4, after: 5 }, { id: 8, after: 9 }, { id: 2, after: 3 }, { id: 9, after: 10 }, { id: 10, after: 11 }, { id: 3, after: 4 }, { id: 5, after: 6 }, { id: 6, after: 7 }, ];
document.write('<pre>' + JSON.stringify(chain(unsorted), 0, 4) + '</pre>');

Edit - with sparse before/after

function chain(array) {
    var after = {}, before = {}, o = {}, pointer = array[0].id;
    array.forEach(function (a) {
        o[a.id] = a;
        if (a.after !== null) {
            after[a.id] = a.after;
            before[a.after] = a.id;
        }
        if (a.before !== null) {
            before[a.id] = a.before;
            after[a.before] = a.id;
        }
    });

    document.write('<pre>before ' + JSON.stringify(before, 0, 4) + '</pre>');
    document.write('<pre>after ' + JSON.stringify(after, 0, 4) + '</pre>');

    do {
        document.write('pointer: ' + pointer + '<br>');
        pointer = before[pointer];
    } while (pointer in before);
    document.write('pointer: ' + pointer + '<br>');
    array = [];
    do {
        array.push(o[pointer]);
        pointer = after[pointer];
    } while (pointer !== undefined);
    return array;
}

var unsorted = [{ id: 'C', before: 'B', after: null }, { id: 'B', before: null, after: null }, { id: 'A', before: null, after: 'B' }];
document.write('<pre>' + JSON.stringify(chain(unsorted), 0, 4) + '</pre>');
Community
  • 1
  • 1
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • 1
    That seems promising. I've got to have lunch now, but I'll check your solution first thing after! Thank you in advance. – Andre Pena Dec 06 '15 at 14:45
  • I appreciate your effort. I just tested your solution against my example... your do/while is exiting on the first iteration so the resulting array has only one item: https://jsfiddle.net/vasa2vx9/1/ – Andre Pena Dec 06 '15 at 17:56
  • hm, `var unsorted = [ { id: 'C', before: 'B', after: null }, { id: 'B', before: null, after: 'A'}, { id: 'A', before: null, after: null }];` makes no sense, because b has two children, but the solution works for chains, not for trees. – Nina Scholz Dec 06 '15 at 18:44
  • I see. Yes, my question is more about of a tree than a chain. Thank you! I appreciate your effort. I'm upvoting you for getting close :) – Andre Pena Dec 06 '15 at 20:16
0

This may be of use.

function sortByRules(rules, arr) {
    var rulesEvaluated = [];

    rules.forEach(rule => {
      applyRule(rule, arr);
      rulesEvaluated.push(rule);

      if (conflicts(rulesEvaluated, arr)){
        throw new Error('rule conflict');
      }
    });

    return arr;
}

function conflicts(rules, arr) {
    return rules.some(r => r.condition.before ?
      arr.indexOf(r.key)>arr.indexOf(r.condition.before) :
      arr.indexOf(r.key)<arr.indexOf(r.condition.after));
}

function applyRule(rule, arr) {
    var increment = rule.condition.before ? -1 : 1;
    var item = arr.splice(arr.indexOf(rule.key), 1)[0];
    var target = rule.condition.before || rule.condition.after;
    arr.splice(Math.max(0, arr.indexOf(target) + increment), 0, item);
}


var toSort = ['b', 'd', 'c', 'a'];    
var rules = [ { key: 'b', condition: { before: 'c' } }, 
              { key: 'd', condition: { after: 'c' } }, 
              { key: 'a', condition: { before: 'b' } } ];
document.write(sortByRules(rules, toSort)); // ['a', 'b',  'c', 'd']
Ben Aston
  • 53,718
  • 65
  • 205
  • 331
-1

Create an object storing the order you need, then use [].sort() as usual:

var sorter = {
  'A': 1,
  'B': 3,
  'C': 2
}

var input = ['A', 'B', 'C'];

input = input.sort(function sort(a, b) {
  return sorter[a] > sorter[b];
});

console.log(input);
document.write("<pre>" + input + "</pre>");

Now, you can sort any array containing any combination of A, B and C. A, B, C can be anything, a property, its length, the first letter of a value...

Shanoor
  • 13,344
  • 2
  • 29
  • 40
  • 1
    "*Create an object storing the order you need*" is actually the hard part if you've got only "before" and "after" rules. – Bergi Dec 06 '15 at 14:21
  • If the rules come from your boss/client, no other choice than manually build this object. If they come from a database, a parsable file or such, there might be a way to programmatically generate it. – Shanoor Dec 06 '15 at 14:26