Google sheets cell reference out of range on getActiveCell() - google-apps-script

I have a google sheet that checks which cell is being changed, and if it falls in the correct range, then it updates the date in the last column out. The code looks like this:
function onEdit() {
var firstProductCol = 6;
var lastProductCol = 30;
var s = SpreadsheetApp.getActiveSheet();
if( s.getName() == "Skills Map" ) { //checks that we're on the correct sheet
var activeCell = s.getActiveCell();
Logger.log("activeCell: " + activeCell.getA1Notation());
var seName = activeCell.offset(0, 3 - activeCell.getColumn()).getValue();
Logger.log('value of B: [' + seName + "]");
}
And the error suggests it's on Line 7 which is just a getActiveCell... so I'm not sure how that can cause an error to be thrown given that if there isn't an active cell, you can't be changing the value of the field.
When I run the script myself, I never run into the error. The problem is every once in a while, when others use the sheet (I don't know what they do in particular), I receive an end of day report on this error...
Using the debugging tools in Script Editor, I can only see the logs from the last run... although there is a View -> Console Logs, but that doesn't seem to store any of the logs from previous runs... any way to do that?
Any suggestions on the error itself?
Cheers.

Looks that the line number 7 is
var seName = activeCell.offset(0, 3 - activeCell.getColumn()).getValue();
Since column number should be a positive numbers it's very likely that the error occurs when the user edit a cell other than the columns A, B or C.
One way to handle this is to check that the value that returns activeCell.getColumn() is less than 3.

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.

Shared Spreadsheet with two instances of onEdit, deleting wrong row

I'm trying to create a lead management format using Google Sheets & Apps Script.
The apps script is checking whether the value in column M of sheet Propsect or Interested has changed and depending on the value, moving the row to the respective sheet (Interested, Postponed, Lost, or Booked)
The spreadsheet is shared with my team who'll make changes and with multiple users editing at a time.
Now, the problem is that, as soon as two onEdits are triggered, and if both require rows to be moved, the first instance runs properly but the second one removes the wrong row.
Eg: In sheet Prospect, Row 2 & Row 3 have status changed to Lost & Postponed at the same time. Now, Lost gets triggered properly, however, the Postponed instance deletes the 4th row (now the 3rd row, as row 2 was removed before).
I have tried to add in lockservice to the code so that only one instance is running but that doesn't seem to solve the problem as the event object is still considering the un-updated row number.
Even tried adding flush() at the start & end of the code but didn't work either.
You can access the spreadsheet here.
My code is as follows:
function Master(e) {
var lock = LockService.getScriptLock();
var SS = e.source;
var Sheet = e.source.getActiveSheet();
var Range = e.range;
if(Sheet.getName() == "Prospect" && Range.getColumn() == "13" || Sheet.getName() == "Interested" && Range.getColumn() == "13"){
moveRows(SS,Sheet,Range);
}
lock.releaseLock();
}
function moveRows(SS,Sheet,Range) {
var val1 = Sheet.getRange(Range.getRow(),1,1,10).getDisplayValues();
val1 = String(val1).split(",");
var tar_sheet = SpreadsheetApp.getActive().getSheetByName(Range.getValue());
var row = tar_sheet.getRange(tar_sheet.getLastRow()+1,1,1,val1.length).setValues([val1]);
Sheet.deleteRow(Range.getRow());
}
}
Is there any way for the second onEdit to run only after the first has completed execution? I guess, if that could happen, the problem would be solved?
I Hope I have been able to convey my question properly.
Issue:
Event object e passed to a onEdit(e) is not altered, when two or more edits are done at the same time and the first edit alters the next edit's row number- making e.range.rowStart of the second+ edit unreliable at the time of it's execution.
Possible Solutions:
Do not delete the rows immediately. Mark them for deletion(save the range string in properties service) and delete them later(time trigger), when document is not in use.
Alternatively, Add code guards: Check range.getValue()===e.value. If they're equal, continue to moveRows else keep offseting the range by -1 row until they're both equal.
References:
PropertiesService
Range#offset
I guess you should trigger only one function based on user interaction and then inside that perform conditional operations.
Something like this:
function onEdit(event_object) {
var sheet = event_object.range.getSheet();
var row = event_object.range.getRow();
var column = event_object.range.getColumn();
if (sheet.getName() == "Sheet1") {
// perform operations when Sheet1 is edited
} else if (sheet.getName() == "Sheet2") {
// perform operations when Sheet2 is edited
}
}
Reference :
https://developers.google.com/apps-script/reference/spreadsheet/range
https://developers.google.com/apps-script/guides/triggers/events#edit

Google sheets script writing, how do I prevent an auto update or recacluation of my timestamps in googel sheets?

I attached a copy of the spreadsheet I run in google. The concept is when someone begins picking an order, they will for instance type their name in I3 and their time will start. once complete they will type their name in J3 , each causing a time stamp below leading to a total duration time. later it will factor percentages.
The problem is the time stamps seem to randomly update without prompting to do so. it seems to be when it is printed or reopened. This will cause inaccuracies in the times and percentages. Any assistance would be greatly appreciated
It would appear that I should write a script to accommodate this need, but I haven't the slightest on how to do this. I was directed to this forum from a reply in google docs help forum
enter link description here
I don't think you can choose on which cell you update or recalculate (Tell me if I'm wrong).
A non-optimized workaround (Maybe some expert have got better solution):
function onEdit(e){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var i = 3;
while(i <= sheet.getLastRow())
{
var data = sheet.getRange(i, 9, 3, 2).getValues();
if(data[1][0].length == 0)
{
if(data[0][0] != "")
{
var d = new Date();
sheet.getRange(i+1, 9).setValue(d);
}
}
if(data[1][1].length == 0)
{
if(data[0][1] != "")
{
var d = new Date()
sheet.getRange(i+1, 10).setValue(d);
}
}
i=i+3;
}
}
The onEdit(e) function is a simple trigger implement in Google Spreadsheet. It will trigger each time a cell is edit.
On this code above, we iterate to check on each element if a name was added or not. If so, the code will set the date and time below the name just added. Since you set the value on one single moment, no auto update will modify those time.
for this workaround, you need to remove the formula you put on the cell where you want your timestamp (i.e I4 and J4). You can keep the cells which calculate the difference between time, they should work with no problem.
Paste below script in the script editor and save it.
function onEdit(e) {
if (e.range.rowStart % 3 != 0 || [9, 10].indexOf(e.range.columnStart) == -1) return;
var o = e.range.offset(1, 0);
if (!o.getValue()) o.setValue(new Date())
}
Don't run the script by clicking the play button in the script editor. Instead to back to any of the tabs, clear all cells where you now have the =now() formula and enter a name. See if the timestamp appears.

Service Error: Spreadsheets (getLastRow)

I am at my wit's end here. I had three scripts working beautifully, and then overnight they all failed with the dreaded "Service Error".
The error always references getLastRow as the offending line. I've tried adding and removing rows at the bottom of the sheet, removing some of the rows with data in them, unfreezing headers and columns, etc. with no luck whatsoever. I also tried reverting back to last week's version of the script, long before the failure (first was Monday night), to no avail.
I've done a lot of research and I've noticed most often the thing that fixes these Service Errors is something completely random. Some people say that this error is caused by memoization on Google's end. If so, what can I do to fix this?
A huge project is basically crashing and burning because these scripts failed...any help would be awesome. Thanks.
For reference, one of my scripts (the one that failed first).
function timeStamp2() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[4];
var numRows = sheet.getLastRow()-1; // Number of rows to process
Logger.log(numRows)
var startRow = 2; // First row of data to process
// Fetch the range of cells
var dataRange = sheet.getRange(startRow, 1, numRows, sheet.getLastColumn());
// Fetch values for each row in the Range.
var data = dataRange.getValues();
//Logger.log(data)
for (var i = 0; i < data.length; ++i) {
var row = data[i];
var schedulingStatus = row[4];
var timeStamp = row[3];
var scheduled = "Scheduled";
if(timeStamp === '') {
if(schedulingStatus == scheduled ) {
sheet.getRange(startRow + i, 3).setValue(new Date());
}
}
}
}
Whether this constitutes an 'Answer' I cannot judge, but rest assured you are not alone in noticing the sporadic behavior of certain intrinsic GAS functions – notably a few in common use with spreadsheets to append new rows or locate the last row/column of data – which has plagued my own code and others' due to extremely long execution times, often as high as 20 seconds per instance!
I insist there must be a hidden timing flaw in getLastRow(), as shown below. The problem now routinely leads to Timeouts in my workflow and generates frequent failures and Service error: Spreadsheets messages.
For example, if you run the following code function every minute (via a timed Trigger)...
function getLastRow_TEST() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("getLastRow_TEST");
var timeBegin = Date.now();
var nextRow = sheet.getLastRow() + 1;
var timeEnd = Date.now();
var range = sheet.getRange("A" + nextRow + ":B" + nextRow);
var timeElapsed = (timeEnd - timeBegin) / 1000;
range.setValues([[nextRow - 1, timeElapsed]]);
}
...on your test Spreadsheet, containing a sheet named 'getLastRow_TEST', you will soon notice clustered delays (> 1 sec.), as shown in the resulting data (collected at 10:55am on Monday 6/13/2016), but with no discernible pattern of occurrence:
TEST # SECONDS
1 0.041
2 10.242 *
3 10.256 *
4 5.194 *
5 0.055
6 0.178
NOTE: This testing was performed with an otherwise pristine (empty) spreadsheet, with no other code running, no timed Triggers, nor script locking active.
I am attempting to submit this issue as a bug report on Google's issues tracker, but felt compelled to share my experience with you here (I posted elsewhere on SO about this glitch).
Are you counting your array positions correctly?
ss.getSheets()[4]
means it's the fifth sheet. I also noticed that although you initialise timeStamp as the value in the fourth column of row:
var timeStamp = row[3]
later on you:
sheet.getRange(startRow + i, 3).setValue(new Date())
in the third column of the row. ie row[3] is one cell to the right of sheet.getRange(startRow + i, 3).
I had a similar issue with a script I wrote.
It gave Service Error: Spreadsheets said the problem was with the line containing getLastRow.
My fix ended up making sure there was at least one row at the bottom without any data. Haven't had a problem since.
I had the same problem recently.
My attempts to fix would be:
Try to replace getLastRow with getMaxRows — get the last
row of a sheet
Try to use the construction like:
sheet.getDataRange().getLastRow(); → assuming my data range starts
at row #1, the result should be the same.

Format row color based on cell value

I am trying to adapt the example script from this previous, related question. For rows where the cell value in column K is zero, I want to make the row yellow.
Here is my current adapted code:
function colorAll() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 3;
var endRow = sheet.getLastRow();
for (var r = startRow; r <= endRow; r++) {
colorRow(r);
}
}
function colorRow(r){
var sheet = SpreadsheetApp.getActiveSheet();
var c = sheet.getLastColumn();
var dataRange = sheet.getRange(r, 1, 1, c);
var data = dataRange.getValue();
var row = data[0];
if(row[0] === "0"){
dataRange.setBackground("white");
}else{
dataRange.setBackground("yellow");
}
SpreadsheetApp.flush();
}
function onEdit(event)
{
var r = event.source.getActiveRange().getRowIndex();
if (r >= 3) {
colorRow(r);
}
}
function onOpen(){
colorAll();
}
My problem is, I can't figure out how to reference column K. In the linked answer above, the script's creator claims, "[h]ere is a Google Apps Script example of changing the background color of an entire row based on the value in column A." First, and most importantly, I can't figure out where he's referencing column A. I thought changing "var dataRange = sheet.getRange(r, 1, 1, c);" to "var dataRange = sheet.getRange(r, 11, 1, c);" would do it, but that just added 10 blank columns to the end of my sheet, and then the script crashed. I do not understand why.
Secondly, but more as an aside, his claim that the script affects entire rows is inaccurate, as his original "var dataRange = sheet.getRange(r, 1, 1, 3);" only colored the first three columns - which is why I added "var c" and changed "3" to "c".
Furthermore, when I play/debug the script, or run "onEdit" from the spreadsheet script manager, I get "TypeError: Cannot read property "source" from undefined." I can see that "source" is undefined - I had mistakenly assumed it was a Method at first - but I'm not sure how to fix this issue either.
Lastly, column K will not always be the reference column, as I mean to add more columns to the left of it. I assume I'll have to update the script every time I add columns, but there is a column heading in row 2 that will never change, so if someone can help me devise a bit of code that will look for a specific string in row 2, then get that column reference for use in function colorRow(), I would appreciate it.
I can't tell if this script is structured efficiently, but ideally, I want my spreadsheet to be reactive - I don't want to have to rerun this script after editing a driving cell, or upon opening; it reads like it's supposed to do that (were it not buggy), but this is my first attempt at using Google Apps Script, and I don't feel certain of anything.
I'm not great with scripting, but I took a programming fundamentals/Python class in grad school back in 2006, and spent 4 years working with Excel & Access shortly after that, often creating and adapting Macros. I can't really design from scratch, but I understand the basic principles and concepts, even if I can't translate everything (e.g., I don't understand what the "++" means in the third argument in the "for" statement I'm using: "for (var r = startRow; r <= endRow; r++)." I think I'm allegorically equivalent to a literate Spanish speaker trying to read Italian.
Help, and educational explanations/examples, will be much appreciated. Thank you kindly for reading/skimming/skipping to this sentence.
Rather than rewriting the code which you have already got some help with, I will try to give you explanations to the specific questions you asked. I see that you have some of the answers already but I am putting thing in completely as it helps understanding.
My problem is, I can't figure out how to reference column K.
Column A = 1, B = 2,... K = 10.
I can't figure out where he's referencing column A.
You were close when you altered the .getRange. .getRange does different things depending on how many arguments are in the (). With 4 arguments it is getRange(row, column, numRows, numColumns).
sheet.getRange(r, 1, 1, c) // the first '1' references column A
starts at row(r) which is initially row(3), and column(1). So this is cell(A3). The range extends for 1 row and (c) columns. As c = sheet.getLastColumn(), this means you have taken the range to be 1 row and all the columns.
When you changed this to
var dataRange = sheet.getRange(r, 11, 1, c) // the '11' references column L
You have got a range starting at row(3) column(L) as 11 = L. This runs to row(3) column(getLastColumn()).
This is going to do weird things if you have gone out of range.
You may have pushed it in to an infinite for loop which would cause the script to crash
Secondly, but more as an aside, his claim that the script affects entire rows is inaccurate, as his original "var dataRange = sheet.getRange(r, 1, 1, 3);"
only colored the first three columns - which is why I added "var c" and changed "3" to "c".
You are correct. The (3) says that the range extend for 3 columns.
"TypeError: Cannot read property "source" from undefined."
What is happening here is not intuitively clear. You can't run the function onEdit(event) from the spreadsheet script manager because it is expecting an "event".
onEdit is a special google trigger that runs whenever any edits the spreadsheet.
it is passed the (event) that activated it and
event.source. refers to the sheet where the event happened so
var r = event.source.getActiveRange().getRowIndex(); gets the row number where the edit happened, which is the row that is going to have its color changed.
If you run this in the manager there is no event for it to read, hence undefined. You can't debug it either for the same reasons.
Lastly, column K will not always be the reference column, as I mean to
add more columns to the left of it. I assume I'll have to update the
script every time I add columns, but there is a column heading in row
2 that will never change, so if someone can help me devise a bit of
code that will look for a specific string in row 2, then get that
column reference for use in function colorRow(), I would appreciate
it.
Before I give you code help her, I have an alternative suggestion because you are also talking about efficiency and it is often faster to run functions in the spreadsheet than using scripts. You could try having column A as an index columns where ColumnA(Row#) = ColumnK(Row#). If you put the following into cell(A1), ColumnA will be an exact match of Column K.
=ArrayFormula(K:K)
Even better, if you add/remove Columns between A and K, the formula will change its reference without you doing anything. Now just hide columnA and your sheet is back to its originator appearance.
Here is your code help, utilizing some of your own code.
function findSearchColumn () {
var colNo; // This is what we are looking for.
var sheet = SpreadsheetApp.getActiveSheet();
var c = sheet.getLastColumn();
// gets the values form the 2nd row in array format
var values = sheet.getRange(2, 1, 1, c).getValues();
// Returns a two-dimensional array of values, indexed by row, then by column.
// we are going to search through values[0][col] as there is only one row
for (var col = 0; col < data[0].length; col++) { // data[0].length should = c
if (data[0][col] == value) {
colNo = col;
break; // we don't need to do any more here.
}
}
return(colNo);
}
If break gives you a problem just delete it and let the look complete or replace it with col = data[0].length;
I can't tell if this script is structured efficiently, but ideally, I
want my spreadsheet to be reactive - I don't want to have to rerun
this script after editing a driving cell, or upon opening; it reads
like it's supposed to do that (were it not buggy), but this is my
first attempt at using Google Apps Script, and I don't feel certain of
anything.
It is ok, the fine tuning of efficiency depends on the spreadsheet. function onEdit(event)
is going to run every time the sheet is edited, there is nothing you can do about that. However the first thing it should do is check that a relevant range has been edited.
The line if (r >= 3) seems to be doing that. You can make this as specific as you need.
My suggestion on a hidden index column was aimed a efficiency as well as being much easier to implement.
I'm not great with scripting,
You are doing ok but could do with some background reading, just look up things like for loops. Unfortunate Python is grammatically different from many other languages. A for loop in google script is the same as VBA, C, JAVA, and many more. So reading about these basic operations is actually teaching you about many languages.
I don't understand what the "++" means in the third argument in the "for" statement
It is why the language C++ gets its name, as a programmer joke.
r++ is the same as saying r = r+1
r-- means r = r-1
r+2 means r = r+2
So
for (var r = startRow; r <= endRow; r++)
means r begins as startRow, which in this case is 3.
the loop will run until r <= endRow, which in this case is sheet.getLastRow()
after each time the loop runs r increments by 1, so if endRow == 10, the loop will run from r = 3 to r = 10 => 8 times
1.The onEdit is a special function that is automatically called when you edit the spreadsheet. If you run it manually, the required arguments won't be available to it.
2.To change the colour of the entire row when column K is 0, you have to make simple modifications to the script . See below
function colorRow(r){
var sheet = SpreadsheetApp.getActiveSheet();
var c = sheet.getLastColumn();
var dataRange = sheet.getRange(r, 1, 1, c);
var data = dataRange.getValues();
if(data[0][10].toString() == "0"){ //Important because based on the formatting in the spreadsheet, this can be a String or an integer
dataRange.setBackground("white");
}else{
dataRange.setBackground("yellow");
}
SpreadsheetApp.flush();
}