Complex transpose exceeding run time in Google Apps Script - google-apps-script

I am receiving data in a single column and must transpose that into individual records. Some records will be 12 characters long, others 10, and the remainder 9. Furthermore, the latter 2 values in the 10 and 9-character-long records must be shifted 1 and 2 fields to the right, respectively. The first value in a given record is always a date. I have created the following code which works well, except that it times out after about 6 minutes and 77 records. I need to be able to handle 15 times as many if not more.
I embedded the calculation of the date objects in the else section of each if statement and nested the subsequent if statements in an effort to reduce unnecessary calculations. This got me from about 48 records to 77.
Very grateful for any clever insight 🙂
function transposeNew(){
let ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
let lr = ss.getRange("A13").getDataRegion().getLastRow();
let sr = 13
// get the data column
let data = ss.getRange(sr,1,lr-sr,1).getValues();
// set up the rows loop
let pasteRow = 2;
let arrayField = 0;
while (arrayField < data.length){
//use the new Date() constructor to create a date object with the date value passed
let isDate12 = new Date(data[arrayField+12]).getFullYear(); //processed; size input;record should include 12 rows & 13th should be a date to begin the next row
if (isDate12 === 2020) {
let record = data.slice(arrayField, arrayField+12);
let recordTr = transposeSub(record);
ss.getRange(pasteRow, 5, 1, 12).setValues(recordTr);
arrayField = arrayField + 12;
}
else {
let isDate10 = new Date(data[arrayField+10]).getFullYear(); //unprocessed;size input
if (isDate10 === 2020) {
let record = data.slice(arrayField, arrayField+10);
let record1 = record.slice(0,8);
let record1Tr = transposeSub(record1);
let record2 = record.slice(8,10);
let record2Tr = transposeSub(record2);
ss.getRange(pasteRow, 5, 1, 8).setValues(record1Tr);
ss.getRange(pasteRow, 14, 1, 2).setValues(record2Tr);
arrayField = arrayField + 10;
}
else {
let isDate9 = new Date(data[arrayField+9]).getFullYear(); //unprocessed;no size
input
if (isDate9 === 2020) {
let record = data.slice(arrayField, arrayField+10);
let record1 = record.slice(0,7);
let record1Tr = transposeSub(record1);
let record2 = record.slice(7,9);
let record2Tr = transposeSub(record2);
ss.getRange(pasteRow, 5, 1, 7).setValues(record1Tr);
ss.getRange(pasteRow, 14, 1, 2).setValues(record2Tr);
arrayField = arrayField + 9;
}
}
}
pasteRow ++;
}
}
function transposeSub(a)
{
return Object.keys(a[0]).map(function (c) { return a.map(function (r) { return r[c]; }); });
}

I see you create a loop and inside this while statement you call several times to SpreadsheetApp functions. This creates a connection to the spreadsheet, reads/changes its data and close connection a lot of times, that's why your code is taking too long to run. Please check GAS Best Practices batch operations section.
You should consider banishing any get/setValue() inside while, instead, call getValues() to hold all values in a javascript array before while and then use setValues() after while to write all outputs at once. The described concept is explored in this answer.

So it turns out that the problem was a flaw in the loop criteria; rookie mistake. There was a data anomaly such that one record did not meet the criteria in any of the if statements and so the loop was continuing in perpetuity. I discovered this by inserting the values for pasteRow and arrayField next to each record on the sheet so that I could see where it was breaking. Interestingly, the records stopped, but the pasteRow and arrayField values continued into the 20,000s before the app quit.
I do note that the feedback provided by #Bruno Polo and #Cooper are correct. Shortly after posting this I reworked it to push the records into a new array and paste the array once complete. That failed due to the same reason noted above. I think I will go back to that version now that I understand the problem.
Thank you for looking at this with me. This is an extraordinary community of experts from whom I have learned so much! 😁

Related

Better/faster way to pass 50+ values from one Google sheet to another

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
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.
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
}
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.

Conversion of a Google Form ItemType via Apps Script

I have a form on Google Forms for selling stuff. I know it's not the best solution for this, but my new customer is using it already, so It's not worthy to change now, we are planning to do this further.
What the code does now?
I have set a sheet where I have stored info about the products, like name, price and available qty. When someone submits a response in the form, the script takes the order from that guy and updates the sheet AND the "front-end" of the form. The form is composed by ScaleItem questions, from 0 to 6, unless the available qty is below six. When this happens, the code changes the bounds of the scale so it's not possible to have overbooking.
The problem is:
When the available stock is 1 or 2, I need to change the ItemType, because the ScaleItem requires to have at least 4 numbers (0 to 3).
I've looked on documentation, google and even here and found no solution for switching the ItemType.
This is the code I have so far, AS IS. I'm trying to substitute the item, but it's not good solution, because it creates a new column in the form response sheet.
var form = FormApp.openById(form_id);
var item = form.getItemById(item_id);
var choices = [];
for (var p = 0; p <= stock; p++) {
choices.push(p);
}
if (stock <= 2) {
if (item.getType() === FormApp.ItemType.MULTIPLE_CHOICE) {
item.asMultipleChoiceItem().setChoiceValues(choices);
} else if (item.getType() === FormApp.ItemType.SCALE) {
//
// THIS IS WHERE THE ITEM SHOULD GET CONVERTED TO MULTIPLE CHOICE
// it is provisionally creating a new item
//
// item.asMultipleChoiceItem().setChoiceValues(choices); <= NOT VALID CONVERSION ERROR
var ii = item.getIndex();
var title = item.getTitle();
form.deleteItem(item);
var new_item = form.addMultipleChoiceItem()
.setTitle(title)
.setChoiceValues(choices);
form.moveItem(new_item, ii); // this line gets error - still working on
}
} else if (stock <= 6) {
item.asScaleItem().setBounds(0, stock);
} else {
item.asScaleItem().setBounds(0, 6)
}
Appreciate any help.
It is not possible to convert a FormApp.ItemType
If it helps, the minimum required number of options for ScaleItem is two and not four (1 to 2).
Otherwise, instead of converting, why do you not declare the item as a different type in the first place? If multiple choice is too bulky for large numbers, maybe you can set the ItemType to List?

How to print variables in google script

function checkWho(n,b)
{
// n and be are comparing two different cells to check if the name is in the registry
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var glr = sheet.getLastColumn();
var glr2 = sheet.getLastRow();
for(var i = 9; i <= glr; i++)
{
for(var z = 10; z<= glr2; z++)
{
if( n == b)
{
var courts = sheet.getRange(3,i).getValue();
var times = sheet.getRange(z,10).getValue();
return(b+ " "+"has booked"+" "+ courts+" "+"at"+times);
}
}
}
}
I am having issues printing out the values contained in var courts and var times. My code consists of two for loops iterating through columns and rows and eventually spitting out the users name, what court they've booked and at what time. As of now the name gets printed, but the courts and the times don't.
it currently prints: "(name) has booked at"
When I want it to print:" (name) has booked court 1 at 4:30"
Any help on the situation?
What is happening is that the the nested for statements are overwriting the result. It's very likely that the court and time are "" (empty strings) because the iteration is done from a start column/row and repeated for the next columns/rows. It's very common that the last column/rows are empty.
Side notes:
The script include a comment mentioning that custom function arguments are cells but custom function can't use cells as argument in the same sense of getCurrentCell(). Custom functions arguments types could be String, Number, Date or Array.
It doesn't make sense to compare the arguments inside the nested for statements as they doesn't change on each iteration.
Including a return inside the nested for statement will stop the iterations. As the arguments are not iteration dependent, only the first iteration is made for the case considered in the question.
If you return a string and your matter is to print those variables, then replace your return statement like this.(same as java script ES6 )
return(`${b} has booked ${courts} at ${times}`);
App script is grooming scripting language. My suggestion is working properly now.

If statement in for loop referencing incremented variable

as quite an novice when it comes to coding, I ran into this problem with my code:
I use Google App Script [Edit: Corrected Google App Engine to Google App Script] to go through a list of timestamps and filter for stamps that equal the current month. Therefor I load the spreadsheet, the according sheet and get the data from all the rows as an object.
In the next step I go though all the elements of the object and check whether they contain the current date.
/* Initial data */
var email = "name#domain.com";
var spreadsheet = SpreadsheetApp.openById("1bN7PTOa6PwryVvcGxzDxuNVkeZMRwYKAGFnQvxJ_0nU");
var tasklist = spreadsheet.getSheets()[0].getDataRange();
var tasks = tasklist.getValues();
var tasksnum = tasklist.getNumRows();
Logger.log(tasks[7][2]); //Console returns "01.12.2014"
Logger.log(tasks[7][2].indexOf(month)); //Console returns "12.2014"
/* Filter tasks by month */
for (var i = 1; i < 9; i++) {
if (tasks[i][2].indexOf(month) >= 0) {
Logger.log(tasks[i]);
}
else {
return;
}
}
What drives me crazy is the following: As stated above, the for loop doesn't work. But if I alter it like this
if (tasks[7][2].indexOf(month) >= o) {
it works like a charm. And that's what I don't get. i should be incremented til 9 so should be seven at some point. At least then, the condition should be true and the loop should return a log.
What am I missing?
Thank you all in advance.
ps: If I am just following the wrong path of how to implement the function, please let me know.
ps2: I think my question's title is a bit cryptic. If you have a better one in mind, I'd love to change it.
The reason your original code doesn't work is due to the return statement in your else block. Return exits the function immediately, so your loop never gets past the first "false" of your IF.
remove the "return" (or the else block entirely) and your code should work.
I'm not sure what's supposed to be in the month variable but I suggested building a string to match your date format and doing string comparisons instead of the indexOf() strategy you're trying. This code is tested and does just that:
function test() {
/* Initial data */
var spreadsheet = SpreadsheetApp.openById("");
var tasklist = spreadsheet.getSheets()[0].getDataRange();
var tasks = tasklist.getValues();
var tasksnum = tasklist.getNumRows();
/* Filter tasks by month */
var today = Utilities.formatDate(
new Date(),
spreadsheet.getSpreadsheetTimeZone(),
"dd.MM.yyyy"
);
for(var i=1; i<9; i++){
if (tasks[i][2] == today){
Logger.log(tasks[i]);
}
}
}

Guild Roster Sheet - Setting value row-by-row in one column

I'm just starting to learn how to code with Google Apps Script. Barely.
I have a guild roster sheet (toon names in range B2:B and Realm (We also add prospects to this list) in range C2:C.
In Column D, I would like to set the value of each cell row-by-row to a function that uses the data in B and C (i.e. "=wowi(B2,C2)")
My intent is to just be able to add as many names as I want in Column B, and run the function manually to update the values for each in column D, and sleeps for a second between each (so as to not abuse the UrlFetch in the "=wowi(toonName, realmName)" function when the names column gets a little long)
Sadly, this is about as far as I got before I gave up:
function rosterUpdate() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var destCell = sheet.getRange("D2");
for (x = 0, x //(less than or equal to)// sheet.getRange("B2:B").getNumRows(), x ++) {
if (x != 0 {
NOCLUEWHATTODO-IDKWHYIEVENTRY;
sleep(1000);
}
}
}
I know I need to use a for, but not quite sure on how to go about reading the value and modifying the D cell's value. I'm quite possibly overthinking this, as it's currently 5:30 in the morning and I'm running on about 2 hours of sleep, but would really appreciate the help.
Thanks in advance. Sorry if I'm bad. :[
Hopefully this answers your question in terms of getting and setting values (you should check the documentation: https://developers.google.com/apps-script/reference/spreadsheet/range) and how to sleep for one second between function calls, however it's pretty inefficient as it will iterate over the whole list each time.
Please be aware this is written off the top of my head very quickly so you may need to play with the i values to get it working properly, and there is probably a much, much cleaner way to do it!
var listLength = sheet.getLastRow() - 1;
for(var i = 0; i < listLength; i++){
var toonName = sheet.getRange("B" + (i+2)).getValue();
var realmName = sheet.getRange("C" + (i+2)).getValue();
sheet.getRange("D" + (i+2)).setValue(yourFunction(toonName, realmName));
Utilities.sleep(1000);
}