Google Script Function Timing - google-apps-script

I am struggling to find the cause of the "mis-timing" of the following two function calls. I am attempting to have google look for a csv file in a particular gmail folder and insert the data into a google sheet. That works fine. Then, once the data is in the sheet, I call "UpdateComplete" whose sole purpose is to sort the sheet by column K (where there is a vlookup function that looks at a sheet with completed rows that have been moved there) so that rows that have the vlookup function already are sort to the top, and it then copies the formula into the rows that are new and don't already have it. However, if the google sheet has, say, 2000 rows, and the csv file contains 2100, for some reason the new 100 rows are being added after the call to UpdateComplete. So the new 100 rows are added, but they do not get the vlookup like all of the other rows. This issue only happens when the google sheet does not have enough rows, initially, for the csv data.
If, however, I comment out the call to "UpdateComplete" from within "RetrieveAwardData", and manually run that first, and then manually run "UpdateComplete", it works perfectly. I have tried adding a Utilities.Sleep call before the call to "UpdateComplete" (but after the csv setvalues line), in case it was a timing thing, but when I do that, the system waits that amount of time before adding the 100 new rows, even though the line for sleep comes after the line to add the csv data. I also tried creating a new function that calls "RetrieveAwardData" first (with the UpdateComplete call commented out) and then calls UpdateComplete 2nd, but the same issue happens. Why does it work properly if I run them, separately, manually, but not one after the other programmatically?
function RetrieveAwardData(){
var threads = GmailApp.search('is:unread subject:VA Benefit Aid');
var message = GmailApp.getMessagesForThreads(threads); //retrieve all messages in the specified threads.
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('AwardData');
if(message[0] != null){
Logger.log(message[0]);
for (var i = 0 ; i < message.length; i++) {
for (var j = 0; j < message[i].length; j++) {
var attachment = message[i][j].getAttachments()[0];
var csvData = Utilities.parseCsv(attachment.getDataAsString('ISO-8859-1'), ",");
sheet.getRange(1, 1, csvData.length, csvData[0].length).setValues(csvData);
UpdateComplete();
GmailApp.markMessageRead(message[i][j]);
}
}
}
else{Logger.log("No file available.");}
}
function UpdateComplete(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('AwardData');
sheet.sort(11);
var LastAwardRow = sheet.getLastRow();
var Avals = ss.getRange("K1:K").getValues();
var LastCompleteRow = Avals.filter(String).length;
if(LastAwardRow != LastCompleteRow){
sheet.getRange("K"+(LastCompleteRow+1) + ":K" + LastAwardRow).setFormulaR1C1(sheet.getRange("K"+LastCompleteRow).getFormulaR1C1());
}
}

Posting this for documentation purposes.
As mentioned by Cooper, use SpreadsheetApp.flush().
This method ensures that later parts of the script work with updated data, since changes made by previous parts are applied to the spreadsheet.

Related

Query Import Range not updating when script runs automatically - error loading

I have a script to paste the raw data from a csv received by email. When the raw data is pasted on the sheet, I expected that another sheet with a query import range formula updates automatically with the new data.
I have a second script to read data from a pivot table that comes from the sheet with those formulas. However when it tries to read the data from the pivot table I get the error Exception: The number of rows in the range must be at least 1.. This happens because my variable numRows is equal to zero.
When I open the g-doc manually I see an error on the sheet with the formulas mentioned: error loading.
However, after really a few seconds that I open the gdoc, the range updates almost instantaneally without any problem, and If I manually run the script after this happening it runs without any problem.
How can I make sure that after updating the raw data I don't get the formulas stucked on error loading? I would like to run the script automatically and not manually. Any tip is more than welcome.
Notes:
I've tried already every type of recalculations but didn't work (on change, on change and every hour, on change and every minute)
The raw data has arround 2300 rows
The formula I am using is the following: =QUERY(IMPORTRANGE("1OpF8gcrV1Yj8bYP1j5PsHM4VRw2pKZOUmJf6VxGeFdY","raw_data!A2:G"), "select Col1,Col2,Col3,Col4,Col5,Col6,Col7 where Col2 is not null order by Col4 asc, Col1 asc, Col5 asc",0)
function sending_emails(){
var ss=SpreadsheetApp.openById("1OpF8gcrV1Yj8bYP1j5PsHM4VRw2pKZOUmJf6VxGeFdY");
var today = new Date();
if(today.getDay() != 6 && today.getDay() != 0){
//Sending emails to reps:
var data_sheet = ss.getSheetByName("Copy of sending_emails");
var aux = data_sheet.getRange("B3:B").getValues();
var startRow = 3; // First row of data to process
var numRows = aux.filter(String).length;
Logger.log('numRows' + numRows);
// Fetch the range of cells
var dataRange = data_sheet.getRange(startRow, 1, numRows, 5); //I get the error here because startRow = 3 and I get numRows = 0
// Fetch values for each row in the Range.
var data = dataRange.getValues();
for (var a in data) {
var row = data[a];
var message = row[3];
var emailAddress = row[0];
Logger.log('emailAddress'+ emailAddress);
MailApp.sendEmail({
to: emailAddress,
subject: 'Task Manager',
htmlBody: message,
cc: row[4]
});
}
}
}
The issue is likely with IMPORTRANGE
The class of functions IMPORTHTML, IMPORTRANGE etc have been the subject of many questions about auto updating - this approach generally seems to be quite flaky. I can't find it documented anywhere but I suspect that these functions stop calculating when they are closed. Or if a recalculation happens, for some reason they are not authorized because they are no longer linked to a user session.
That said, although I don't use this approach, I have tested it various times and it seems to work for me, though I know there are many people for whom it does not.
Some people have found that by removing all protections and making the sheet public removes errors, though in my experience its just best to remove formulae from the equation (no pun intended).
Suggested fix
In your chain of Mail > Apps Script > Sheet > FORMULA > Sheet.
Change it to Mail > Apps Script > Sheet > Apps Script > Sheet.
I don't have your source data to test with, but to implement your query in Apps Script would look something like this:
const ss = SpreadsheetApp.openById("YOUR ID");
const dataRange = ss.getSheetByName("Sheet1").getRange("A2:G");
const data = dataRange.getValues()
const filteredData = data.filter(row => row[1] !== "")
You could potentially sort the data with formulae once it has been imported with the script.
TLDR: Chaining IMPORTRANGE may work sometimes, but it doesn't seem very reliable. In my opinion, you are better off moving everything to Apps Script at this point.

Exception: The starting row of the range is too small - Logs Numbers Why?

I have a basic script to take numbers from a sheet and use them to create a range, as well as using the last column function. I have had the error range is too small for the posting range.
When I log the output for both the column and row numbers these come out as expected!
I thought initially, it was because one was a last column pull and the other was pulling an integer from the cell in the sheets, as they were coming with decimal places, so I have overcome this with the conversion to number and then removing the decimals with the .tofixed() but this does not work either. Any ideas?
function weeklyData() {
var sourcess = SpreadsheetApp.openById('1B93Oq2s9Nou5hVgOb3y3t15t9xnqRMBnrYkAed-oxrE'); // key of source spreadsheet
var sourceSheet = sourcess.getSheetByName('Measures & Answers'); // source sheet name - change to your actual sheet name
var lr = Number(sourceSheet.getRange(2,3).getDataRegion(SpreadsheetApp.Dimension.ROWS).getLastRow()).toFixed(0);
for(var i=0;i<lr+1;i++){
var dataValue = sourceSheet.getRange(i+2,3).getValue(); //This weeks numbers to update into table
var rowdataRange = sourceSheet.getRange(i+2,4).getValue(); //The row that the data needs to be pasted
var rowformat = Number(rowdataRange);
var row = rowformat.toFixed(0);
var pasteSheet = sourcess.getSheetByName('WHERE DATA ENDS UP'); // Data is to be pasted - change to your actual sheet name
var pasteColumn = pasteSheet.getRange(12,12).getDataRegion(SpreadsheetApp.Dimension.COLUMNS).getLastColumn()+1;
var column = pasteColumn.toFixed(0); // Column that is free for this weeks data
var pasteRange = pasteSheet.getRange(row,column,1,1);
Logger.log(pasteRange);
// pasteRange.setValue(dataValue);
}};
Your script works fine for me. I suspect this is an example script you've adapted from somewhere and trying to apply it to your data structure?
The reason you are getting the error is probably because the data in column 4 of your source sheet is not of number format? Either change your data or change the following line to the column containing numeric values.
var rowdataRange = sourceSheet.getRange(i+2,4).getValue();
This script is poorly written for this particular use.
You might want to check your Spreadsheet since it has lots of random values at random ranges.
When you use the following command Logger.log(pasteSheet.getLastColumn()); the number returned is 3753: which means that that is the next available column at which your data will be pasted.
The error message you were getting is due to the fact that the range was incorrect and you were passing wrong values in order to access it, which was most likely because of the values mentioned above.
Moreover, after cleaning all the unnecessary data, you can make use of the below script.
Snippet
function weeklyData() {
var spreadsheet = SpreadsheetApp.openById("ID_OF_THE_SPREADSHEET");
var sourceSheet = spreadsheet.getSheetByName("Measures & Answers");
var pasteSheet = spreadsheet.getSheetByName("WHERE DATA ENDS UP");
var valsToCopy = sourceSheet.getRange(2, 3, sourceSheet.getLastRow(), 1).getValues();
var rowsAt = sourceSheet.getRange(2, 4, sourceSheet.getLastRow(), 1).getValues();
var column = pasteSheet.getRange(12,12).getDataRegion(SpreadsheetApp.Dimension.COLUMNS).getLastColumn()+1;
for (var i = 0; i < valsToCopy.length; i++)
if (rowsAt[i][0] != "")
pasteSheet.getRange(parseInt(rowsAt[i][0]), parseInt(column)).setValue(valsToCopy[i][0].toString());
}
Explanation
The above script gathers all the data that needs to be copied as well as the rows associated with it. In order to make sure you don't end up using inappropriate values for the ranges, an if condition has been placed to make sure the value is not empty.
Reference
Sheet Class Apps Script - getLastColumn();
Sheet Class Apps Script - getRange(row, column, numRows, numColumns);

How could a dummy Browser.msgBox call affect output in GAS?

I have a script that moves data from one sheet to another and as part of this must insert the appropriate number of rows into the second sheet to account for the data from the first sheet. I'm running into an issue where, for reasons I don't quite understand, the data is duplicated in the second sheet, e.g. if 94 rows are in the original sheet, the second contains these 94 rows then an additional 94 rows right below with the exact same data. My code is:
function moveToEntrySheet(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var rawRow = 2;
var entrySheet = ss.getSheetByName('Sheet2');
var rawSheet = ss.getSheetByName('Sheet1');
var entryRow = 4;
var numEntries = rawSheet.getLastRow() - 1;
if (numEntries <= 0){
return;
}
Browser.msgBox(numEntries);
entrySheet.insertRowsBefore(entryRow, numEntries);
Browser.msgBox(numEntries);
var entryRange = entrySheet.getRange(4, 2, numEntries, 6);
var vals = rawSheet.getRange(2, 1, numEntries, 6).getValues();
Browser.msgBox(numEntries);
entryRange.setValues(vals);
}
To try and debug this I put calls to Browser.msgBox() in my code to make sure that numEntries is the expected number (94), which it does appear to be at each of the 3 points that I put the call in. The weird thing is that when I run this function with the calls to msgBox() included, the entries are only pulled to the other sheet once. If I keep the first call in it works the same, and if I keep the second or third call in double the number of rows that should be inserted are inserted but the data isn't copied itself, essentially giving n empty rows in the sheet, where n is the number of rows of data in sheet 1. How could calls to Browser.msgBox() affect the copy in this way, and how can I avoid the copy from duplicating? Thanks!

using query() with apps script keeps adding 500 rows to sheet

I'm working on a large sheet and cells are precious given gsheets quota.
I have a range, and when data updates automatically a script updates the named range automatically to be the full length of the data.
The named range is called "gadatapull". This range is on the tab "datapull".
Two tabs. "datapull" is where fresh data is dumped and "data_prep" is where I do stuff to the data. After a fresh pull just now datapull has 2,733 rows of data, including the headers.
I would like data_prep to have the same length as datapull. Plus 7 rows for text at top of data_prep.
When my script to update data runs I do this:
// clear dataprep sheet for new data
var lastRow = 7;
var maxRows = dataprep.getLastRow();
if(maxRows - lastRow > 0) {
dataprep.deleteRows(lastRow+1, maxRows-lastRow);
}
data_prep has 7 rows (because the script just deleted all rows above 7).
Now, in data_prep cell A7 I have:
=query(indirect("gadatapull"),"select *")
Expected result was that all the fresh data in "gadatapull" would appear in data_prep tab and that data_prep tab would expand accordingly.
But what actually happens is all the data arrive as expected, but then there are an additional blank 500 rows at the bottom. This 500 number is too rounded off. Makes me think Gsheets is automatically adding this number as a default under some condition.
How can I prevent Gsheets from adding these additional 500 rows?
Instead of letting the sheet API expanding the number of rows (which is your hypothesis and might well be true ;-) you can add all the necessary cells before importing data.
I didn't try in real conditions but this should work.
Btw, the script imports data as well.
code :
function copyDataToSheet(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var dataprep = ss.getSheetByName('data_prep');
var datapull = ss.getSheetByName('datapull');
var lastRow = 7;
var maxRows = dataprep.getLastRow();
if(maxRows - lastRow > 0) {
dataprep.deleteRows(lastRow+1, maxRows-lastRow);
}
var datapullSize = datapull.getLastRow();
dataprep.insertRows(7,datapullSize);// insert exactly the number of rows you need.
var dataToCopy = datapull.getDataRange().getValues()
dataprep.getRange(7,1,dataToCopy.length,dataToCopy[0].length).setValues(dataToCopy);
}

Copying Data Sheet1 to Sheet2 so you can sort & edit both sheets (google apps script?)

I am working in goggle sheets and think I need to use a google apps script to do what I want, but I am a psychologist at a non-profit University hospital trying to do some good and not a programmer (which probably shows) and I am desperately in need of help. I am trying to set up a series of spreadsheets to track participation in workshops for our treatment method.
1) I have a sheet “Participant_Registration” where basic information is entered
2) I want to transfer information from only the first four columns (A:D) of “Participant_Registration” to a second sheet “Learning_Sessions_Attendance”
3) I am also transferring the same information to a third sheet 'Consultation1_Attendance' – but I need to first filter and select only those people assigned to that group.
Here is a link to a copy of my spreadsheet.
https://docs.google.com/spreadsheets/d/17d0bT4LZOx5cyjSUHPRFgEZTz4y1yEL_tO3gtSJ4UJ8/edit?usp=sharing
More generically this is what I am trying to do. Is this possible in google app scripts? It seems it should be.
1) I have original data in sheet1
2) I want the first four columns (A:D) to transfer to sheet2 (it is fine if I need a trigger variable)
3) I want them to transfer in such a way that if you sort either sheet, the data are still fine (still linked to the right line).
4) Ideally if there is a change to the data in the source sheet (Sheet1) the same change will be made in Sheet2.
5) Ideally this would all happen automatically without human intervention through a script.
Any ideas?? I so need your help. I have been all over the forum, git hub, and done a ton of searches and tried following a lot of examples I saw but nothing works. I really need help.
Here are my sample scripts each with a problem:
//The following code copies a range from sheet1 to sheet2 as I wanted. A problem occurs if after if we copy the data from sheet1 we add data to other columns on sheet2. Later if we sort on some variable (which people are bound to do) if the function is deployed again it will overwrite data meaning the data from sheet1 are not connected to the right individual on sheet2
function CopyRange() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Participant_Registration");
var range = sheet.getRange("A14:D");
var values = range.getValues();
var target = ss.getSheetByName("Learning_Sessions_Attendance");
var target_range = target.getRange("A10:D");
range.copyTo(target_range);
}
So I tried again. This time I tried to just copy the last edited row from sheet1 to sheet2. This function does not appear to work for me.
function CopyRow2() {
// Get Spreadsheets
var source = SpreadsheetApp.openById("1egn6pnRd6mKMGuQxX_jtgwYDtkuMUv2QJItLdh7aIEs");
var target = SpreadsheetApp.openById("1egn6pnRd6mKMGuQxX_jtgwYDtkuMUv2QJItLdh7aIEs");
// Set Sheets
var source_sheet = source.getSheetByName("Participant_Registration");
var target_sheet = target.getSheetByName("Learning_Sessions_Attendance");
var rowIdx = source_sheet.getActiveRange().getRowIndex();
var rowValues = source_sheet.getRange(rowIdx,1,1,source_sheet.getLastRow()).getValues();
Logger.log(rowValues);
var destValues = [];
destValues.push(rowValues[0][0]);// copy data from col A to col A
destValues.push(rowValues[0][1]);//copy data from col B to col B
destValues.push(rowValues[0][2]);//copy data from col C to col C
destValues.push(rowValues[0][3]);//copy data from col D to col D
var dest=source.getSheets()[4];
dest.getRange(dest.getLastRow()+1,1,1,destValues.length).setValues([destValues]);//update destination sheet with selected values in the right order, the brackets are there to build the 2D array needed to write to a range
}
So I tried again and again. I have lots of examples but none seem to work.
Thanks so much.
Chandra
For that to happen automatically (one sheet's change updating another sheet), you will surely need an "event/trigger" to run a script whenever you change a cell. (that is the "onEdit()" function).
But since scripts are likely to fail sometimes (even when they are perfect, that's because of some Google issues), it's not guaranteed that the sheets will always contain the same data.
But, if I could suggest another way, do not let ID be optional. If that is a real ID (like the person ID card number), create another ID exclusively for working with the sheet.
I have edited your second sheet showing a suggestion of how to do it without using scripts. The only things you must be aware of are:
Do not create two people with the same ID.
You have to insert (only) the ID manually in the second sheet.
The VLOOKUP forumla will search for that ID in the first sheet and return the data in the same line. You can sort any sheet in whatever way you like. As long as you don't change people's IDs.
So, in sheet 2, use this in the First Name, Last Name and Email address:
=vlookup(A10,Participant_Registration!$A:$D,2,false)
=vlookup(A10,Participant_Registration!$A:$D,3,false)
=vlookup(A10,Participant_Registration!$A:$D,4,false)
Just extend this formula downwards
I hope this helps. I would avoid scripting for that at any cost. It would be my last resort. (Scripts also need to be changed if you want to rearrange your sheet, and if not, they might cause trouble, write over existing data...)
I also added a button (insert - drawing) and put a script in it (right button, click down arrow, "transfer? script" -- translated from Portuguese).
If you lock all four columns in sheet2 and lock the ID column in sheet 1, people will not be able to chang IDs and cause mess. They can edit people in sheet 1 and not change the formula in sheet2. Script is not affected by sorting or empty spaces (it adds the person in the first empty row it finds).
I added "named ranges" for the four column headers. (With named ranges, the script can refer to names instead of coordinates, which enables you to rearrange the sheet inserting and deleting columns, or moving them with CUT and paste - but the VLOOKUP formula will need manual update if you rearrange columns).
Here is the code: (it could get better if you manage to create dialog boxes and ask for the person's data inside that dialog, then you could lock everything - and you would need an edit button besides the add).
function AddPerson()
{
var S1Name = "Participant_Registration";
var S2Name = "Learning_Sessions_Attendance";
var ID1Name = "regID";
var ID2Name = "learnID";
//these vars are not used in this script
var FN1Name = "regFirstName";
var FN2Name = "learnFirstName";
var LN1Name = "regLastName";
var LN2Name = "learnLastName";
var Email1Name = "regEmail";
var Email2Name = "learnEmail";
var sSheet = SpreadsheetApp.getActiveSpreadsheet();
var Sheet1 = sSheet.getSheetByName(S1Name);
var Sheet2 = sSheet.getSheetByName(S2Name);
var ID1 = getRangeByName(sSheet, Sheet1.getName(), ID1Name);
var ID2 = getRangeByName(sSheet, Sheet2.getName(), ID2Name); Logger.log("ID2: " + ID2.getValue());
var Empty1 = getFirstEmpty(ID1);
var Empty2 = getFirstEmpty(ID2);
var Biggest1 = getBiggestID(ID1); Logger.log("Biggest 1: " + Biggest1);
var Biggest2 = getBiggestID(ID2); Logger.log("Biggest 2: " + Biggest2);
if (Biggest1 !== Biggest2)
Browser.msgBox("Warning: there are IDs in one sheet that are not in the other sheet");
var Biggest;
if (Biggest1 > Biggest2) Biggest = Biggest1;
else Biggest = Biggest2;
Biggest++;
Empty1.setValue(Biggest);
Empty2.setValue(Biggest);
}
function getFirstEmpty(Header)
{
while (Header.getValue() !== "")
{
Header = Header.offset(1,0);
}
return Header;
}
function getBiggestID(Header)
{
var Sheet = Header.getSheet();
var LastRow = Sheet.getLastRow();
var Values = Sheet.getRange(Header.getRow(), Header.getColumn(), LastRow - Header.getRow() + 1).getValues();
var len = Values.length;
var MaxID = 1;
for (var i = 0; i < len; i++)
{
var val = Number(Values[i]);
if (!isNaN(val) && val > MaxID)
MaxID = val;
}
return MaxID;
}
function getRangeByName(spreadSheet, sheetName, rangeName)
{
Logger.log("Trying range: " + "'" + sheetName + "'!" + rangeName);
return spreadSheet.getRangeByName("'" + sheetName + "'!" + rangeName);
}