I have two spreadsheets. I want to match column C of spreadsheet1 with column A of spreadsheet2. Both of these spreadsheet has records more than 8000. Due to huge number of records my script constantly gives exceeded maximum execution time error. Here is the script
function compare() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheetList1=ss.getSheetByName("spreadsheet1");
var sheetList2=ss.getSheetByName("spreadsheet2");
var sheet1Data=sheetList1.getRange(2,3,sheetList1.getLastRow(),1).getValues();
var sheet2Data=sheetList2.getRange(1,1,sheetList2.getLastRow(),1).getValues();
for (i in sheet2Data){
var row = 2;
for (j in sheet1Data){
if (sheet1Data[j][0]==sheet2Data[i][0]){
sheetList1.getRange("A"+row).setValue('Inactive');
}
row++;
}
}
}
any suggestions for optimizing this script. Or how to handle this error ?
Thanks in advance :)
EDIT
Thanks for the wonderful reply. There is one issue. If I push data in newSheet1Data array before the if statement then it write Inactive twice. i.e if there are two rows it writes inactive to four rows.Like
newSheet1Data.push(sheet1Data[j]);
if (sheet1Data[j][2]==sheet2Data[i][0]){
newSheet1Data[j][0]='Inactive';
}
If I push data inside if statement and no match occur then it does not find row and give this error TypeError: Cannot set property "0.0" of undefined to "Inactive". Like
if (sheet1Data[j][0]==sheet2Data[i][0]){
newSheet1Data.push(sheet1Data[j]);
newSheet1Data[j][0]='Inactive';
}
You should avoid any calls to spreadsheet API in a loop, especially when you have so much data in it.
The idea is to play only with arrays and to write the result back once it is finished.
The code below does it (based on you code if data in col C sheet1 is the same as data in sheet2 col A then write a string in sheet1 col A ).
I hope I made no mistake but I didn't have the opportunity to test my code... it might need some debugging ;-)
function compare() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheetList1=ss.getSheetByName("Sheet1");
var sheetList2=ss.getSheetByName("Sheet2");
var sheet1Data=sheetList1.getDataRange().getValues();// get the whole sheet in an array
var sheet2Data=sheetList2.getRange(1,1,sheetList2.getLastRow(),1).getValues();
var newSheet1Data = [] ; // create a new array to collect data
for (i=0;i<sheet1Data.length;++i){
for (j=0;j<sheet2Data.length;++j){
if(i!=j){continue};
newSheet1Data.push(sheet1Data[i]); // add the full row to target array
if (sheet1Data[i][2]==sheet2Data[j][0]){
newSheet1Data[i][0]='Inactive';//if condition is true change column A
break
}
}
}
newSheet1Data.shift();// remove first row (probably headers ?)
sheetList1.getRange(2,1,newSheet1Data.length,newSheet1Data[0].length).setValues(newSheet1Data); // write back to sheet1 in one batch
}
EDIT : after seing your doc I understand what you want more exactly...
here is the new code (tested on your SS)
function compare() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sheetList1=ss.getSheetByName("Sheet1");
var sheetList2=ss.getSheetByName("Sheet2");
var sheet1Data=sheetList1.getDataRange().getValues();// get the whole sheet in an array
var sheet2Data=sheetList2.getRange(1,1,sheetList2.getLastRow(),1).getValues();
var newSheet1Data = [] ; // create a new array to collect data
for (i=0;i<sheet1Data.length;++i){
newSheet1Data.push(sheet1Data[i]); // add the full row to target array
for (j=0;j<sheet2Data.length;++j){
if (sheet1Data[i][2]==sheet2Data[j][0]){
newSheet1Data[i][0]='Inactive';//if condition is true change column A
break;// don't continue after condition was true, this will speed up the process
}
}
}
newSheet1Data.shift();// remove first row (probably headers ?)
sheetList1.getRange(2,1,newSheet1Data.length,newSheet1Data[0].length).setValues(newSheet1Data); // write back to sheet1 in one batch
}
Related
I am working on a Google sheet script to manage stocks of items in a game, which is supposed to work as such:
People can make request to deposite or withdraw items using a Google form, which send all the infos, including what resource and in what amount, to a first "log" sheet. I then want a script to read these logs, and use them to update a different sheet, which show the actual stocks.
I should mention, there's about 800 different items to stock, and we like to move them around (up or down the list) because we're dumb.
So my idea what the have the script first retrieve the name of the item we made a request for, then try to match it in the stock sheet.
If it can, it should then add or substract the amount to the stock.
If it can't, it should just colour the log line in red so we can see it and redo the request.
My first problem is that I have no idea if a script in Gsheet can stay active for a long time, and the second is that I have even less of an idea how to properly retrieve a string of text and store it, then compare it with others, and that +800 times each time.
Thank you !
From the question
My first problem is that I have no idea if a script in Gsheet can stay active for a long time,
Google Apps Script have quotas. In this case, the corresponding quota is the execution time limit. For free accounts the limit is 6 minutes, for Workspace accounts the limit is 30 minutes.
and the second is that I have even less of an idea how to properly retrieve a string of text and store it, then compare it with others, and that +800 times each time.
Start by reading https://developers.google.com/apps-script/guides/sheets
Tl;Dr.
You need to learn the pretty basics of JavaScript.
You might use the Spreadsheet Service (Class SpreadsheetApp) or the Advanced Sheets Service, i.e.
/**
* Returns the values from the data range of the active sheet
*
*/
function readData(){
const sheet = spreadsheet.getActiveSheet();
const values = sheet.getDataRange().getValues();
return values;
}
You should decide where do you will store the values, then use JavaScript comparison expressions. You might use loops (for, while, do..while, or use Array methods like Array.prototype.forEach()
Here is an example how it could be done for simplest case, for manual firing of the functions.
Let's say you have the log sheet that look like this:
And your data sheet looks like this:
Here is the function that takes all items from the log sheet, sums them and put on the data sheet:
function add_all_items_from_log() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var log = ss.getSheetByName('log').getDataRange().getValues();
// put all data into the object {item1:q, item2:q, item3:q, ...etc}
var obj = {};
for (let [date, item, q] of log) {
if (item in obj) obj[item] += q; else obj[item] = q;
}
console.log(obj);
// convert the object into a 2d array [[item1,q], [item2,q], [item3,q], ...]
var array = Object.keys(obj).map(key => [key, obj[key]]);
console.log(array);
// put the array on the data sheet (starting from second row)
var sheet = ss.getSheetByName('data');
sheet.getRange(2,1,sheet.getLastRow()).clearContent();
sheet.getRange(2,1,array.length, array[0].length).setValues(array);
}
The result:
Here is the function that takes item from the last line of the log sheet and add the item to the data sheet:
function add_last_item_from_log() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
// get item from the last row of the log sheet
var [date, item, q] = ss.getSheetByName('log').getDataRange().getValues().pop();
console.log(date, item, q);
// get data from the data sheet
var sheet = ss.getSheetByName('data');
var [header, ...data] = sheet.getDataRange().getValues();
// put the data into the object {item1:q, item2:q, item3:q, ...etc}
var obj = {};
data.forEach(row => obj[row[0]] = row[1]);
console.log(obj);
// add the item to the object
if (item in obj) obj[item] += q; else obj[item] = q;
// convert the object into a 2d array [[item1,q], [item2,q], [item3,q], ...]
var array = Object.keys(obj).map(key => [key, obj[key]]);
console.log(array);
// put the array on the sheet (starting from second row)
var sheet = ss.getSheetByName('data');
sheet.getRange(2,1,sheet.getLastRow()).clearContent();
sheet.getRange(2,1,array.length, array[0].length).setValues(array);
}
Here is my sheet.
You can run these function manually from Text Editor. Just to see how it works. But actually, as far as I can tell, you better to run the last function (or its variant) automatically every time the log sheet is updated from the Form submit. It can be done with the trigger onFormSubmit().
And this is a simplest case. If you have 800+ items and many columns the code may require some optimizations.
I am transferring data from a (shUser)form to a new (blank)row in a datasheet.
I want to add a running total at the end col. of the row ie into col. 12 of the blankRow. After adding number from shUserForm("E17")-(received)-, subtract the number from shUserform("E15")- paid out- and adding to previous running total from datasheet lastRow col.12.
scripts so far -
function submitData(){
var myGoogleSheet=SpreadsheetApp.getActiveSpreadsheet();
var shUserForm=myGoogleSheet.getSheetByName("TRANSACTIONS");
var shAccount=shUserForm.getRange("E5").getValue();
var datasheet=myGoogleSheet.getSheetByName(shAccount);
var blankRow=datasheet.getLastRow()+1;
//code to update the data in datasheet
datasheet.getRange(blankRow,2).setValue(shUserForm.getRange("E5").getValue());
datasheet.getRange(blankRow,3).setValue(shUserForm.getRange("E7").getValue());
datasheet.getRange(blankRow,4).setValue(shUserForm.getRange("E9").getValue());
datasheet.getRange(blankRow,5).setValue(shUserForm.getRange("E11").getValue());
datasheet.getRange(blankRow,6).setValue(shUserForm.getRange("E13").getValue());
datasheet.getRange(blankRow,7).setValue(shUserForm.getRange("E19").getValue());
datasheet.getRange(blankRow,12).setFormula???XXXXXXXXXXXXXXXXX;
Here is a sample script of how I would do it. Notice I make every effort to combine calls to the server. Rather than a bunch of getValue()/setValue() I use getValues()/setValues() as much as possible. For example, in your script you are getting numerous values from column E. I simply get a 2D array of E using getValues() and then remember which index into that array corresponds to the row. Similar to your setValue(). You are placing values in consecutive columns of the new row. I simply construct a 1D array of that row and then use setValues([dataSheetValues]) to place a 2D array in that row at the proper location.
Because your shUserForm values are changing based on account I would think you wouldn't want to have a bunch of formulas in datasheet link to that sheet, but I have shown how to build the formula in case that is your intention. I've also shown how to calculate the value but have commented it out.
It is also my practice to always enclose my code in a try{} catch(){} block.
Code.gs
function submitData(){
try {
let myGoogleSheet=SpreadsheetApp.getActiveSpreadsheet();
let shUserForm=myGoogleSheet.getSheetByName("TRANSACTIONS");
let shAccount=shUserForm.getRange("E5").getValue();
let datasheet=myGoogleSheet.getSheetByName(shAccount);
let blankRow=datasheet.getLastRow()+1;
let shUserFormValues = shUserForm.getRange(5,5,15,1).getValues();
let dataSheetValues = []; // [[E5],[E7],[E9],[E11],[E13],[E19]
for( let i=0; i<9; i+=2 ) {
dataSheetValues.push(shUserFormValues[i][0]); // E5 to E13
}
dataSheetValues.push(shUserFormValues[14][0]); // E19
//code to update the data in datasheet
datasheet.getRange(blankRow,2,1,6).setValues([dataSheetValues]);
// to perform the running total
// If you only want the value
// let previousValue = dataSheet.getRange(blankRow-1,12).getValue();
// let runningTotal = previousValue+shUserFormValues[12][0]-shUserFormValues[10][0];
// datasheet.getRange(blankRow,12).setValue(runningTotal);
// If you prefer a formula
let sumFormula = "="+shAccount+"!L"+(blankRow-1)+"+TRANSACTIONS!E17-TRANSACTIONS!E15";
datasheet.getRange(blankRow,12).setFormula(sumFormula);
}
catch(err) {
Logger.log(err);
}
}
Reference
Best Practices
Sheet.getRange()
Range.getValues()
Range.setValues()
Range.setFormula()
Array.push()
Error handling
I have two ranges of equal size on different sheets in the same spreadsheet. I am trying to find a row (based off of user input) in the first sheet and then use that index to modify a table in the second sheet that counts how many times that certain index has been used before (to make a nice looking pie chart).
This code runs but will not produce results on the second sheet. I've gone through the debugging process and my best guess is that for some reason, my for in loop is not running through. Attached is my code that takes in the beforementioned index and attempts to perform the second half of my goal.
function acceptToEncounterChart(ghostrow) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
SpreadsheetApp.setActiveSheet(ss.getSheets()[1]);
ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Average Encounter Chart");
var range = sheet.getRange("B3:B14")
for(var i in range) {
if(ghostrow == i) {
var before = range[i][0].getValue()
range[i][0].setValue(before + 1);
}
}
SpreadsheetApp.setActiveSheet(ss.getSheets()[0]);
};
Explanation:
I am not entirely sure what is your goal.
However, here is some fixes / improvements starting from the beginning:
You define 2 times the same variable ss with exactly the same value.
You don't need to set the active sheet, if your goal is to just get the sheet, therefore this line is redundant:
SpreadsheetApp.setActiveSheet(ss.getSheets()[1]);
Variable range is not an array but a range object. You can't index it and therefore you can't also use a for loop to iterate over a single object. For the same exact reason, the code inside the if statement is wrong, you can't index range. But you don't see any errors because the if statement evaluates to false.
In JavaScript and in many other programming languages, array indexes start from 0. Since your range starts from cell B3 or row 3, you need to use i+3 to match the data with the range.
For the same reason as the previous point, ghostrow is an index, not a row. The if statement compares an array index i with ghostrow, so ghostrow should not be confused with the actual sheet row. For example, if you choose ghostrow=5 then the current script will increment the value of the cell B8 (remember i+3) by 1.
Solution:
Here is a workable code snippet:
function acceptToEncounterChart(ghostrow) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Average Encounter Chart");
var data = sheet.getRange("B3:B14").getValues().flat();
data.forEach((v,i)=>{
if(ghostrow == i){
sheet.getRange(i+3,2).setValue(v+1)
}
});
ss.setActiveSheet(ss.getSheets()[0]);
}
Related:
Please explore the official google apps script documentation.
I'm using Google Spreadsheets for this:
I have a spreadsheet which is basically a 4-week planner. Each day is divided into several slots, which can be assigned to any of our active clients. These cells have validation rules which reject invalid values.
The data that is permitted by the validation rules is sourced from a list on a separate sheet, which filters out clients when their status is changed from 'Active' to 'Cancelled', meaning they can no longer be assigned. The status is changed manually. Once an assigned client changes to 'Cancelled', it becomes an invalid client on the calendar.
Is there a way, using scripts, to find and clear the values of cells containing these invalid values? I've included a screen clipping below. The red corner is the invalid value.
I already have the onEdit trigger set up to run code, this will be calling a function to deal with this specific area.
screen clipping
Any help will be appreciated.
The code would look something like this:
function onEdit(e) {
//First check if you want the entire code to execute
if (myNeededCondtion !=== "theValueToMach") {
//End the code here
return;
}
var mySpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var theSheet = mySpreadsheet.getSheetByName("name of sheet");
var arrayOfColumnValues = theSheet.getRange(row to start at, column to start at, numRows, numColumns).getValues();
var i=0;
var thisValue = "";
for (i=0;i<arrayOfColumnValues.length;i+=1) {
thisValue = arrayOfColumnValues[i][0];
if (thisValue==="Cancelled") {
//Set the cell value to a blank string
theSheet.getRange(i, column).setValue("");
};
};
};
You need to figure out what the range value parameters need to be, and edit the code. Add the correct sheet name to the getSheetByName method. Note that getValues() returns a two dimensional array. Each inner array represents a row. If you only get one column of data, then each inner array will only have one element in it. Arrays are indexed starting at zero.
So, I'm trying to write a script using the onEdit() event, which will basically remove links that are duplicates (technically, it removes everything, and only puts back things which aren't duplicates).
My code works fine all the way until it's time to write back non-duplicates. Namely, the line in which I use range.setValues(). I understand that it needs an array of arrays of cells which to edit, and that said array needs to fit in the range.
So far, I have :
if (unique)
{
newData.push(editedRow[0]);
Browser.msgBox(newData);
}
Unique is a variable I use that is false if an exact entry was found. With the msgBox command, I can verify that newData contains what it needs to contain. Further down, I have :
newDataFinal = [newData];
Browser.msgBox('Put values '+newDataFinal+' in range ' +range.getA1Notation());
range.setValues(newDataFinal);
To my knowledge, this should make NewDataFinal an array of arrays, which I can verify if I change setValues() to setValue(), which writes [[22.0, 13.0, 23.0]] (for my example) in the spreadsheet, which looks like an array of arrays to me.
The range should also match, since for this example, I get a prompt along the lines of "Put values 22,13,23 in range B2:B4" from the msgBox, which seems as a fitting range.
So, what am I doing wrong?
Here's the rest of the code (please excuse the abundancy of comments/msgboxes and lack of elegancy, the priority is to get it to work, I can probably optimize it and clean it up a bunch afterwards) :
function onEdit(e)
{
var range = e.range;
var values = range.getValues();
var sheet = SpreadsheetApp.getActiveSheet();
if (sheet.getName() != 'testiranje') return;
newData = new Array();
// Browser.msgBox(range.getA1Notation());
range.clear();
var data = sheet.getDataRange().getValues();
var counter = 0;
for (editedRowIndex in values)
{
unique = true;
editedRow = values[editedRowIndex];
// Browser.msgBox('Edited Row ' +editedRow);
for(i in data)
{
var row = data[i];
// Browser.msgBox('Old Row '+row);
for (j in row)
{
// Browser.msgBox(row[j] + ' vs ' + editedRow[0])
if (editedRow[0] == row[j])
{
Browser.msgBox('Hit! '+editedRow[0]);
unique = false;
}
}
}
if (unique)
{
// Browser.msgBox('Pushing '+editedRow[0]+' in newdata');
newData.push(editedRow[0]);
Browser.msgBox(newData);
}
}
newDataFinal = [newData];
Browser.msgBox('Put values '+newDataFinal+' in range ' +range.getA1Notation());
range.setValues(newDataFinal);
// range.setNote('SCIENCE');
}
I didn't test your code because I didn't feel like creating a sheet for it but what I can suggest (that should solve this issue in any case) is to replace your range.setValues(newDataFinal); with this :
sheet.getRange(range.getRowIndex(),range.getColumnIndex(),newDataFinal.length,newDataFinal[0].length).setValues(newDataFinal);
And if you want to know why the range and array didn't fit you can use this code :
(I used Browser because you seem to like it... I prefer Logger.log)
Browser.msgBox('data height = '+newDataFinal.length+', data width = '+newDataFinal[0].length+' and range height is '+range.getHeight()+', range width is '+range.getWidth()+' ... does it fit ?');
Note : I'm almost sure that your initial range is bigger than the newData array since you remove elements from the initial data... My best guess would be that heights don't fit. (but that's only a guess ;-) since you didn't mention the error message you get...)
the problem is that you cant change cells from an onEdit handler. see the docs. instead install your own onEditCustom handler.