Google script - Exceeded maximum execution time , help optimize - google-apps-script

google script spreadsheet
Novice
I try to create a matrix , if the array is a small database everything works fine, of course if it exceeds 800 lines and more rests on the error "You have exceeded the maximum allowed run time ." Not effectively create a matrix :
var s = SpreadsheetApp.getActiveSheet(); //List
var toAddArray = []; //Greate Arr
for (i = 1; i <= s.getLastRow()+1; ++i){ //Start getting Value
var numbr = s.getRange(i,4); //detect range
var Valus = numbr.getValues().toString(); //get value
//filter value
var newznach = Valus.replace(/\-/g, "").replace(/[0-9][0-9][0-9][0-9][0-9][a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "").replace(/[a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "");
toAddArray.push([i.toFixed(0),Valus,newznach]); //add to array 0- Row numb, 1- Value, 2- "filtered" value
}
toAddArray =
{
Row, Value, NewValue - filtered
Row, Value, NewValue - filtered
Row, Value, NewValue - filtered
...
}
Can I somehow get an array of the same the other way ( faster, easier ) ?

You're doing a call to getValues every row, that eats a lot of performance.
It is better to do one big call to have all the data and then go through it sequentially.
var s = SpreadsheetApp.getActiveSheet();
var data = s.getRange(1,4, s.getLastRow()).getValues();
var toAddArray = data.map(function(row, i) {
var Valus = row[0].toString();
var newznach = Valus.
replace(/\-/g, "").
replace(/[0-9][0-9][0-9][0-9][0-9][a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "").
replace(/[a-zA-Zа-яА-Я][a-zA-Zа-яА-Я]/g, "");
return [i.toFixed(0), Valus, newznach];
});

this code:
var Valus = numbr.getValues().toString();
slows you down because you read data from the sheet in a loop.
Try reading data once into array and then work with it:
var data = s.getDataRange().getValues();
And then work with data, in a loop. This sample code log each cell in active sheet:
function logEachCell() {
var s = SpreadsheetApp.getActiveSheet();
var data = s.getDataRange().getValues();
// loop each cell
var row = [];
for (var i = 0; i < data.length; i++) {
row = data[i];
for (var j = 0; j < row.length; j++) {
Logger.log(row[j])
}
}
}

Related

Trying to use onEdit to autofill timeline of a Gantt Chart

I'm trying to create an apps script to autofill a gantt chart when the sheet is edited, but having trouble.
Here is a link to the spreadsheet if it helps.
function ganttChart()
{
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ganttSheet = ss.getSheetByName("Gantt Chart");
var headerRow = ss.ganttSheet.getRange('headerRow').getRow();
var lastRow = ss.ganttSheet.getLastRow();
var lastCol = ss.ganttSheet.getLastColumn();
var firstTask = headerRow + 1
var taskRoleCol = ss.ganttSheet.getRange('taskRole').getColumn();
//I'm not sure if I need to do the below RoleCol if I already have a named range -- this will return an integer which is the column #
var roleCol = ss.getSheetByName("Roles").getRange('Roles').getColumn();
var taskCol = ss.ganttSheet.getRange('taskNames').getColumn();
var startWeekRow = ss.ganttSheet.getRange('startWeek').getRow();
var expDurationCol = ss.ganttSheet.getRange('expDuration').getColumn();
//set the requirements for the edit trigger -- not sure what these would be
//if (e.range)
//{
for (var i = firstTask; i < lastRow; i++)
{
var currentTask = ss.ganttSheet.getRange(i, taskCol).getValue();
var currentStartWeek = ss.ganttSheet.getRange(i, startWeekCol).getValue();
var currentTaskExpDuration = ss.ganttSheet.getRange(i,expDurationCol).getValue();
var currentTaskRole = ss.ganttSheet.getRange(i,taskRoleCol).getValue();
if (currentTask != null)
{
if (currentStartWeek != null)
{
//for loop to identify the column that matches the start week #
for (var j = 0; j < lastCol; j++)
{
var checkWeek = ss.ganttSheet.getRange(startWeekRow, j).getValue();
if (checkWeek == currentStartWeek)
{
//identify the range
var taskTimeRange = ss.ganttSheet.getRange(i,j - 1,(currentTaskExpDuration*2 +1), 1);
//for loop get the background color based on role
for (var k = 0; k < lastRow; k++)
{
var checkRole = ss.ganttSheet.getRange(k, roleCol).getValue();
//if role value matches the currentTaskRole
if (checkRole == currentTaskRole)
{
var roleColor = ss.ganttSheet.getRange(k, roleCol).getBackground();
//reformat the range based on duration
taskTimeRange.setBackground(roleColor);
}
}
}
}
}
}
}
//}
}
I took off the "onEdit" to try and get the program to work on run, but I'm still getting a "cannot read properties of undefined" error.
What should happen is:
when a user edits the "Gantt Chart" sheet
the program changes the background color in the corresponding range to indicate the weeks a task is being worked on, based on the start week and calculated duration
the background color should correspond to the task role, based on the colors set in the "Roles" sheet
If the above isn't clear, here is a link to a video where I try to explain what the program should do
The question relates to onEdit but the trigger is, at this point of development of the script, irrelevant, since the script is littered with syntax errors.
Even so, I suggest that the trigger is irrelevant in any event. The script can/should be triggered when the data has been populated. This trigger can be done manually, or (perhaps) via a menu option.
getRange(), getLastRow() and getLastColumn are sheet-based methods.
Incorrect
var headerRow = ss.ganttSheet.getRange('headerRow').getRow()
var lastRow = ss.ganttSheet.getLastRow()
var lastCol = ss.ganttSheet.getLastColumn()
Correct
var headerRow = ganttSheet.getRange('headerRow').getRow()
var lastRow = ganttSheet.getLastRow()
var lastCol = ganttSheet.getLastColumn()
Other
startWeekCol is not defined
for (var j = 0; j < lastCol; j++)
"j" substitutes for the column number, but a value of 0 is invalid
var taskTimeRange = ganttSheet.getRange(i,j - 1,(currentTaskExpDuration*2 +1), 1)
when "j" is 1, "j-1" resolves to 0 (zero) which is invalid
for (var k = 0; k < lastRow; k++)
"k" substitutes for the row number, but a value of 0 is invalid

Trying to copy cell value to different column(s) dependent on variable value

i'm trying to transpose an array to a column dependent on a variable value.
Essentially what i'm trying to do is build a script which will transpose an input sheet, and depending on the language used in the specific input sheet row, transpose that rows values to a different column on an output.
i.e. depending on column value (C), values in columns A,B transpose to Column I,J,K depending on column value C.
Google sheets link: https://docs.google.com/spreadsheets/d/1wuBfK4PKg79vCGGhsBPmHPT7hjfYNdR0srvX9nj0Ajs/edit?usp=sharing
I've already found a simple copyto script which works if it's one array with same language, but I need it to offset based on language.
Any help will be appreciated.
example code:
//missing the offset
function transpose() {
var sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange("A2:C").copyTo(sheet.getRange("F2"),
SpreadsheetApp.CopyPasteType.PASTE_VALUES,true);
}
Proposed Script
function createReport() {
// Initilaize Sheets, Ranges, Values
let file = SpreadsheetApp.getActive();
let sheet1 = file.getSheetByName("Blad1");
let range1 = sheet1.getDataRange();
let values1 = range1.getValues();
let sheet2 = file.getSheetByName("Blad2");
let range2 = sheet2.getDataRange();
let values2 = range2.getValues();
// Deal with headers
let langs = values2[0];
values1.shift(); // to remove headers
// Creating array of sub arrays with info to paste into report
// In this format:
// [[Column to paste in, Input 1, Input 2, Input 3]]
let output = [];
values1.forEach((row, i) => {
let outputRow = [];
let whichCol = langs.findIndex((i) => i == row[3]) + 1;
outputRow.push(whichCol);
for (let i = 0; i < 3; i++) {
outputRow.push(row[i]);
}
output.push(outputRow);
});
// With output array, pasting into report
output.forEach((entry) => {
let col = entry.shift();
// Find where the next free slot is in column
let occupiedRange = sheet2
.getRange(1, col)
.getDataRegion(SpreadsheetApp.Dimension.ROWS)
let height = occupiedRange.getHeight();
// Transposing array
set = entry.map((val) => [val]);
// Inserting Values to Report
let outRange = sheet2.getRange(height + 1, col, 3, 1);
outRange.setValues(set);
});
}
Source Data in Blad1
Destination Template and script in action in Blad2
Explanation
You'll notice its quite a bit longer than your script! What you are trying to do is deceptively complex, which is why I hesitated to answer fully as this script is so far removed from what you initially posted that it almost seemed like it was a "give me the code" question. Though you are new on the site and I had already written out most of the code, so what the hell. In future please try to include more info in your original question, your attempts and research. I have tried to keep it an concise as possible, but there may be certain syntax that you haven't come across, like forEach and map.
The script first gets the data with getValues that returns 2D arrays of the values.
I take out the headers on the source data, and use the headers on the target data to find the column index where the source data will end up. So ENG is index 1, and X index 2 etc.
For each row in the source data it transforms it into an intermediary array (which is not necessary, but I think its clearer to understand each step). The intermediary array is composed of sub arrays representing each "set". Each sub array has this format [Column_Index, Input1, Input2, Input3].
Once this has been build, each of those sub arrays can be gone through to insert them into the output sheet, which I have called the "Report".
Within this process is the need to get the first unoccupied row of the target column. So if ENG already has 3 sets that have been filled in, the script needs to know where the next set starts. It does this by using getDataRegion(SpreadsheetApp.Dimension.ROWS) then getHeight() + 1 to find the starting row for the set to be inserted.
Also within this final process is the need to transform the array from this format:
[1,2,3]
Which Apps Script understands as a row, to a column, which would be this:
[[1],[2],[3]]
which was done with map.
I encourage you to use Logger.log to log a bunch of the values and inspect the output so that you can understand the script and adapt it to your needs. I have tried to name everything in a "friendly" manner.
References
Map
forEach
Range object
Try this code:
function Sort()
{
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sh = ss.getSheetByName("Blad1")
var input = sh.getRange(1, 1, 4, 4).getValues();
var output = sh.getRange("H1:K1").getValues();
var index = [0]; var nInput = []
for(var j = 1; j < output[0].length; j++)
{
for(var i = 1; i < input.length; i++)
if(output[0][j] == input[i][3]) index.push(i)
}
for(var i = 0; i < input.length; i++) nInput.push(input[index[i]])
var nData = [];
for(var i = 1; i < nInput.length; i++)
{
for(var j = 0; j < nInput[0].length-1; j++)
{
if(nData[j] == null) nData[j] = []
nData[j].push(nInput[i][j])
}
}
sh.getRange(2, 9, nData.length, nData[0].length).setValues(nData);
}
Try this code:
function Sort()
{
var ss = SpreadsheetApp.getActiveSpreadsheet()
var sh = ss.getSheetByName("Blad1")
var input = sh.getRange(1, 1, 7, 4).getValues();
var output = sh.getRange("H1:K1").getValues();
var index = [0]; var nInput = []
for(var j = 1; j < output[0].length; j++)
{
nInput[j-1] = []
for(var i = 1; i < input.length; i++)
if(output[0][j] == input[i][3])
for(var k = 0; k < input[0].length-1; k++) nInput[j-1].push(input[i][k])
}
var nData = [];
for(var i = 0; i < nInput.length; i++)
{
for(var j = 0; j < nInput[0].length; j++)
{
if(nData[j] == null) nData[j] = []
if(nInput[i][j]) nData[j].push(nInput[i][j])
else nData[j].push("")
}
}
sh.getRange(2, 9, nData.length, nData[0].length).setValues(nData)
}
This code will work only if you have same number of inputs (3 in this case) for each language.

How to store data in Array using For loop in Google apps script - pass array by value

Edit: answer at the bottom
Im experiencing some weird behavior I came across when using google script.
I have a 2d array and I insert data into it using for loop.
I noticed that if use
appendRow(someArray[i])
INSIDE the for loop, everything works as expected.
But If try to access data or use appendRow outside of the for loop, it always gives me the data of the last row the ran in the for loop.
so:
appendRow(someArray[1])
gives same result as
appendRow(someArray[2]),appendRow(someArray[3])
when used OUTSIDE of the for loop.
Can anyone tell me whats causes it?
It also happens when im using setValue on a 2d array, I can use it outside of the loop, or all of the rows are identical.
I have spent 2 hours on this simple little thing, and finally understand what causing the problem but I still cant figure out how to fix it.
Im attaching a simple code that explains what the problem, please focus on the second FOR loop.
function myFunctionAppendRowInside() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var data = sheet.getDataRange().getValues();
var newRow = data[5];
var arr = new Array(100,100);
var currentId = 20000;
var productSku = data[5][2];
for (var i = 0; i < 99; i++){
arr[i] = newRow
}
for (var i = 0; i < 99; i++){
target.getRange(targetRow,1).setValue(evento[i]);
arr[i][0] = currentId + i;
arr[i][2] = productSku + i;
sheet.appendRow(arr[i]);
}
//All of them gives the same row, which is the one created in the last run of the FOR loop arr[98]
sheet.appendRow(arr[1]);
sheet.appendRow(arr[2]);
sheet.appendRow(arr[3]);
}
Please explain to me whats causes it and how to overcome it.
Edit : added my code which uses "setValues" , but still experiencing the same problem. My array is populated only with the LAST row created by the "for loop"
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var newSheet = activeSpreadsheet.insertSheet();
newSheet.setName("newSheet");
var data = sheet.getDataRange().getValues();
//Taking the 5th row, using it as a template for rest of rows
var newRow = data[5];
//2d Array
var arr = new Array(100,100);
//data for the only 2 columns who are going to change between rows
var currentId = 20000;
var productSku = data[5][2];
for (var i = 0; i < 99; i++){
newRow[0] = currentId + i;
newRow[2] = productSku + i;
arr[i] = newRow;
}
newSheet.getRange(2, 1, arr.length, arr[0].length).setValues(arr);
}
Second Edit:
So the issue was that "getValues()" returns an array, which I needed to work with.
But Array are passed by reference and not by value, so any changes I made along the code, where changing the original array which I got from "getValues".
The solution:
iterate over the array received from "getValues", and copy the values one by one(cell by cell) to the a new array and only then manipulate it.
I also created a 2d array, which also requires running a "for loop"
Im attaching the working code which does:
1.copy row 13, which includes 51 columns from my original sheet.
2.create an empty 2d array (9999x51).
3. take row 13 and manipulate its columns based of current iteration (ie row '1' will include original data + '1'
The code :
function myFunction() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var activeSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var data = sheet.getDataRange().getValues();
//create Array
var arr = [];
//turn it to a 2 dimension array
for (var i=0;i<10000;i++) {
arr[i] = [];
}
//starting point for our new id which will run from 30000-39999
var currentId = 30000;
//run on our 2 dimension array and manipulate data cell by cell
for (var i=0; i <= 9999; i++){
for (var j=0; j<= data[13].length - 1; j++){
if (j == 0){
var obj = currentId + i;
arr[i][j] = obj;
}else{
if (j == 2){
arr[i][j] = data[13][j] + i;
}else{
arr[i][j] = data[13][j];
}
}
}
}
//copy to new sheet
var newSheet = activeSpreadsheet.insertSheet();
newSheet.setName("newSheet466");
newSheet.getRange(1, 1,10000, 51).setValues(arr);
}
I'm not sure what causes your problem. But instead of appending each row (which can get very slow) it is faster to get a range and set its value. For example:
sheet.getRange(1, 1, arr.length, arr[0].length).setValues(arr);
Where the two 1's are the starting position that you wish to use. For more info check out the documentation on getRange.

Optimal way to set cell values in Google Sheet via Script

I have a template sheet with checkboxes and I want to copy the checked ones to a new sheet. I have a working version that involves adding rows but I am looking for something faster. I thought getting a range of values on both the new and old sheets and working on the arrays would be best but I hit a error:
'Cannot covert Array to Object[][]".
I think the issue has to do with the fact that this is a new unpopulated sheet. The code below is the simplest example of what is happening. Am I doing something wrong, or is this just not possible?
function test(){
var s = SpreadsheetApp.getActiveSpreadsheet().insertSheet();
var r = s.getRange(1,1,5);
var v = r.getValues();
for ( var i=0; i < 5; i++) {
v[i] = i;
}
r.setValues(v); //ERROR: Cannot covert Array to Object[][]`enter code here`
}
It looks like the line v[i] = i; converts the Object[][] to an array. So , i think (bizarre) I need to create a new array[][] asfollows:
function test(){
var s = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var r = s.getRange(1,1,5,1);
var v = r.getValues();
var ta = [];
for ( var i=0; i < 5; i++) {
ta[i] = [];
ta[i].push(i) ;
}
r.setValues(ta);
}
Ok. Here is the full solution.
The function looks for the sheet "Work" that has 2 columns; the first is a checkbox, the second is the string value of interest. For every checked box (value == true), the 2nd column's value, Font weight, and Font size are copied into appropriately 'shaped' structures.
Once constructed, a new sheet is created, a range in the new sheet is retrieved and used to set the values, weights and sizes of a single column.
function copyCheckedItems () {
var cl = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Work');
if (cl) {
var cnt = cl.getLastRow();
var range = cl.getRange(1,1, cnt, 2 );
var values = range.getValues();
var weights = range.getFontWeights();
var sizes = range.getFontSizes();
// Compute data needed for new sheet in the right shape.
var tv = [];
var tw = [];
var ts = [];
var newCnt = 0;
for (var row in values) {
if(values[row][0]){
tv[newCnt] = [];
ts[newCnt] = [];
tw[newCnt] = [];
tv[newCnt].push(values[row][1]);
tw[newCnt].push(weights[row][1]);
ts[newCnt].push(sizes[row][1]);
newCnt++;
}
}
// construct the new sheet in a minimum of calls
var name = Browser.inputBox('Enter WorkSteet name');;
var sheetOut = SpreadsheetApp.getActiveSpreadsheet().insertSheet(name);
var ro = sheetOut.getRange(1,1,newCnt,1);
ro.setValues(tv);
ro.setFontSizes(ts);
ro.setFontWeights(tw);
//Browser.msgBox("Done.");
}
else {
Browser.msgBox('Work sheet not found!');
}
}

Handling time duration in Google Sheets Api

I'm making a basic script that fetches time durations from external sheets, and sums them. What I have so far is:
function getHorasCasoUso() {
var libros = {
"key1" : "externalSheetURL1",
"key2" : "externalSheetURL2",
...
};
var horas_por_caso_uso = {};
for (var key in libros) {
var libro = SpreadsheetApp.openByUrl(libros[key]);
var sheets = libro.getSheets();
for (var i = 0; i < sheets.length; i++) {
var sheet = sheets[i];
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
for (var j = 5; j < numRows; j++) {
var row = values[j];
var caso_uso = row[6];
var horas = row[4]; //The cell format is 'Duration'
if (!caso_uso)
continue;
if (!!horas_por_caso_uso[caso_uso])
horas_por_caso_uso[caso_uso] += horas;
else
horas_por_caso_uso[caso_uso] = horas;
}
}
}
var ss = SpreadsheetApp.getActiveSheet();
for (var key in horas_por_caso_uso) {
ss.appendRow([key, horas_por_caso_uso[key]]);
}
}
The problem is that the data stored in 'horas' is a string. I want to get the time duration in that cell. How can I do that?
Your issue seems quite similar to the one in this post but at a larger scale...
You should convert row[4] value to minutes (or seconds if you need this accuracy) before adding that value to the total counter.
If the cells are formatted as duration (as you say it is) it should work without changes.(see code at the end of this post)
If not, ie if these values are returned as strings then a simple string manipulation will do the job for you like this :
example : testString = 12:34
function toMinutes(value){
var minutes = Number(value.split(':')[0].replace(' ',''))*60+Number(value.split(':')[1].replace(' ',''));
return minutes;
}
(code working as a function) will return 754 (60*12+34)
usage in your code : var horas = toMinutes(row[4]);
function toMinutes(value){
var duration = new Date(value);
var min = duration.getMinutes()+60*duration.getHours();
return min;
}
You can eventually improve this function with a few error trapping features to handle cases where cell is empty of malformed... Since I don't know what the data look like I prefer let you do it yourself.