Google Sheets Custom Function Timing Out - javascript

So I have been having erratically getting an error: "Internal Error Executing the Custom Function" when running my custom function on a decently sized range of cells.
Google specifies "A custom function call must return within 30 seconds. If it does not, the cell will display an error: Internal error executing the custom function."
My custom function is this:
function ConcatLoop(rangeString, concatString, isPrefix, isOneColumn) {
//var rangeString = "A1:A10,B1:B10,C1:C10";
//var concatString = "1x ";
//var isPrefix = "true";
//var isOneColumn = "true";
var rangeStringArray = rangeString.split(',');
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var rangeValues=[];
//GRABBING THE VALUES FROM EACH RANGE
for(i=0;i<rangeStringArray.length;i++)
{
var range = sheet.getRange(rangeStringArray[i]);
rangeValues.push(range.getValues());
}
if(isOneColumn){var newRangeValues = [[]]};
//REMOVES EMPTY STRINGS AND ARRAYS OR CONCATENATES THE STRINGS
for (i = 0; i < rangeValues.length; i++) {
for (j = 0; j < rangeValues[i].length; j++){
if (rangeValues[i][j] == "")
{
rangeValues[i].splice(j, 1);
j--;
}
else if(isPrefix == "true")
{
rangeValues[i][j] = concatString + rangeValues[i][j];
if(isOneColumn){newRangeValues[0].push(rangeValues[i][j])};
}
else
{
rangeValues[i][j] = rangeValues[i][j] + concatString;
if(isOneColumn){newRangeValues[0].push(rangeValues[i][j])};
}
}
if (rangeValues[i] ==""){
rangeValues.splice(i,1);
i--;
}
}
//LOG WHILE TESTING
//if(isOneColumn){Logger.log(JSON.stringify(newRangeValues))}
//else{Logger.log("range values after concat: " + rangeValues)}
//RETURN WHILE RUNNING
if(isOneColumn){return newRangeValues}
else{return rangeValues};
}
When I have 1000 values plugged into the function, it takes quite a while to pull all the values. It runs fine when I test it in GOogle Scripts, because there isn't a time constraint there.
If there is anything I can do to work around this or make this more efficient, could someone let me know? Thank you so much!

Thanks to the incredible individuals who commented, I did find an answer earlier, so I'm going to post it for anyone who might need it.
Google sets a limitation of 30 seconds for the execution time of any custom function on a spreadsheet. Anything above that will give you an internal error. Although there is a limitation of 5 minutes for the execution time in the Script Editor, there is probably an issue if your script takes over 30 seconds to execute. In my case, I had too many separate ranges I needed to pull data out of. So I was calling "getValues(range)" quite a few times within a for-loop.
The solution was to pull each entire sheet in the spreadsheet as one range. So rather than having 7 ranges for each of my 27 sheets, I had 1 range for each of my 27 sheets. This caused me to have an excess amount of unnecessary information in memory, but also caused the execution time to go from around 45 seconds to 10 seconds. Solved my problem entirely.
Thank you!

Related

Is there a way to stop a COUNTIF function after the value changes from TRUE to FALSE in google spreadsheets?

I have a row of tick boxes and I need to count the most recent successive TRUE values. So it should start counting when the first TRUE appears and stop when it changes to FALSE, ignoring anything that comes after that.
Right now I have a script doing that, but since I have a lot of entries, it takes a long time to run and stops after 6min without finishing everything.
for(var j=3;j<lastRow;j++){
count = 0;
for (var k=stupac-1;k>2;k--){
if (range.getCell(j,k).getValue() == true){
count++;
}
else if ((range.getCell(j,k).isChecked() == false)&&(count>0)){
break;
}
}
range.getCell(j,stupac).setValue(count);
}
I thought the best way would be to stop the COUNTIF when the value changes, but have had no luck trying to get that working.
I came up with this solution which seems pretty fast. Your approach iterates over each cell which is not efficient especially when the search space is large. The following approach iterates over the rows in the specified data range.
function myFunction() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet1');
const data = sh.getRange('A3:G'+sh.getLastRow()).getValues();
const counts = [];
data.forEach(row=>{
let t_index = row.indexOf(true);
let cutRow = t_index > -1 ? row.slice(t_index, row.length+1) : [];
let f_index = cutRow.indexOf(false);
let ct = f_index > -1 ? f_index : cutRow.length;
counts.push([ct]);
});
sh.getRange(3,8,counts.length,counts[0].length).setValues(counts);
}
Please adjust the data ranges according to your needs.
The solution here matches the following file:
Please make sure that you have enabled V8 Runtime.

Message box will not pop up when itis run from the menu. This function is also slow

I have two problems.
1) My function noDateAlert(); is SLOW. Any suggestions to make it take less than 5 seconds? Currently takes 17-30 depending on how many lines it has to check.
2) The error message will not pop up when I run noDateAlert(); in another function from my custom menu. It works fine when I run it by itself from the script page. It also works fine when run within the other program from the script page.
I'm using this program to warn me if the 3 cells are empty.
When it is enclosed in the other function it is the first thing listed.
Here's the function:
function noDateAlert() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh0 = ss.getSheetByName("Loads");
// get data from Loads sheet
var data = sh0.getRange(8,29,53,31).getValues();
var data2 = sh0.getRange(8,3,53,3).getValues();
for(var i=0; i < 45; i++) { //Controls how many rows it goes through for posting // len = data.length; i < len
//Only work on loads currently wanting to post
if(data2[i][0] == "P-Post"){
//Warn me if all 3 cells in one row are empty/blank
if(data[i][0] == "" && data[i][1] == "" && data[i][2] == ""){
Browser.msgBox("Some of the loads you want to post are missing dates. Please check dates and run 'Post Loads' again.");
return false;
break;
};
};
};
};
Try this for your msgbox. And I'll keep on looking at the other problem.
SpreadsheetApp.getUi().alert('Some of the loads you want to post are missing dates. Please check dates and run \'Post Loads\' again.');
I'm not privy to your data but I doubt that I do things this way. But since you have access to and knowledge of your data I'll assume that breaking this up in two ranges makes sense. However, I hope you realize that data2[i][0] is actually column 3 and data[i][0] is actually column 29. I left the limit for i at 53 since you specified 53 rows in both ranges. But again. I don't know the detail nor do I have access to the data. So if this doesn't help, you can change it back.
function noDateAlert()
{
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh0=ss.getSheetByName("Loads");
var data = sh0.getRange(8,29,53,31).getValues();
var data2 = sh0.getRange(8,3,53,3).getValues();
for(var i=0;i<53;i++)
{
if(data2[i][0]=="P-Post")
{
if(!data[i][0] && !data[i][1] && !data[i][2])
{
SpreadsheetApp.getUi().alert('Some of the loads you want to post are missing dates. Please check dates and run \'Post Loads\' again.');
return false;
}
}
}
}

How do I Merge Column Values in Google Sheets with Google Apps Script

I recognize that this question is similar to this one. I'm sad to say, I just don't know enough to follow that one.
I also recognize that it is very similar to the Google tutorial, but they are creating a new array, and I need to combine arrays.
The problem
I'm trying to create a permanent list from a volatile list. The permanent list will be updated daily. I need the script to look for duplicates on one sheet, and copy everything new to another sheet. I'm pretty sure that my problem lies mostly in my logic.
I've been bashing together all the similar answers I could find to help (commented out) and I just can't figure it out. More explanation below.
Spreadsheet here
Script here
Code.gs
function updateRoster() {
var ss = SpreadsheetApp
.openById("15DRZRQ2Hcd7MNnAsu_lnZ6n4kiHeXW_OMPP3squbTLE");
var data = ss
.getSheetByName("Volatile Data")
.getDataRange()
.getValues();
var saveData = ss
.getSheetByName("All Data")
.getDataRange()
.getValues();
for (i in data) {
var value = data[i][3];
for (j in saveData) {
var search = saveData[j][3]
if(value == search) {
Logger.log(data[i][3]);
}
}
}
}
I get this in the Log:
[16-03-24 06:35:44:914 PDT] 1.00000006E8
[16-03-24 06:35:44:916 PDT] 1.00000012E8
[16-03-24 06:35:44:918 PDT] 1.00000022E8
Which is correct, those are the duplicates, but I need to select every other row. The ones that are not duplicates.
I've tried a bunch of changes, but I just keep getting the second sheet's list repeated. Please help.
Simply replacing value == search with value != search will not work since then it would add the row to the second sheet if even one of the rows in saveData is different. But it needs to check if all rows are different.
If you are sure that your volatile data sheet doesn't have any duplicates in it, you can do it this way:
var saveSheet = ss.getSheetByName("All Data");
loopThroughData:
for (i in data) {
var value = data[i][3];
for (j in saveData) {
var search = saveData[j][3]
if(value === search) continue loopThroughData;
//here we leave the inner loop and jump back to the top if
//even one row is equal
}
saveSheet.appendRow(data[i]);
}
edit: However it's usually better to use functions instead of a combination of continue and labels.
Therefore I would prefer this solution:
function updateRoster() {
...
function areThereDuplicates(i) {
var value = data[i][3];
for(var j in saveData) {
if(saveData[j][3] === value) return true;
}
return false;
}
for (i in data) {
if(!areThereDuplicates(i)) saveSheet.appendRow(data[i]);
}
}
Well to get every OTHER value you could check for every row that is Different or "new"
if(value != search) {
Logger.log(data[i][3]);
}
and simply add the new rows to the file as
if(value != search) {
Logger.log(data[i][3]);
ss.getSheetByName("All Data").appendRow(data[i]);
}
Good luck =)

Cannot read property "0" from undefined. Error Google Apps Script

I´m geting Cannot read property "0" from undefined. Error on line 16. ( while (colunaDias[emptyCell][0] <= dias) )
It should be a very simple function for google SpreadSheets. I can´t see what I´m doing wrong...
The bizarre thing is that if I don´t use the variable "dias" and use a integer instead. The while function works....
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var leadsSheet = ss.getSheetByName("Leads Todas as Categorias, menos outros - Days (5days)");
var targetSheet = ss.getSheetByName("feticaria");
var cellLeads = leadsSheet.getRange(1,1).getValue();
//var cellTarget = targetSheet.getRange(1,1).setValue(valor);
var colunaDias = leadsSheet.getRange('B:B').getValues();
var sourceMedium = leadsSheet.getRange('A:A').getValues();
var emptyCell = 16;
var dias = 1;
while (colunaDias[emptyCell][0] != ""){
while (colunaDias[emptyCell][0] <= dias){
dias++;
emptyCell++;
}
emptyCell++;
}
Logger.log(emptyCell);
}
I think the only thing that could cause that error is if emptyCell is bigger than the colunaDias array, i.e. if your sheet is smaller than 16 rows (if the value you show here is correct).
Add this line right before the first while :
Logger.log('emptyCell = '+emptyCell+' and colunaDias.length = '+colunaDias.length);
I tested a copy of your script and it runs without error except if I define emptyCell > 1000 on a 1000 rows Sheet.
I'm guessing that colunaDias[emptyCell] is undefined. It passes the first while condition, because "undefined" is not equal to "". If colunaDias[emptyCell] is undefined, then either there is something wrong with this line:
var colunaDias = leadsSheet.getRange('B:B').getValues();
or
colunaDias[emptyCell][0]
Where "emptyCell" is 16 is the problem. getValues() returns an object of rectangular grid of values. I would test to see if there is anything in the rectangular grid, by checking [0][0].
Logger.log('is there any data? ' + colunaDias[0][0])
If there is no data, then the something failed on line:
var colunaDias = leadsSheet.getRange('B:B').getValues();
If that line is working, then something higher up is wrong.
You should be checking the return type of getSheetByName for null.
// The code below will log the index of a sheet named "YourSheetName"
var leadsSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("YourSheetName");
if (leadsSheet != null) {
Logger.log(leadsSheet.getIndex());
}
I know this post in from a few years ago, but this was the closest to a problem I am having. I just want to post my answer in case someone else ends up here one day.
For some reason the check variable "dias" is being passed as a string.
That's why replacing it with a number allows the script to run.
dias = parseInt(dias); //use this before the variable needs to be read
I cant say why it is passing a string after being ++ but this will fix it

Simple arithmetic challenge function with limited attempts

Recently began studying Javascript, trying to read out of Javascript: The Definitive Guide and Eloquent Javascript, while going off on my own to experiment with things in order to really etch them in my memory. I thought a good way to get my head around arithmetic operations and conditional statements, I'd build a series of little games based around each Math operator, and began with addition.
function beginAdditionChallenge() {
var x = Math.ceiling(Math.random()*100);
alert(x);
for (var i = 0; i < 3; i++) {
var a = Number(prompt("Provide the first addend.", ""));
var b = Number(prompt("Provide the second addend.", ""));
if (a + b === x) {
alert("Well done!");
break;
}
else if (a + b !== x && i < 3) {
alert("Please try again.");
}
else {
alert("Fail.");
}
}
}
function initChallenge() {
var button = document.getElementById("challengeButton");
button.addEventListener("click", beginAdditionChallenge);
}
window.addEventListener("load", initChallenge);
You can see the whole thing thus far on JSFiddle, here. The idea is that clicking the button generates a random number between 1 and 100, displays it to the user, then prompts them to provide two addends, giving them 3 attempts. If the sum of these addends is equal to the RNG number, it congratulates the user and ends the program. If they do not provide suitable addends, the loop prompts them to try again, until they've hit 3 attempts, at which point the program snarks at them and ends.
I know the event listener is not the failure point here, as when I change beginAdditionChallenge to simply display a test alert, it works, but I don't know what exactly is wrong with the loop I've created.
You did it correctly. However, Math.ceiling isn't a function and should be Math.ceil. In addition, your code (in jsfiddle) should be set to wrap in head. Why? Because right now you call initChallenge when the page loads. However, in your jsfiddle example, the code runs onLoad so the load event never gets called. Essentially, you're adding a load event after the page has loaded.
http://jsfiddle.net/rNn32/
Edit: In addition, you have a for loop that goes up to three. Therefore
else if (a + b !== x && i < 3) {
alert("Please try again.");
}
should be
else if (a + b !== x && i < 2) {
alert("Please try again.");
}
because when i === 2, the user's last chance has ended.
Everything is fine. Just change:-
var x = Math.ceiling(Math.random()*100);
to:-
var x = Math.ceil(Math.random()*100);

Categories

Resources