0

The Problem

Following our team check-in, we break off in to co-ordination meetings with (n) participants covering various (uniquely named) Topics.

Currently, people stand around trying to self organize and we are not maximizing concurrency of meetings, and/or releasing team members as soon as possible to return to work.

We keep a list of all co-ordination meetings that come out of the check-ins in a Google Spreadsheet:

+-----------------------------------------------------------+
|  Meetings                                                 |
+-----------------------------------------------------------+
|  Topic A  |  Topic B  |  Topic C  |  Topic D  |  Topic E  |
+-----------------------------------------------------------+
|  Billy    |   JD      |  David    |   Tania   |   JB      |
|  Mel      |   Rowan   |  Emily    |   David   |   Fred    |
|  Tracey   |   Mike    |           |   Mike    |   Tania   |
|  JB       |   Aaron   |           |   Fred    |           |
|  Luke     |   Billy   |           |           |           |
|  Aaron    |           |           |           |           |
|  Michael  |           |           |           |           |
+-----------------------------------------------------------+

As a starting point, I have (surely quite inefficient) code that iterates over every meeting and returns an array of pairs that have no participant clashes.

Example output from main() with above as input data (don't worry all code is below):

[["Topic A","Topic C"],["Topic A","Topic D"],["Topic B","Topic C"],["Topic B","Topic E"],["Topic C","Topic E"]]


What I Need

Ideally, I need a solution that returns an array ordered by most meetings possible to be run at once. Either using the array of pairs returned from main() or an entirely other method using the data in the spreadsheet as input.

In my example here, it would take the form:

[[Topic B, Topic C, Topic E],[Topic A, Topic D]]

I'll then group these visually by colour in the Google Spreadsheet so staff can see.

I attempted to create some code (main2()) that reduces the array based on mutually exclusive pairs, but it is plain wrong. Eg: That code outputs:

[["Topic A","Topic C","Topic B","Topic E"],"Topic D"]

...which is incorrect as A and B obviously have a clash. (A and C, B and E would be fine consecutively of course).


My Current Code

function onOpen () {
 var ui = SpreadsheetApp.getUi();
  ui.createMenu('Meeting Tools')
  .addItem('Compute Optimal Meeting Order', 'main')
  .addToUi();
}

function main() {
  var ss = SpreadsheetApp.getActive();
  var sss = ss.getActiveSheet();
  //var sss = ss.getSheetByName("Sheet3");
  var data = sss.getDataRange().getValues();

  var ui = SpreadsheetApp.getUi();

  var meetings = {};
  var pairs = [];
  var objKey;

  // Structuring data to obj
  for (var x=0; x < sss.getLastColumn(); x++) {
    for (var y=1; y < data.length; y++) {
      if (y==1) {
        objKey = data[y][x];
        meetings[objKey] = [];
      }
      else if (data[y][x]) {
        meetings[objKey].push(data[y][x]);
      }
    }
  }

  var keys = Object.keys(meetings).sort();
  var loc;

  // Starting with "A"
  for (var m in meetings) {
    if (!meetings.hasOwnProperty(m)) continue;

    Logger.log("-----------------------------");
    Logger.log("SCANNING:  " + m);
    Logger.log("-----------------------------");

    loc = keys.indexOf(m);

    // check A, B, C, D, E
    for (var m2 = 0; m2 < keys.length; m2++) {

      var pointer = (m2 + loc) % keys.length;      
      // DO NOT CHECK SAME MEETING
      if (keys[pointer] == m) {
    Logger.log("||||||||| WE ARE COMPARING OURSELVES: (" + keys[pointer] + "/"     + m + "). SKIPPING!");
        continue;
      }

      // FOR EACH PARTICIPANT OF CURRENT MEETING
      for (var p = 0; p < meetings[m].length; p++) {

        Logger.log("");
        Logger.log("  >>> COMPARING " + keys[pointer]);
        Logger.log("  >>> >>> " + meetings[m][p] + " " + (p+1) +"/"+meetings[m].    length);

          // SEEK THE LOCATION OF THE MEETING PARTICIPANT IN THE NEXT MEETING
          var testIndex = meetings[keys[pointer]].indexOf(meetings[m][p]);

          // IF THE MEETING PARTICIPANT IS ALSO IN THE NEXT MEETING
          if (testIndex > -1) {

        Logger.log("We have a match! Meeting "+ m + " has participant " + meetings[m][p] + " which unfortunately clashes with meeting " + keys[    pointer]);
        Logger.log("Therefore Meeting " + m + " cannot run at the same time as     " + keys[pointer]);
        Logger.log("We need to bail out of compairing against: " + keys[pointer    ]);

            // WE BAIL OUT AS WE HAVE A CLASH
            break; 
          }
      // IF THE MEETING PARTICIPANT IS NOT IN THE NEXT MEETING AND WE HAVE     CHECKED EVERYONE. WE SHOULD BE GOOD.
          else if (testIndex == -1 && p+1 == meetings[m].length) {
            Logger.log("This looks to be clear!!! Adding to pair group.");
            pairs.push([m,keys[pointer]]);
          }
      }//loop
    } //loop
  } //obj_loop

  // Logger.log("FINAL TALLY: " + JSON.stringify(pairs));
     Logger.log("FINAL TALLY (CLEANED): " + JSON.stringify(removeRepeats(pairs.sort())    ));

  // ui.alert(JSON.stringify(removeRepeats(pairs.sort())));
  // ss.toast(JSON.stringify(removeRepeats(pairs.sort())));

  // debugger;
  return removeRepeats(pairs.sort());
}


function main2(array) {

  // DEBUG
  //  array = [["A","C"],["A","D"],["B","C"],["B","E"],["C","E"]];
  //  array = [["A","C"],["A","D"],["B","C"],["B","E"],["C","E"]];
  array = main();

  var holdingArr = [];
  for (var i = array.length - 1; i >= 1; i--) {

    //DEBUG
    //var pair = ["Z","X"];
    var pair = array[i];

    if (arrayPairCheck([array[0]], pair)) {
      holdingArr.push(pair);
      array.pop(i);
    } 
    else {
      array[0] = array[0].concat(array[i]);
      array.pop(i);
    }
  } //loop

  if (holdingArr && holdingArr.length > 0) {
    for (var j=0; j < holdingArr.length; j++) {
      var checkIndex = holdingMeetingExistsInPair(array[0],holdingArr[j]);
      if (checkIndex !== false) {
        array.push(holdingArr[j][checkIndex]);
      }
    } //loop
  }

  // DEBUG
  debugger;
  Logger.log(JSON.stringify(array));
}


function holdingMeetingExistsInPair(arrayFirstIndexGroup,holdingArrPair) {
  if (arrayFirstIndexGroup && arrayFirstIndexGroup.length > 0) {
    if (arrayFirstIndexGroup.indexOf(holdingArrPair[0]) === -1) {
      return 0;
    }
    if (arrayFirstIndexGroup.indexOf(holdingArrPair[1]) === -1) {
      return 1;
    }
  }
  return false;
}


function arrayPairCheck(array,pair) {

  // DEBUG
  // array = [["A","C"],["A","D"],["B","C"],["B","E"],["C","E"]];
  // pair = ["Z","X"];

  var seen = false;

  if (array && array.length > 0) {
    for (var i=0; i < array.length; i++) {
      array[i].map(function(item,item2) {
        if (item === pair[0] || item === pair[1]) {
          seen = true;
          return true;
        }
      });
    } //loop
  }

  if (seen) { return true; } else { return false; }
}

// http://stackoverflow.com/questions/27734661/javascript-arrays-ab-ba
function removeRepeats(list) {
    var i;
    var b = [];
    var _c = [];

    for (i = 0; i < list.length; i++) {
        var a = list[i].sort();
        var stra = a.join("-");

        if(_c.indexOf(stra) === -1) {
            b.push(a);
            _c.push(stra);
        }
    }

    return b;
}


Lastly

Some avenues I've been reading about to piece together an understanding are below. My rep is too low to link these. Someone may confrim this is a direction to pursue?


Thank you for any time spent on this.


UPDATE 1

For the following, the solution should correctly have meeting order set as:

1) W, X, Z
2) V, Y

+-----------------------------------------------------------+
|  Meetings                                                 |
+-----------------------------------------------------------+
|  Topic V  |  Topic W  |  Topic X  |  Topic Y  |  Topic Z  |
+-----------------------------------------------------------+
|  Billy    |   JD      |  David    |   Tania   |   JB      |
|  Mel      |   Rowan   |  Emily    |   David   |   Fred    |
|  Tracey   |   Mike    |           |   Mike    |           |
|  JB       |           |           |   Fred    |           |
|  Luke     |           |           |           |           |
|  Aaron    |           |           |           |           |
|  Michael  |           |           |           |           |
+-----------------------------------------------------------+
jotterbot
  • 1
  • 1
  • I assume all participants gather at the beginning and each participant is released as soon as its last meeting has finished. Do you want to reduce the total time spent by all participants until they are released? Then you should not optimize for maximal concurrency of meetings. What if those concurrent meetings all only bind a few participants, while the majority of participants is waiting for a single non-concurrent meeting scheduled afterwards? – le_m Mar 23 '17 at 22:58

1 Answers1

1

This definitely is an interesting problem. My approach is to use a matrix operation to figure out which Topics don't have a clash. The first step is to create a matrix to represent pairs that don't have a clash, similar to your approach.

So for this example data set

+-----------------------------------------------------------+
|  Meetings                                                 |
+-----------------------------------------------------------+
|  Topic A  |  Topic B  |  Topic C  |  Topic D  |  Topic E  |
+-----------------------------------------------------------+
|  Billy    |   JD      |  David    |   Tania   |   JB      |
|  Mel      |   Rowan   |  Emily    |   David   |   Fred    |
|  Tracey   |   Mike    |           |   Mike    |   Tania   |
|  JB       |   Aaron   |           |   Fred    |           |
|  Luke     |   Billy   |           |           |           |
|  Aaron    |           |           |           |           |
|  Michael  |           |           |           |           |
+-----------------------------------------------------------+

We can say Topic A doesn't have a clash with Topic C and Topic D, we can represent it in a matrix/array like so:

+---------------------------------------------------------------+
        |Topic A|Topic B |Topic C|Topic D|Topic E|
Topic A |1      |0       |1      |1      |0      |

However, Topic C and Topic D do have a clash, we will get to it in a minute. Similarly, we fill up the remaining matrix like so

Unique Pair Matrix
        TopicA TopicB TopicC TopicD TopicE
TopicA  1      0      1      1      0   
TopicB  0      1      1      0      1   
TopicC  1      1      1      0      1   
TopicD  1      0      0      1      0   
TopicE  0      1      1      0      1   

As you can note from this Table topic C row has 0 corresponding to Topic D column which signifies a clash. Also, note that the diagonal is set to 1 because same meetings cannot clash with each other (Also for the next step we need it to be 1).

Next just multiply the matrix by itself to give a matrix like the one below. Ideally, you would transpose the matrix and then multiply, in this case, you don't need to (square matrix)

Number of Unique Pair Matrix
        Topic A Topic B Topic C Topic D Topic E
Topic A 3       1       2       2       1
Topic B 1       3       3       0       3
Topic C 2       3       4       1       3
Topic D 2       0       1       2       0
Topic E 1       3       3       0       3

This matrix represents the number of times the meetings dont clash with the one other. So for meeting A and C, they don't clash twice, once when computed from row 1 and the other in row 3. Similarly, for Topic "D" and "A", we get a value 2. However total unique pair for A (represented in the diagonal value) is 3, this implies meeting C and D have a clash as they only form unique pair twice with A.

In row 2 you will notice, Topic B,C,E from a total of 3 unique pairs among themselves, this represents both the highest number of meetings that can take place together and which of those meetings can take place.

Using this you can prioritize the highest number of meetings to take place first followed by the remaining meeting. The below code does exactly that:

function onOpen () {
 var ui = SpreadsheetApp.getUi();
  ui.createMenu('Meeting Tools')
  .addItem('Compute Optimal Meeting Order', 'Jmain')
  .addToUi();
}
function Jmain() {
  var ss = SpreadsheetApp.getActive();
  var sss = ss.getActiveSheet();
  var sss = ss.getSheetByName("Sheet2");
  var data = sss.getRange(2,1,sss.getLastRow()-1,sss.getLastColumn()).getValues();
 //This the matrix that will hold data for unique pairs
  var shadowRel = []
  var headers = data[0].slice()
  data = transpose(data)
  var shadowRel = []
  for (var i = 0 ; i < data.length ; i ++){
    shadowRel[i] = headers.slice()                                            //Each Row is set Headers values to begin with
    for (var j = 1 ; j < data[i].length ; j++){
      var found = false
     for(var k = 0 ; k < data.length ; k++){

       if(k != i) {
         if (data[k].indexOf(data[i][j]) != -1 && data[i][j] != ""){
             //
            shadowRel[i][k] = 0                                                   // Whenver there is a clash the martix is set to zero
            found = true
         }  
       }
     }
    }
  }

 shadowRel = mMutliply (shadowRel)              //Matrix Multiplication
 sss = ss.getSheetByName("Sheet3")                
 sss.getRange(2,2,shadowRel.length,shadowRel[0].length).setValues(shadowRel)        //Set the values of multiplied martix in sheet 3
 var ui = SpreadsheetApp.getUi()

 var meetingOrder = mostNoMeetings(shadowRel,headers)
 Logger.log("Meeting Order: ")
 Logger.log(meetingOrder)
 var prompt = "Meeting Order: "
 for (i = 0 ;i <meetingOrder.length; i++){
 prompt += "\n"+(i+1)+") "+ meetingOrder[i]
 }
 ui.alert(prompt)
}

// Transpose the data so as to check match in each row. 
function transpose(arr) {
        return Object.keys(arr[0]).map(function (c) {
            return arr.map(function (r) {
                return r[c];
            });
        });
    }

function mMutliply (arr){                           //Number of row and column needs to be equal
  var mMutRes = []
  for (var i = 0; i < arr.length ; i++)
    for(var j = 0; j<arr[0].length ; j++){
      if(arr[i][j] !== 0)
         arr[i][j] = 1                    // Remaining Text is converted to 1, has no class

    }
  var ss = SpreadsheetApp.getActive()
  var sss = ss.getSheetByName("Sheet3")
  sss.getRange(arr.length+5,2,arr.length,arr[0].length).setValues(arr)  //Set Value of unique pair to sheet 3
  for (var i = 0; i < arr.length ; i++){
     mMutRes[i] = []
    for(var j = 0; j<arr[0].length ; j++){
      var sumProd = 0
      for(var k = 0 ; k < arr.length ; k ++)
      {
        sumProd += (arr[k][j] * arr[i][k])

      }
      mMutRes[i][j] = sumProd

    }
  }
  return mMutRes 
}

function mostNoMeetings(shadowRel,headers){
  var UsedIndex = []
  var MaxColIndex = []
  var counter = 0
  var MeetingGroups = []
  for(var maxNo = shadowRel.length ; maxNo > 0 ; maxNo--) {        //Look for most repeated pair, max possible is the numbers of topics/meetings at same time
  var maxFound = false
  for (var i = 0 ; i < shadowRel.length; i++){
    for(var j= 0; j< shadowRel[0].length; j++){
      if(i != j && UsedIndex.indexOf(i) == -1 && UsedIndex.indexOf(j) == -1)   // Eliminate row / column corresponding to topics/Meetings already scheduled. 
        if(shadowRel[i][j] == maxNo){
          MaxColIndex[counter] = j
          counter++
          maxFound = true

        }
    }
    if(maxFound){
    var temp =  []
    temp.push(headers[i])
    for (var k in MaxColIndex){
      if(temp.length < maxNo)
       temp = temp.concat(headers[MaxColIndex[k]])
      else
       MaxColIndex.splice(k)
    }
    MaxColIndex[counter] = i
    MeetingGroups.push(temp)
    UsedIndex = UsedIndex.concat(MaxColIndex)
    //Logger.log(UsedIndex)
    MaxColIndex = []
    counter = 0
    maxFound = false
    }
   }
  }
  // Incase any are remaining meetings that didnt form pairs which have to be held independently
  for ( i = 0 ; i < shadowRel.length; i++){
    if(i != j && UsedIndex.indexOf(i) == -1 && UsedIndex.indexOf(j) == -1){
     MeetingGroups.push(headers[i]) 
    }
  }

  return MeetingGroups
}

Lastly, make sure your data is in sheet 2 and sheet 3 is empty, output of the above mentioned matrix and multiplied matrix is posted to sheet 3 for a visual representation of what happens.

Hope this helps!

Jack Brown
  • 5,802
  • 2
  • 12
  • 27
  • Excellent @Jagannathan, thank you for your response which is clearly thought through and well explained! I have noticed with this solution when testing on historical meeting lists that it seems to miss opportunities for maximum concurrent meetings, instead favouring pairs. I had tested this and it will have ordered 2,2,1 instead of 3,2 for example. I'll update my question to reflect that scenario... – jotterbot Mar 19 '17 at 21:57