How do I write data in every last column? - google-apps-script

I need to add a new column everyday and the data insertion will be done individually for the specific row comparing with the input... please tell me if it is correct or not.
will this piece of code work for adding new columns ever day and initialize all the entries by 0?
function trig(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1);
var trigger = builder.create();
}
function addcol(){
var cname = builder.atDate( day, month, year)
var column = eventRange.getLastColumn();
sheet.insertColumnAfter(column).setName(cname);
sheet.getRange("E1").setValue(new Date()).setNumberFormat('d/M/yyyy');
var col = [];
for(var n=0 ; n<s.getMaxRows();n++){
col.getLastColumn().push(['0']);
}
ss.getRange('N:N').setValues(col);
}
// now for the insertion part
here the sr will be compared to SRN from the sheet (E) and if it matches it will replace 0 with 1 in the last column added everyday. plese tell me will this work?
function doPost(e){
var action = e.parameter.action;
if(action == 'scanner'){
return scanner(e);
}
}
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for(i=1; i<=R; i++)
{
if (srn == sheet.getDataRange([i][2]))
{
sheet.getDataRange([i],[C]).push[(1)];
sheet.append([i],[C]);
return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);
break;
}
}
}

Time-based trigger:
There are no event objects associated with time-based triggers, so variables like eventRange cannot work. It seems like you want to use variables in addcol that are defined in trig (e.g. builder). That is not possible. Also, if you want your function to run once a day, there is no need for lines like this: builder.atDate(day, month, year)). The trigger will be created by running this function once:
function createTrigger(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1).create();
}
Adding column with 0's:
There are many problems with the function addcol:
Several uninitialized variables are being used (s, builder, eventRange).
Unexisting methods are being used: e.g.: setNumberFormat is a method of the Range class, not of the Date object. You should use Utilities.formatDate(date, timeZone, format) to format dates. Also, you are using setName when inserting a new column, but that changes the sheet name. Is that what you want to do? And also, cname is assigned a trigger builder as value, which I seriously doubt is your purpose. The same way, an array col does not have a method getLastColumn().
You could use this addcol function instead (change your sheet name, currently set to Your sheet name, and the timeZone in formatDate, currently set to GMT:
function addcol() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Your sheet name"); // Change accordingly
var lastCol = sheet.getLastColumn();
var lastRow = sheet.getLastRow();
if (sheet.getMaxColumns() === lastCol) sheet.insertColumnAfter(lastCol);
var newCol = sheet.getRange(1, lastCol + 1, lastRow, 1);
var values = [];
values.push([Utilities.formatDate(new Date(), "GMT", "d/M/yyyy")]); // Change accordingly
for (var i = 1; i < sheet.getLastRow(); i++) {
values.push([0]);
}
newCol.setValues(values);
}
Replacing 0's with 1's:
Assuming that you are getting the function scanner to run correctly and that the parameter e.parameter.sr is getting populated correctly, you can do the following:
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for (i=1; i<=R; i++) {
if (srn == sheet.getRange(i, 2).getValue()) {
sheet.getRange(i, C).setValue(1);
}
}
}
Here you were also using unexisting methods or providing incorrect parameters:
The method getDataRange doesn't allow any argument, you should be using getRange(row, column), and provide the row and column indexes separated by commas, not as if trying to access a 2D array.
break terminates the current loop, so only use it if you only want to update 1 cell. The same goes for return which finishes current function execution.
Reference:
Spreadsheet Service
Installable Triggers

Short
No
Long
There are several problems with the script:
getDataRange() expects no arguments passed (docs only say it is the same as using getRange(yourSheet.getLastRow(), yourSheet.getLastColumn()), not that you should do it). Certainly it does not expect instances of Array (bracket [] notation wraps C and i, which are of type Number into one). Moreover, it returns a Range, which at the time of writing does not have push() method.
getLastColumn() returns an instance of Number, and thus does not have a push() method as well. You are on the right track, though, since col is an Array, and you need to push() into it.
If you want the script to add a zero-filled column, don't get constant ranges: in current state, getRange('N:N') guarantees that each time you will re-initialize column N. Btw, same goes for getRange("E1").
You still haven't addressed issues listed in comments to your previous question.
Also, in your scanner function there is a syntax error: push[(1)] should be push([1]).
Also, the sheet variable is either undeclared or is declared globally, which is bad.
Notes
If you don't expect number of students to change dynamically, you can switch from getMaxRows() to getLastRow() to only zero-fill cells that are in range of cureent student info grid.
This question is a direct continuation of a currently closed one (please, always disclose that for reference at least).
How about skipping init to zero step at all? If cell is empty, getValue() / getValues() will return its value as an empty string, which is a falsy value, just as 0 is. If you want to count attendance at the end of period, a simple conditional will suffice to sum up.
The default MIME type for TextOutput instance obtained by createTextOutput() is plain text, so setting it to ContentService.MimeType.TEXT is an overkill in your case.
Reference
getDataRange() docs
getLastColumn() docs
getValue() docs
getValues() docs
Range docs
createTextOutput() docs
Falsy values explanation on MDN

Related

Need to optimize Apps Script function [duplicate]

I've just written my first google apps scripts, ported from VBA, which formats a column of customer order information (thanks to you all of your direction).
Description:
The code identifies state codes by their - prefix, then combines the following first name with a last name (if it exists). It then writes "Order complete" where the last name would have been. Finally, it inserts a necessary blank cell if there is no gap between the orders (see image below).
Problem:
The issue is processing time. It cannot handle longer columns of data. I am warned that
Method Range.getValue is heavily used by the script.
Existing Optimizations:
Per the responses to this question, I've tried to keep as many variables outside the loop as possible, and also improved my if statements. #MuhammadGelbana suggests calling the Range.getValue method just once and moving around with its value...but I don't understand how this would/could work.
Code:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getRange("A:A").getLastRow();
var row, range1, cellValue, dash, offset1, offset2, offset3;
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
range1 = s.getRange(row + 1, 1);
//if cell substring is number, skip it
//because substring cannot process numbers
cellValue = range1.getValue();
if (typeof cellValue === 'number') {continue;};
dash = cellValue.substring(0, 1);
offset1 = range1.offset(1, 0).getValue();
offset2 = range1.offset(2, 0).getValue();
offset3 = range1.offset(3, 0).getValue();
//if -, then merge offset cells 1 and 2
//and enter "Order complete" in offset cell 2.
if (dash === "-") {
range1.offset(1, 0).setValue(offset1 + " " + offset2);
//Translate
range1.offset(2, 0).setValue("Order complete");
};
//The real slow part...
//if - and offset 3 is not blank, then INSERT CELL
if (dash === "-" && offset3) {
//select from three rows down to last
//move selection one more row down (down 4 rows total)
s.getRange(row + 1, 1, lastRow).offset(3, 0).moveTo(range1.offset(4, 0));
};
};
}
Formatting Update:
For guidance on formatting the output with font or background colors, check this follow-up question here. Hopefully you can benefit from the advice these pros gave me :)
Issue:
Usage of .getValue() and .setValue() in a loop resulting in increased processing time.
Documentation excerpts:
Minimize calls to services:
Anything you can accomplish within Google Apps Script itself will be much faster than making calls that need to fetch data from Google's servers or an external server, such as requests to Spreadsheets, Docs, Sites, Translate, UrlFetch, and so on.
Look ahead caching:
Google Apps Script already has some built-in optimization, such as using look-ahead caching to retrieve what a script is likely to get and write caching to save what is likely to be set.
Minimize "number" of read/writes:
You can write scripts to take maximum advantage of the built-in caching, by minimizing the number of reads and writes.
Avoid alternating read/write:
Alternating read and write commands is slow
Use arrays:
To speed up a script, read all data into an array with one command, perform any operations on the data in the array, and write the data out with one command.
Slow script example:
/**
* Really Slow script example
* Get values from A1:D2
* Set values to A3:D4
*/
function slowScriptLikeVBA(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
for(var row = 1; row <= 2; row++){
for(var col = 1; col <= 4; col++){
var sourceCellRange = sh.getRange(row, col, 1, 1);
var targetCellRange = sh.getRange(row + 2, col, 1, 1);
var sourceCellValue = sourceCellRange.getValue();//1 read call per loop
targetCellRange.setValue(sourceCellValue);//1 write call per loop
}
}
}
Notice that two calls are made per loop(Spreadsheet ss, Sheet sh and range calls are excluded. Only including the expensive get/set value calls). There are two loops; 8 read calls and 8 write calls are made in this example for a simple copy paste of 2x4 array.
In addition, Notice that read and write calls alternated making "look-ahead" caching ineffective.
Total calls to services: 16
Time taken: ~5+ seconds
Fast script example:
/**
* Fast script example
* Get values from A1:D2
* Set values to A3:D4
*/
function fastScript(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
var sourceRange = sh.getRange("A1:D2");
var targetRange = sh.getRange("A3:D4");
var sourceValues = sourceRange.getValues();//1 read call in total
//modify `sourceValues` if needed
//sourceValues looks like this two dimensional array:
//[//outer array containing rows array
// ["A1","B1","C1",D1], //row1(inner) array containing column element values
// ["A2","B2","C2",D2],
//]
//#see https://stackoverflow.com/questions/63720612
targetRange.setValues(sourceValues);//1 write call in total
}
Total calls to services: 2
Time taken: ~0.2 seconds
References:
Best practices
What does the range method getValues() return and setValues() accept?
Using methods like .getValue() and .moveTo() can be very expensive on execution time. An alternative approach is to use a batch operation where you get all the column values and iterate across the data reshaping as required before writing to the sheet in one call. When you run your script you may have noticed the following warning:
The script uses a method which is considered expensive. Each
invocation generates a time consuming call to a remote server. That
may have critical impact on the execution time of the script,
especially on large data. If performance is an issue for the script,
you should consider using another method, e.g. Range.getValues().
Using .getValues() and .setValues() your script can be rewritten as:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getLastRow(); // more efficient way to get last row
var row;
var data = s.getRange("A:A").getValues(); // gets a [][] of all values in the column
var output = []; // we are going to build a [][] to output result
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
var cellValue = data[row][0];
var dash = false;
if (typeof cellValue === 'string') {
dash = cellValue.substring(0, 1);
} else { // if a number copy to our output array
output.push([cellValue]);
}
// if a dash
if (dash === "-") {
var name = (data[(row+1)][0]+" "+data[(row+2)][0]).trim(); // build name
output.push([cellValue]); // add row -state
output.push([name]); // add row name
output.push(["Order complete"]); // row order complete
output.push([""]); // add blank row
row++; // jump an extra row to speed things up
}
}
s.clear(); // clear all existing data on sheet
// if you need other data in sheet then could
// s.deleteColumn(1);
// s.insertColumns(1);
// set the values we've made in our output [][] array
s.getRange(1, 1, output.length).setValues(output);
}
Testing your script with 20 rows of data revealed it took 4.415 seconds to execute, the above code completes in 0.019 seconds

How can I avoid a VLOOKUP Error that occurs when I delete a name?

The pictures used are only from an example sheet! My basic problem is that I have a list called Assignment in which names appear (dropdown list). For Location (in the assignment sheet) I use the following formula: =IF(C2<>"",VLOOKUP(C2,'Input Data'!C$3:D$7,2,FALSE),"")
These names are assigned certain values, they are in the same line. The names are defined in a worksheet called Input Data!
If I now delete a name like Green, John from the Input Data worksheet, then I get the following error in another worksheet (Evaluation). (More than 40 people have access to this worksheet and randomly delete names)In this evaluation worksheet the values are evaluated by the following formula:
=ARRAY_CONSTRAIN(ARRAYFORMULA(SUM(IF((IF($B$2="dontcare",1,REGEXMATCH(Assignment!$E$3:$E$577,$B$2 &"*")))*(IF($B$3="dontcare",1,(Assignment!$E$3:$E$577=$B$3)))*(IF($B$4="dontcare",1,(Assignment!$D$3:$D$577=$B$4)))*(IF($B$5="dontcare",1,(Assignment!$F$3:$F$577=$B$5)))*(IF($B$6="dontcare",1,(Assignment!$B$3:$B$577=$B$6))),(Assignment!S$3:S$577)))), 1, 1)
The following error appears in the evaluation sheet:
Error:
During the evaluation of VLOOKUP the value "Green, John" was not found.
How can I avoid this error? Is it possible to avoid this error with a macro that deletes Names from assignment sheet that are not in the Input data sheet? Do you have any ideas for a code?Maybe a Formula or perhaps a Macro?
example sheet with explanation: https://docs.google.com/spreadsheets/d/1OU_95Lhf6p0ju2TLlz8xmTegHpzTYu4DW0_X57mObBc/edit#gid=1763280488
If what you want to do is make sure that rows are deleted in a sheet when there are incorrect values you could try something like this in Apps Script:
function onEdit(e) {
var spreadsheet = e.source;
var assignment = spreadsheet.getSheetByName("Assignment");
var assignmentRange = assignment.getDataRange();
var assignmentNames = assignment.getRange(3, 2, assignmentRange.getNumRows());
var inputData = spreadsheet.getSheetByName("Input Data");
var inputDataRange = inputData.getDataRange();
var i = 1;
while(assignmentNames.getNumRows() > i){
var currentCell = assignmentNames.getCell(i, 1);
var txtFinder = inputDataRange.createTextFinder(currentCell.getValue());
txtFinder.matchEntireCell(true);
if(!txtFinder.findNext()){
assignment.deleteRow(currentCell.getRow())
}else{
// We are only steping when no elements have been deleted
// Otherwise we would skip rows due to shifting in row deletion
i++;
}
}
}
Explanation
onEdit is a special function name in Apps Script that would execute every time it's parent sheet is modified.
After that we retrieve the spreadsheet from the event object
var spreadsheet = e.source;
Now we get the relevant range in the Assignment sheet. Look at the usage of getDataRange documentation to avoid retrieving unnecessary cell values. And from that range we actually get the specific column we are interested on.
var assignment = spreadsheet.getSheetByName("Assignment");
var assignmentRange = assignment.getDataRange();
var assignmentNames = assignment.getRange(3, 2, assignmentRange.getNumRows());
Now we do the same for the other sheet(Input Data):
var inputData = spreadsheet.getSheetByName("Input Data");
var inputDataRange = inputData.getDataRange();
Note: Here I'm not getting a specified column because I assume that the full name will not repeat in any other column. But if you want you could get the specified range as I have done at Assignment.
After that we want to look for specific values in the Assignment range that don't exist in the Input Data sheet, you should try the TextFinder.
For every name in Assignment you should create a TextFinder. I have also forced to make a whole cell match.
var i = 1;
while(assignmentNames.getNumRows() > i){
var currentCell = assignmentNames.getCell(i, 1);
var txtFinder = inputDataRange.createTextFinder(currentCell.getValue());
txtFinder.matchEntireCell(true);
If txtFinder finds a value the findNext() will evaluate to true. In the other hand when the txtFinder does not find a value it will be null and evaluated to false.
if(!txtFinder.findNext()){
assignment.deleteRow(currentCell.getRow())
}else{
// We are only stepping forward when no elements have been deleted
// Otherwise we would skip rows due to shifting in row deletion
i++;
}
}
}

Clear content in S:S when T:T contains "Copied" Google Script

I am stuck with, at first sight, simple script.
I want to clear a content from cell S when T has value "Copied".
What I have at the moment is this:
function onEdit(e) {
if(e.range.columnStart === 20) {
e.range.offset(0,-1).clearContent();
}
}
I am not sure how to include IF. Also, bear in mind that T column has a formula, so I don't edit it manually, and with this script, it doesn't work.
It doesn't have to be OnEdit, I can set a trigger to run the script every minute which is even better, but it is important to filter it by the value Copied.
To explain a bit more how my file works (example):
1) I add a comment in the cell S5.
2) My second script runs every minute where it copies values from column S to column V.
3) In the column T, I have the formula (=IF(V5<>"",IF(RegExMatch(S5,V5),"Copied",""),"")), which means if the value exist in the column V5 add Copied in cell T5.
4) I am looking for a solution that when cell T:T has "Copied", delete the cell range S:S
Thank you millions!
As #TheWizEd points out the value in T is dependant on the result in another cell. However an OnEdit function does not necessarily have to respond to the range where the change was made. I've used this code to use the OnEdit event to evaluate the values in Column T and then make the relevant change to values in Column S.
Column T uses a for loop to go through the various row, but the relevant value is pushed to array. This allows a single setValues to be executed at the end of the function.
The function should be assigned to the OnEdit trigger for the Spreadsheet.
function so_53469142() {
// Setup spreadsheet and target sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("so_53469142");
// get the last row of data
var lastRow = sheet.getLastRow();
// get range for Column S & T
var values = sheet.getRange(1, 19, lastRow, 2).getValues();
// set counter variable
var i = 0;
var dataArray = [];
var masterArray = [];
// start loop
for (i = 0; i < lastRow; i++) {
// Logger.log("i="+i+", S = "+values[i][0]+", T = "+values[i][1]);//DEBUG
// empty the array
dataArray = [];
// test value of first row in T
if (values[i][1] === "Copied") {
// If value = "Copies then push blank onto array for Column S
dataArray.push("");
} else {
// else push existing value for column S
dataArray.push(values[i][0]);
}
// make the array 2D
masterArray.push(dataArray);
}
// Update values in Column S
sheet.getRange(1, 19, lastRow).setValues(masterArray);
}

Cannot find method getRange(number,(class),number)

I have read all answers about this error, but none of them work for me. So maybe someone has another idea.
I'm trying to pass parameter to function getRange but when I'm trying to invoke this function it shows me error
Cannot find method getRange(number,(class),number)
Here is my code:
function conditionalCheck(color,rangeCondition, rangeSum, criteria){
var condition = SpreadsheetApp.getActiveSheet().getRange(2,rangeCondition,15).getValues();
var val = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,rangeSum,15).getValues();
var bg = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,rangeSum,15).getBackgrounds();
var sum = 0;
for(var i=0;i<val.length;i++){
if(condition[i][0] == criteria && bg[i][0] != color){
sum += val[i][0];
}
}
return sum;
}
And I pass a custom function like:
conditionalCheck("#ffff00",1,3,A3)
This is how the sheet looks like:
I understand that JS trying to guess the type of parameters, that is why it thinks that ex. "rangeCondition" is a class, but I don't know how to give him a Number type.
Funny thing is, that this function works when I open the spreadsheet, but when I'm trying to invoke this function while I'm working it shows me this error. So to update sheet I have to reopen the whole spreadsheet.
I was able to reproduce the error by executing the function directly from the editor.
This behavior makes sense, considering custom function needs to receive a valid parameter. At runtime, your 'rangeSum' parameter is set to 'undefined', which invalidates the getRange() method as it requires a number.
It's actually quite strange that you got your 'for' loop working. In most cases, using the the '+' operator on array values obtained from the sheet will concatenate them into a string, so you'll get '5413' instead of 13 (5 + 4 + 1 + 3)
function calculateSum(column) {
column = column||1; //set the default column value to 1
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(1, column, sheet.getLastRow());
var values = range.getValues();
var sum = 0;
for (var i=0; i < values.length; i++) {
sum += parseInt(values[i]);
}
return sum;
}
Finally, this approach is not very efficient in terms of performance. Once you get the instance of the object, you should use that instance instead of attacking the server with multiple calls like you do in the first 3 lines of your code. For example
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var range = sheet.getRange(1, 1, sheet.getLastRow(), 1);
If you plan on using this custom function in multiple places within the sheet, consider replacing it with the script that processes the entire range - this way you will only perform a single read and write operation to update all values. Otherwise, you may end up filling up your service call quotas very quickly
UPDATE
Instead of creating custom function, create the function that takes the entire range and processes all data in one call. You can test the 'calculateSum()' function above with predefined column number, e.g.
var column = 1 // the column where you get the values from
Upon getting the 'sum' value, write it into the target range
var targetRange = sheet.getRange("B1"); // the cell that you write the sum to
targetRange.setValue(sum);
Finally, you can make that function execute after you open or edit the sheet by appending the following code
function onOpen() {
var ui = SpreadsheetApp.getUi();
var menu = ui.createMenu('Menu')
.addItem('Calculate sum', 'calculateSum')
.addToUi();
calculateSum();
}
function onEdit() {
calculateSum();
}
Calling it from the custom menu would be simple - you just need to create the handler and pass function name as parameter.
I found a solution for a problem with reloading custom function after changing data.
The solution is described here:
Refresh data retrieved by a custom function in google spreadsheet
It showed that we cannot refresh custom function if we don't change inputted parameters because it is cached. So if we change inputted data, a function will reload. We can force a function if we put there timestamp

Custom script returned array overwrite non-empty cells

Is it possible to make a script returning an array of values that will overwrite non-empty cells?
For example: I have a list of tasks with the following fields: 'task-name, estimated time as in '3w'. The 3rd column is calculated with a custom function that translate the '3w' to 3*5=15 (in days). The custom script takes a range and returns an array of results for the given array of estimated-times from the 2nd column.
All works well until I decide to take a row and move it to re-order the list of tasks. Once I move that row, lets say, one row above, the script complains about an error because the moved row contains a non-empty value where it used be automatically computed by the custom-script. To correct it I need to clear the value of that row at the 3rd column and the script returns to calculate as before.
There should be a way to tell the script to overwrite. Is there?
Thanks,
Erez
Use the isBlank() function to check whether the specified cell in empty and the clear() function in order to clear the value in the cell and provide a new one. Here is a small sample:
function myFunction(){
var ss = SpreadsheetApp.getActiveSheet();
var range = ss.getDataRange();
var maxCol = range.getLastColumn();
var maxRow = range.getLastRow();
for (var i=0; i<maxRow; i++)
{
for (var j=0; j<maxCol; j++)
{
if(!ss.getRange(i+1, j+1).isBlank())
{
ss.getRange(i+1, j+1).clear();
}
}
}
}