1

Overview I'm constructing a script to assign individuals to groups based on first-come-first-served ranked preference input data. The script constructs a response order from the data array using two For Loops, selecting student1, and then choice 1, 2, 3. It then checks to see which group matches the student's first choice, and if that group is below current capacity, it will copy student1 to that group and move onto student2 choice 1, 2, 3.

Issue ..The need to include a i++ loop iteration trigger (line 46) - my understanding is that after meeting all of the required criteria for the loop, it would iterate by itself. Removal of i++ at line 46 breaks the script.

A consequence of having this iteration trigger is that at i = respCount (number of student responses), after assigning the last student, the i++ will still iterate the counter, and then cause

"TypeError: Cannot read property '0' of undefined"

as it tries to read outside of the bounds of the responses array.

What I've Tried In order to prevent this error, I have added a check to see if the response array is undefined (line 21), however this merely hides the problem.

I am new to coding, and desire to write clear and maintainable code.

Example Spreadsheet: https://docs.google.com/spreadsheets/d/1pR8r2cjapY6YAbW5Yi3tY0nio-u9JLkRjG5JxV8S3fA/edit?usp=sharing

function assignGroups() {
  const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data") //Select the sheet in the document our data is located on

  /** Obtain Data Range */
  //https://stackoverflow.com/questions/17632165/determining-the-last-row-in-a-single-column
  const respCount = ss.getRange("A2:A").getValues().filter(String).length //Number of responses to parse
  const responses = ss.getRange("A2:D"+(respCount+1)).getValues() //All students & responses as an array. (+1) gets accurate range size due to header row
  const choices = 3 //Maximum number of preferences a scholar can declare. Must be edited to suit actual data

  //Logger.log(respCount)
  //Logger.log(responses.length)

  /** Obtain Department Information */
  const deptCount = ss.getRange("F2:F").getValues().filter(String).length //Count how many unique departments there are in this range
  const departments = ss.getRange("F2:G"+(deptCount+1)).getValues() //Then, grab each department and its associated capacity as an array. +1 because of header rows

  /** Construct Response Parse Order */
  for (var i = 0; i < respCount; i++){ //Iterate through each row in the responses array, starting at row [0]
    for (var j = 1; j <= choices; j++){ //Iterate through each column in the responses array, starting at column [k][1] and ending at column [k][choices]
      
      if (responses[i] == undefined){ //Before declaring the next few variables, check and see if the counter is within the range of the array
        break; //Break the script if i=respCount before declaring the next variables, and terminate the script
        //This is only needed because I have to manually iterate the script following the nested ifs towards the end of the file
        //If this wasn't here, studentName[i] would look outside of array bounds and throw "TypeError: Cannot read property '0' of undefined"
      }
      
      var studentName = responses[i][0] //Gets student name from position row i column 1
      var studentChoice = responses[i][j] //Gets each student choice from row i column 2 through the upper bound denoted by var choices
      //Logger.log(studentChoice+"_"+studentName)

  /** Construct a List of Department Names and Capacities */
      for (var k = 0; k < deptCount; k++){ //Iterate through each row in the departments array, starting at row [0]
        var depID = departments[k][0] //Gets department ID from position row k column 1
        var depSize = departments[k][1] //Gets department capacity from position row k column 2
        //Logger.log(depID+"_"+depSize)

  /** Do Some Assignment Logic */
        if (studentChoice == depID){ //If the student choice matches the department ID...
          var depSpace = ss.getRange("I2:I").offset(0,k).getValues().filter(String).length 
          //Get the first column in the department assignment area
          //Offset by the department number given by k
          //Count how many students are currently assigned to that department

          if (depSpace < depSize){ //If the number of students currently assigned to the department is less than its capacity...
            ss.getRange("I2").offset(depSpace,k).setValue(studentName); //Copy the current student's name to that department column...
            i++;//Move on to next Student || THIS IS CAUSING ISSUES - why does this series not loop by itself without me iterating it?
            j=0;//And reset Choice counter j back to choice 1
          }
        }
      }
    }
  }
}

Edit: Shape of data array

Names Pref1 Pref2 Pref3
Zi Yang R T E
Reeva Dalton T Q E
Keagan Mcguire T Y R
Randy Robins E R W

Edit 2 - Thank you for all of your helpful responses. Error was caused by not breaking out of the nested if.

Edit 3 - Updated code as of 07/02/2022 15:23

function assignDepartment() {
  const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Data")

  /** Obtain Data Range */
  const responses = ss.getRange("A2:D").getValues().filter(row => row[0] !== "");
  const respCount = responses.length
  const choices = 3

  /** Obtain Department Information */
  const departments = ss.getRange("F2:G").getValues().filter(row => row[0] !== "");
  const deptCount = departments.length
  
  /** Construct Response Parse Order */
  for (let i = 0; i < respCount; i++) {
    for (let j = 1; j <= choices; j++) {
      var studentName = responses[i][0]
      var studentChoice = responses[i][j]

  /** Construct a List of Department Names and Capacities */
      for (let k = 0; k < deptCount; k++) {
        var depID = departments[k][0]
        var depSize = departments[k][1]

  /** Assignment Logic */
        if (studentChoice == depID) {
          var depSpace = ss.getRange("I2:I").offset(0,k).getValues().filter(String).length 

          if (depSpace < depSize) {
            ss.getRange("I2").offset(depSpace,k).setValue(studentName);
            j = choices+1;
            break;
          }
        }
      }
    }
  }
}
TheMaster
  • 45,448
  • 6
  • 62
  • 85
mss_
  • 41
  • 6
  • [Tables](https://webapps.stackexchange.com/a/161855/) are a better alternative than spreadsheets to show your data structure. Questions asked here should be self contained. If you share spreadsheets, make sure to also add images of your sheet. [Your email address can also be accessed by the public](https://meta.stackoverflow.com/questions/394304/), when you share Google files. – TheMaster Jul 02 '22 at 18:26
  • 1
    this doesn't make much sense `if (responses[i] == undefined){` becaause responses[0] is an array of 4 cells. It will be defined even if all four of the values are not. – Cooper Jul 02 '22 at 18:44
  • You have 3 nested loops, i, j, k but are incrementing `i` in the `k` loop so it could exceed the size of the array `response[i]` in the `j` loop. Since you can only `break` out of the inner most loop `k` loop you need to set a test to break out of the `j` loop as well. Or change everything to while loops. – TheWizEd Jul 02 '22 at 19:17
  • @TheMaster The objective here is to determine how many responses there are. The .filter(string) function returns how many cells in the range have data in them, including the header. To get all of the cells that have *responses*, I count how many cells have data, subtract 1 to avoid the header row, and use this number to determine the size of the responses array. Ex (A2:A(100-1) How would you approach this? – mss_ Jul 02 '22 at 20:35
  • Ok, what's wrong with just `sheet.getLastRow()`? – TheMaster Jul 02 '22 at 20:44
  • @TheMaster Getlastrow returns the last row with data in it for the sheet, not the specific range. If the data range is A1:D150, and there is a cell with data in it for G250, getLastRow returns 250. Alternatively, SpreadsheetApp.getActive().getRange("A:A").getLastRow() returns the last row of the entire sheet. – mss_ Jul 02 '22 at 20:48

1 Answers1

1

I'm not sure if this will entirely solve your problem because your code is hard to follow but replace this:

if (depSpace < depSize){ //If the number of students currently assigned to the department is less than its capacity...
  ss.getRange("I2").offset(depSpace,k).setValue(studentName); //Copy the current student's name to that department column...
  i++;//Move on to next Student || THIS IS CAUSING ISSUES - why does this series not loop by itself without me iterating it?
  j=0;//And reset Choice counter j back to choice 1
}

with this:

if (depSpace < depSize){ //If the number of students currently assigned to the department is less than its capacity...
  ss.getRange("I2").offset(depSpace,k).setValue(studentName); //Copy the current student's name to that department column...
  j = choices+1;  // will terminate j loop and continue i loop
  break;  // break out of k loop
}

And eliminate this altogther:

if (responses[i] == undefined){ //Before declaring the next few variables, check and see if the counter is within the range of the array
  break; //Break the script if i=respCount before declaring the next variables, and terminate the script
  //This is only needed because I have to manually iterate the script following the nested ifs towards the end of the file
  //If this wasn't here, studentName[i] would look outside of array bounds and throw "TypeError: Cannot read property '0' of undefined"
}

Regarding discussion of getting the last row.

The method shown in the OP is not safe. It does not guarantee getting the proper number of rows from another range. And it takes 2 getValues() calls, and can be up to 1000 or more empty rows long.

Assuming we have the following spreadsheet.

enter image description here

function test() {
  try {
    let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
    const respCount = sheet.getRange("A1:A").getValues().filter(String).length //Number of responses to parse
    const responses = sheet.getRange("A1:D"+(respCount+1)).getValues() //All students & responses as an array. (+1) gets accurate range size due to header row
    console.log(respCount);
    console.log(responses);
  }
  catch(err) {
    console.log(err);
  }
}

2:10:50 PM  Notice  Execution started
2:10:51 PM  Info    5
2:10:51 PM  Info    [ [ 'A', 1, '', '' ],
  [ 'B', 2, '', '' ],
  [ 'C', 3, '', '' ],
  [ '', 4, '', '' ],
  [ '', 5, '', '' ],
  [ 'D', 6, '', '' ] ]
2:10:51 PM  Notice  Execution completed

Prefered method IMHO. It makes one call to get all values and filters out rows starting with blank. I don't need 2 arrays to work with. It also get only rows that contain data.

function test() {
  try {
    let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
    let responses = sheet.getDataRange().getValues();
    responses = responses.filter( row => row[0] !== "" );
    console.log(responses);
  }
  catch(err) {
    console.log(err);
  }
}

2:12:23 PM  Notice  Execution started
2:12:24 PM  Info    [ [ 'A', 1 ],
 [ 'B', 2 ],
 [ 'C', 3 ],
 [ 'D', 6 ],
 [ 'E', 7 ] ]
2:12:24 PM  Notice  Execution completed
TheWizEd
  • 7,517
  • 2
  • 11
  • 19
  • This works. Regarding your feedback about the code being hard to follow, what steps can I take to ensure that it is understandable? – mss_ Jul 02 '22 at 20:13
  • I've edited my answer to show what I consider a flaw in the way you are getting the rows containing data. Also 3 nested loops are difficult to debug. I would break into smaller parts and using the results of one loop on it to get another set of results. – TheWizEd Jul 02 '22 at 21:21
  • Thank you - I've implemented your feedback regarding finding the size of arrays. I am unclear about what you mean by "using the results of one loop on it to get another set of results". Could you please elaborate on your thought process? – mss_ Jul 02 '22 at 21:45
  • As I said, i'm not sure what your code is doing, so I can't comment on how I would do it. – TheWizEd Jul 02 '22 at 22:16
  • I would have hoped that the line by line documentation would have been sufficient, but if you don't understand it, that's a failure on my part. Objective - assign individuals to groups based on first come first serve (the order you appear in the array) and by ranked preference (Choice 1,2,3). Each group has a capacity that must not be exceeded Procedure: Grab an array of all students and responses, select student 1, select choice 1 --> goto depts array and check to see if the department title matches choice 1. If yes is the dept full? Dept is not full, copy the student to that dept. – mss_ Jul 02 '22 at 22:21
  • I have added my most recent version of the script to the main comment – mss_ Jul 02 '22 at 22:24