1

I'm brand new to App Script, so please forgive my ignorance.

The Google sheet I use to hold student data is so long and unwieldy (50+ columns) that I decided to create another sheet to act as a front-end for data entry. Through hours of tutorial videos + bumbling trial and error, I've come up with a working script that takes values from my data entry form-like sheet ('Students') and passes those values to the first empty row in my destination/container sheet ('Master').

I'm really pleased with how the script working - except for the fact that it is ridiculously slow. Based on what I've read, I think I'm making too many calls to the Sheets API, and I need to figure out how to pass all the values from 'Students' to 'Master' en masse rather than one-by-one, but I don't have the skills to do that, and I can't seem to find an example.

I'm sure there's a really simple, elegant solution. Can anyone help?

Here's a little piece of my code (hopefully it's enough to see the inefficient strategy I'm using):

function submitStudentData(){
var caseloadManager = SpreadsheetApp.getActiveSpreadsheet();
var enterStudents = caseloadManager.getSheetByName('Students');
var masterSheet = caseloadManager.getSheetByName('Master');
var clearFields = enterStudents.getRangeList(['C6:C18', 'C22', 'E6:E18','G6:G14','G20','I6:I14','K6:K16', 'M6:M18']);
var blankRow = masterSheet.getLastRow()+1;

  masterSheet.getRange(blankRow,1).setValue(enterStudents.getRange("Z1").getValue()); //Concatenated Student Name
  masterSheet.getRange(blankRow,3).setValue(enterStudents.getRange("C6").getValue()); //First Name
  masterSheet.getRange(blankRow,2).setValue(enterStudents.getRange("C8").getValue()); //Last Name
  masterSheet.getRange(blankRow,4).setValue(enterStudents.getRange("C10").getValue()); //Goes By
  masterSheet.getRange(blankRow,6).setValue(enterStudents.getRange("E6").getValue()); //DOB
  masterSheet.getRange(blankRow,7).setValue(enterStudents.getRange("E8").getValue()); //Grade
  masterSheet.getRange(blankRow,5).setValue(enterStudents.getRange("E10").getValue()); //Student ID
  masterSheet.getRange(blankRow,10).setValue(enterStudents.getRange("E14").getValue()); //Last FIE
  masterSheet.getRange(blankRow,11).setValue(enterStudents.getRange("Z2").getValue()); //Calculated FIE Due Date
  masterSheet.getRange(blankRow,8).setValue(enterStudents.getRange("E12").getValue()); //Last Annual Date[enter image description here][1]
  masterSheet.getRange(blankRow,13).setValue(enterStudents.getRange("G6").getValue()); //PD
  masterSheet.getRange(blankRow,14).setValue(enterStudents.getRange("G8").getValue()); //SD
  masterSheet.getRange(blankRow,15).setValue(enterStudents.getRange("G10").getValue()); //TD
  masterSheet.getRange(blankRow,16).setValue(enterStudents.getRange("G3").getValue()); //Concatenated Disabilities
  masterSheet.getRange(blankRow,18).setValue(enterStudents.getRange("G12").getValue()); //Program Type
  masterSheet.getRange(blankRow,12).setValue(enterStudents.getRange("G14").getValue()); //Evaluation Status
  masterSheet.getRange(blankRow,20).setValue(enterStudents.getRange("I6").getValue()); //DYS
  masterSheet.getRange(blankRow,21).setValue(enterStudents.getRange("I8").getValue()); //GT
   masterSheet.getRange(blankRow,19).setValue(enterStudents.getRange("I10").getValue()); //EB
  masterSheet.getRange(blankRow,24).setValue(enterStudents.getRange("I12").getValue()); //ESY
  masterSheet.getRange(blankRow,22).setValue(enterStudents.getRange("I14").getValue()); //BIP
  masterSheet.getRange(blankRow,29).setValue(enterStudents.getRange("K6").getValue()); //TR
  masterSheet.getRange(blankRow,30).setValue(enterStudents.getRange("K8").getValue()); //OT

It goes on and one like this for 52 values before clearing all the fields in 'Students.' It works, but it takes well over a minute to run.

I'm trying to attach a picture of my 'Students' form-like sheet in case my description isn't clear.

Thanks so much for helping a humble special educator who knows not what she's doing. :)

Image of 'Students' form/sheet

DAYNA
  • 13
  • 2

3 Answers3

1

Read best practices Even though your data isn't a contiguous range it is part of one so get the whole range with getValues() and use the appropriate indices to access the ones that you want. In the end if will be much faster. You may not want to use setValues to write the data because of other issues like messing up formulas. Avoid the use of setValue() and getValue() whenever possible

function submitStudentData() {
  const ss = SpreadsheetApp.getActive();
  const ssh = ss.getSheetByName('Students');
  const msh = ss.getSheetByName('Master');
  const nr = msh.getLastRow() + 1;
  const vs = ssh.getRange(nr, 1, ssh.getLastRow(), ssh.getLastColumn()).getValues();
  let oA1 = [[vs[0][25], vs[7][2], vs[5][2], vs[9][2], vs[9][4], vs[5][4], vs[7][4], vs[11][4]]];
  msh.getRange(msh.getLastRow() + 1, 1, oA1.length, oA[0].length).setValues(oA1);//This line replaces all of the below lines

  msh.getRange(nr, 1).setValue(vs[0][25]);//Concatenated Student Name
  msh.getRange(nr, 2).setValue(vs[7][2]); //Last Name
  msh.getRange(nr, 3).setValue(vs[5][2]); //First Name
  msh.getRange(nr, 4).setValue(vs[9][2]); //Goes By
  msh.getRange(nr, 5).setValue(vs[9][4]); //Student ID
  msh.getRange(nr, 6).setValue(vs[5][4]); //DOB
  msh.getRange(nr, 7).setValue(vs[7][4]); //Grade
  msh.getRange(nr, 8).setValue(vs[11][4]); //Last Annual Date[enter image description here][1]

You could also do a similar thing by using formulas to map all of the data into a single line or column making it much easier to run the scripts.

Cooper
  • 59,616
  • 6
  • 23
  • 54
  • Thank you, Cooper! I think I understand the approach you're showing here, but I'm getting an error that says "oA1 is not defined." When I google oA1, the results all pertain to ocular albanism. I'm not familiar with oA1, but I assumed that it's kind of a placeholder meaning the "nth" row and the vs[] notation order items from the array. I dunno. – DAYNA Aug 15 '21 at 13:46
  • I think it just dawned on me what you mean "by using formulas to map data into a single line". I am actually using some hidden cells (Z1:Z9) to operate on values entered into 'Students' and then passing those calculated values to 'Master' with my super inefficient script. Are you saying that I could assemble all the cells into a hidden row and than pass that row to the master...maybe with appendRow (like John shows below)? You've been so helpful! Thank you! – DAYNA Aug 15 '21 at 13:58
  • You could use appendRow() or setValues(). Whenever I work with clients that must have their data entry a certain way I always require them to build a page that reads the data from all of the locations distributed throughout the form and puts them on a sheet that I can read one time with a getValues(). It makes my life easier and basicly that's all I care about. – Cooper Aug 15 '21 at 14:46
0

As Cooper said you want to avoid reading and writing to the sheet(s) as much as possible. (I had the same issue when I started with Google Script)

This means that you should read the whole range into a variable and then write your rows out to the master sheet.

Below is an example of what you could use to avoid the setValue() and getValue() slowness you are experiencing

function submitStudentData(){
var caseloadManager = SpreadsheetApp.getActiveSpreadsheet();
var enterStudents = caseloadManager.getSheetByName('Students');
var masterSheet = caseloadManager.getSheetByName('Master');
var clearFields = enterStudents.getRangeList(['C6:C18', 'C22', 'E6:E18','G6:G14','G20','I6:I14','K6:K16', 'M6:M18']);
var blankRow = masterSheet.getLastRow()+1; //You will not need this


//First we will all the data from the students sheet. This will make and array of arrays [[row],[row],[row]]. 
studentData = enterStudents.getRange(1,1,enterStudents.getLastRow(),enterStudents.getLastColumn()).getValues()
Logger.log(studentData)
//We are going to build an array of arrays of the data that we want to write back to the master sheet. We will start by creating our first array
writeData = []
//Then we loop through all the student data
for (var i = 0; i < studentData.length; i++) { 
  Logger.log(studentData[i][0])
  //We are selecting data from each row to add to our array. in "studentData[i][0]" the [0] is the column number (remember we are starting with 0)
  rowData = []
  rowData.push(studentData[i][0])
  rowData.push(studentData[i][2])
  rowData.push(studentData[i][1])
  //Then we send the full row to the first array we made
  writeData.push(rowData)

}


Logger.log(writeData)
// Now to write out the data. Normally it would not be a good idea to loop a write like this but this as an atomic operation that google will automatically batch write to the sheet. 
for (var i = 0; i < writeData.length; i++) { 
  masterSheet.appendRow(writeData[i])
}

}

Hope this helps get you started.

John Wells
  • 66
  • 3
  • Thanks, John! I used this code (even though I don't fully understand it *blush*), and it's very zippy. I don't get any errors and the ui.Alert (which I didn't share in my original post) confirms the save at the end of the script. The only trouble is that no data is appended to my Master sheet. I'm assuming I need to work on the rowData section. I'm struggling to understand how this script identifies/disregards the relevant (non-empty) cells in the enterStudents range and how it would order those cells to form a row consistent with the order of the information in my Master sheet. – DAYNA Aug 15 '21 at 14:08
  • I'm going to keep plugging away at it, but if it's obvious to you where I've gone wrong, I'd definitely welcome any pointers! Thanks so much for your help! – DAYNA Aug 15 '21 at 14:08
0

Here is the working example. Just complete the mapping array as desrbied in the code. The runtime is below 1 second.

const mapping= [
  // enter the array [ sourceRange, destinationRow ] for each cell you want to copy form Students to Master
  ['Z1',1],
  ['C6',3],
  ['C8',2],
  ['C10',4],
  ['E6',6]
  // ... and so on
]

function submitStudentData() {
  console.time('submitStudentData')
  const caseloadManager = SpreadsheetApp.getActive();
  const enterStudents = caseloadManager.getSheetByName('Students');
  const masterSheet = caseloadManager.getSheetByName('Master');
  const data = enterStudents.getDataRange().getValues()
  const destRow = []

  mapping.forEach((m,i)=>{
    [rowi,coli] = rangeToRCindex(m[0])
    const destRowIndex = m[1] - 1
    destRow[destRowIndex] = data[rowi][coli]
  })

  masterSheet.appendRow(destRow)
  console.timeEnd('submitStudentData')
}

function rangeToRCindex(range){
  const match = range.match(/^([A-Z]+)(\d+)$/)
  if (!match) {
    throw new Error(`invalid range ${range}`)
  }
  const col = letterToColumn(match[1])
  const row = match[2]
  return [row-1,col-1]
}

function letterToColumn(columnLetters) {
  let cl = columnLetters.toUpperCase()
  let col = 0
  for (let i = 0; i < cl.length; i++) {
    col *= 26
    col += cl.charCodeAt(i) - 65 + 1
  }
  return col
}
GoranK
  • 1,628
  • 2
  • 12
  • 22
  • @DAYNA, have you tested the proposed solution? – GoranK Aug 20 '21 at 17:29
  • Sorry, Goran! I've had such a busy week that I haven't had as much time to play with spreadsheet as I would have liked. I tried many other solutions first (because I thought I understood them better than yours), but ultimately, yours was the one that worked...flawlessly and speedily. *chef's kiss* I only wish I understood WHY. Maybe I'll get there someday. Thanks so much for helping me out!!! – DAYNA Aug 21 '21 at 01:32
  • thanks! Let me know if you have any questions about the code – GoranK Aug 21 '21 at 21:21