Insert formula in a Google Sheets programmatically - google-apps-script

I need to put a formula into a cell in each new row added to a Google Sheets. I have this working in VBA but not been able to build it correctly in Script.
I loop through i rows until lastrow. In cell J, I want this formula inserted:
var Discount = '=IF(ISBLANK("F"+i,,IF(ISNUMBER(FIND("CM","B"+i)),IF("C"+i>"F"+i,150,0),0))';
I use this method to add the row:
var dtaCollect = ["","",StartDate,CustomerName,Monthly,"",Discount,LateFee,TotalPaid,Commission,Note,Referral];
target_sheet.appendRow(dtaCollect);
i++;
} else {
i++;
}
}
However, the formula is written exactly as above, without i substituted with the iteration value. As a result I get #ERROR! in the cell. I've tried INDIRECT and concat.
How can I fix this?

The value i isn't being substituted in your string because it's just text. You need to break it out of the string, and be more careful with your use of quotes to ensure you end up with a viable formula. This would work:
var Discount = '=IF(ISBLANK(F'+i+',,IF(ISNUMBER(FIND("CM",B'+i+')),IF(C'+i+'>F'+i+',150,0),0))';
Since you're using A1Notation, a simple JavaScript String.replace() should be all you need to provide a more readable solution:
var Discount = '=IF(ISBLANK(F%row%,,IF(ISNUMBER(FIND("CM",B%row%)),IF(C%row%>F%row%,150,0),0))'
.replace(/%row%/g, i.toString());
Explanation:
replace() will find regexp or substring matches, and replace them with a new substring.
in this case, we're looking for a regexp; the g flag means we'll look for all occurrences of "%row%" and replace them with the value of i.
We've used the % as bookends, to make the replaceable text stand out clearly - just a convention, not a requirement.
Note: You didn't show how you used INDIRECT, only mentioned that you tried it. It is an alternative here, and might be preferred as you could simply copy a formula from an existing cell without worrying about adjusting the references.

Related

How to use custom function ExtractAllRegex() as an array formula? [Google Sheets]

I'm using #Wiktor Stribiżew 's custom function ExtractAllRegex(). The script extracts all occurrences of a Regex pattern. In the example, I extract all words in column A starting with "v_"
Here is a Google Sheet showing what I'm trying to do.
The original strings are stored in column A. The custom function/the matches are in column B.
Wictors function works great for single cells. It also works great when I manually drag the formula down the column.
Here's Wictor's original code:
function ExtractAllRegex(input, pattern,groupId,separator) {return Array.from(input.matchAll(new RegExp(pattern,'g')), x=>x[groupId]).join(separator);}
Description:
input - current cell value
pattern - regex pattern
groupId - Capturing group ID you want to extract
separator - text used to join the matched results.
The question is, how do I turn column B into a working array formula? Or, perhaps better, how do I modify Wictor's script so it accepts a range instead and auto-fills down column B?
I updated your script to:
function ExtractAllRegex(input, pattern,groupId,separator) {
return input.map ? input.map( inp => ExtractAllRegex(inp, pattern, groupId, separator)) :
Array.from(input.matchAll(new RegExp(pattern,'g')), x=>x[groupId]).join(separator);
}
and changed the formula in B2 to
=ExtractAllRegex(A2:A13,"(v_.+?\b)",0," ")
See if that works for you?

Error when trying to add code back into cleared sheet using Set Formula

I am attempting to create a manual archive function in a google sheet (based on form responses). It's a bit of a Frankenstein effort at this point as I've gathered bits and pieces to put it together. I am so close to completion, but I've hit a wall when I try to add formulas back into the sheet after clearing it to the archive.
I'm sure it's no surprise that am new to this, and I feel like I am missing something simple here. The formulas below are copied directly from the active spreadsheet where they are working fine, but for some reason, I can't get the script to parse in order to put them back after clearing the sheet. I would appreciate any assistance anyone is willing to offer.
I get the error:
Missing ) after argument list.
on the two lines of "cell.setFormula" code that won't "code block" below:
function addFormulas(){
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var sourcesheet = sheet.getSheetByName("Form Totals");
//Add formula back into Column A
var cell = sourcesheet.getRange("A2:A1000");
cell.setFormula("=D2");
//Add formula back into Column U
var cell = sourcesheet.getRange("U2:U1000");
cell.setFormula("=IF(ISNUMBER(FIND("14.29",P2)),14.29,IF(ISNUMBER(FIND("5.24",P2)),5.24,""))");
//Add formula back into Column V
var cell = sourcesheet.getRange("V2:V1000");
cell.setFormula("=IF(ISNUMBER(FIND("14.29",U2)),U2*Q2,IF(ISNUMBER(FIND("5.24",U2)),U2*Q2,""))");
}
What's going on here? Are my formulas "wrong" even though they work in the spreadsheet?
In JavaScript, double quotes are used to denote string type. If you place anything inside " ", it tells the JS code parser to treat anything between these double quotes as a string.
String is also the only valid argument type for setFormula() method of the Range class, so anything that goes inside the brackets should be of this type.
Take a look at this part in your formula
cell.setFormula("=IF(ISNUMBER(FIND("14.29",
The syntax parser would recognize the part between the first pair of double quotes as a string, but 14.29 will be treated a a number as that's where the string ends. The parser will immediately stop and throw an error.
The solution is to use single quotes for strings inside your formula, e.g.
range.setFormula("=IF(ISNUMBER(FIND('14.29',P2)),14.29,IF(ISNUMBER(FIND('5.24',P2)),5.24,''))");
You must be carefull with the use of the "". Like you write it, the console get it like
cell.setFormula("=IF(ISNUMBER(FIND("
and he don't find the missing ).
You should write it like
cell.setFormula("=IF(ISNUMBER(FIND(\"14.29\",U2)),U2*Q2,IF(ISNUMBER(FIND(\"5.24\",U2)),U2*Q2,\"\"))");

Update script cell references when columns are moved

We're migrating a lot of our business logic to scripts behind the scenes, but I'm worried that they'll be much more fragile when columns move.
On Sheet Updates Automagically
For example, If I have a formula on a spreadsheet like this:
=If(A1=5,"Yes","No")
And then I Insert 1 Column Left of A, the formula will be automatically updated like this:
=If(B1=5,"Yes","No")
Apps scripts doesn't update
For example, if I have the formula in the script section:
function myFunction() {
var value = SpreadsheetApp.getActiveSheet().getRange("A1").getValue();
var output = (value == 5) ? 'Yes' : 'No';
Logger.log(output);
}
It will not update when the sheet changes.
Q: How can I get stable references in the code behind for columns that could potentially move?
This is a general problem when hardcoding strings or numbers in code.
In general the javascript parser can't tell which strings might be used on a sheet function call. Its sometimes not trivial to solve.
Two approaches are:
If the columns/cells/ranges are known beforehand, use named ranges:
Define a named range and use NamedRange in code. Use the range to directly write to it or query its row/column position.
Another for column based ranges like yours is that your code does this naming manually by using the column header as the column names. Code uses those names and reads the header to build the mapping.

How to evaluate a spreadsheet formula within a custom function?

In a spreadsheet I can enter =SIN(45)+123 in a cell, and it will be evaluated.
How can I evaluate spreadsheet functions within a custom function, something like an "eval"
function that would work like this :
function myFunc() {
return Sheet.eval("=SIN(45)+123")
}
is it possible ?
Note that I don't care about the SIN function in particular, what I want is to have access to the complete arsenal of spreadsheet functions (PMT, QUERY, NPER, etc..)
Spreadsheet functions from Apps-Script
Not possible - This has been asked many times. Suggest you check the google-apps-script issue list to see if anything has changed. But last I checked, there is no way to do it, and they have no plan to add it. https://code.google.com/p/google-apps-script-issues/issues/list
Ethercalc - java script spreadsheet formulas
If you need to, you can always copy the code from "ethercalc" as it has a java script versions of the spreadsheet formulas.
https://github.com/audreyt/ethercalc
I know this is an old question, but it might help someone.
just assign the formula to a range, then grab the value.
//asign a formula to the range
var range = sheet.getRange("A1").setFormula("=SUM(D1:D100)");
//get the result
var result = range.getValue();
//clean up
range.setFormula("");
I got this working. Using set value will do the trick. Thus something like this:
function MyFun1(){
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange('A1').setValue(Myfun2())
}
function MyFun2(){
return "=SIN(45)+123"
}
Hope this helps!
I think you need to divide this issue up into two different concerns.
Do you want to grab data that is already on the spreadsheet, perform a calculation, and then print a result, or do you want to use the sin() function on calculations in code unrelated to the data in the spreadsheet?
If you are trying to do the latter, you should be able to reference spreadsheet functions by using Math.sin() in your Google Apps Script. For more information on using the sin() function in JavaScript, check this post out: http://www.w3schools.com/jsref/jsref_sin.asp
If you are trying to do the former, then what you should do is use a readRows() function (more information available here: http://gassnippets.blogspot.com/2012/11/create-your-first-google-apps-script.html) to load your spreadsheet data into a variable (or variables) in memory, perform your calculations, and print the final result out to the spreadsheet using a similar function.
Let me know if this helps.
I came across this question in an attempt to find a way to evaluate part of a function like it is possible in Excel.
Here is my dirty workaround - instead of outputting the result in an msgbox, you could simply store the value or displayvalue of the activecell in a variable and use it to your liking.
Notice however, that the function will temporarily overwrite whatever you have in your currently selected cell and it will need to recalculate the sheet before the result is available. Hence it's not a viable solution if you need to evaluate multiple cell values.
function evalPart() {
var ui = SpreadsheetApp.getUi();
myPart = Browser.inputBox("Enter formula part:", ui.ButtonSet.OK_CANCEL);
if (myPart != "cancel") {
myActiveCell = SpreadsheetApp.getActiveSpreadsheet().getActiveCell();
myBackup = myActiveCell.getFormula();
myActiveCell.setFormula(myPart);
Browser.msgBox("Result of \\n \\n" + myPart + " \\n \\n " + myActiveCell.getDisplayValue());
myActiveCell.setFormula(myBackup);
}
}
I don't know if it's possible with high-level functions. However, it's possible with some common and easy-to-understand functions like (sum, subtract etc).
Following is the code I used to set values after the calculation is done in scripting itself.
function MyFun1() {
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange('A1').setValue(MyFun2());
}
function MyFun2() {
var one = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Dashboard");
var two = one.getRange('A2').getValue();
var three = one.getRange('A3').getValue(); return two*three;
}
Don't forget to add a trigger "onEdit" on Myfun1() to automatically update the return value.

Selecting the last value of a column

I have a spreadsheet with some values in column G. Some cells are empty in between, and I need to get the last value from that column into another cell.
Something like:
=LAST(G2:G9999)
except that LAST isn't a function.
Similar answer to caligari's answer, but we can tidy it up by just specifying the full column range:
=INDEX(G2:G, COUNT(G2:G))
So this solution takes a string as its parameter. It finds how many rows are in the sheet. It gets all the values in the column specified. It loops through the values from the end to the beginning until it finds a value that is not an empty string. Finally it retunrs the value.
Script:
function lastValue(column) {
var lastRow = SpreadsheetApp.getActiveSheet().getMaxRows();
var values = SpreadsheetApp.getActiveSheet().getRange(column + "1:" + column + lastRow).getValues();
for (; values[lastRow - 1] == "" && lastRow > 0; lastRow--) {}
return values[lastRow - 1];
}
Usage:
=lastValue("G")
EDIT:
In response to the comment asking for the function to update automatically:
The best way I could find is to use this with the code above:
function onEdit(event) {
SpreadsheetApp.getActiveSheet().getRange("A1").setValue(lastValue("G"));
}
It would no longer be required to use the function in a cell like the Usage section states. Instead you are hard coding the cell you would like to update and the column you would like to track. It is possible that there is a more eloquent way to implement this (hopefully one that is not hard coded), but this is the best I could find for now.
Note that if you use the function in cell like stated earlier, it will update upon reload. Maybe there is a way to hook into onEdit() and force in cell functions to update. I just can't find it in the documentation.
Actually I found a simpler solution here:
http://www.google.com/support/forum/p/Google+Docs/thread?tid=20f1741a2e663bca&hl=en
It looks like this:
=FILTER( A10:A100 , ROW(A10:A100) =MAX( FILTER( ArrayFormula(ROW(A10:A100)) , NOT(ISBLANK(A10:A100)))))
LAST() function is not implemented at the moment in order to select the last cell within a range. However, following your example:
=LAST(G2:G9999)
we are able to obtain last cell using the couple of functions INDEX() and COUNT() in this way:
=INDEX(G2:G; COUNT(G2:G))
There is a live example at the spreedsheet where I have found (and solved) the same problem (sheet Orzamentos, cell I5). Note that it works perfectly even refering to other sheets within the document.
Summary:
=INDEX( FILTER( G2:G , NOT(ISBLANK(G2:G))) , COUNTA(G2:G) )
Details:
I've looked through and tried several answers, and here's what I've found:
The simplest solution (see Dohmoose' answer) works if there are no blanks:
=INDEX(G2:G; COUNT(G2:G))
If you have blanks, it fails.
You can handle one blank by just changing from COUNT to COUNTA (See user3280071's answer):
=INDEX(G2:G; COUNTA(G2:G))
However, this will fail for some combinations of blanks. (1 blank 1 blank 1 fails for me.)
The following code works (See Nader's answer and jason's comment):
=INDEX( FILTER( G2:G , NOT(ISBLANK(G2:G))) , ROWS( FILTER( G2:G , NOT(ISBLANK(G2:G)) ) ) )
but it requires thinking about whether you want to use COLUMNS or ROWS for a given range.
However, if COLUMNS is replaced with COUNT I seem to get a reliable, blank-proof implementation of LAST:
=INDEX( FILTER( G2:G , NOT(ISBLANK(G2:G))) , COUNT( FILTER( G2:G , NOT(ISBLANK(G2:G)) ) ) )
And since COUNTA has the filter built in, we can simplify further using
=INDEX( FILTER( G2:G , NOT(ISBLANK(G2:G))) , COUNTA(G2:G) )
This is somewhat simple, and correct. And you don't have to worry about whether to count rows or columns. And unlike script solutions, it automatically updates with changes to the spreadsheet.
And if you want to get the last value in a row, just change the data range:
=INDEX( FILTER( A2:2 , NOT(ISBLANK(A2:2))) , COUNTA(A2:2) )
In order to return the last value from a column of text values you need to use COUNTA, so you would need this formula:
=INDEX(G2:G; COUNTA(G2:G))
try this:
=INDIRECT("B"&arrayformula(max((B3:B<>"")*row(B3:B))))
Suppose the column in which you are looking for the last value is B.
And yes, it works with blanks.
This one works for me:
=INDEX(I:I;MAX((I:I<>"")*(ROW(I:I))))
It looks like Google Apps Script now supports ranges as function parameters. This solution accepts a range:
// Returns row number with the last non-blank value in a column, or the first row
// number if all are blank.
// Example: =rowWithLastValue(a2:a, 2)
// Arguments
// range: Spreadsheet range.
// firstRow: Row number of first row. It would be nice to pull this out of
// the range parameter, but the information is not available.
function rowWithLastValue(range, firstRow) {
// range is passed as an array of values from the indicated spreadsheet cells.
for (var i = range.length - 1; i >= 0; -- i) {
if (range[i] != "") return i + firstRow;
}
return firstRow;
}
Also see discussion in Google Apps Script help forum: How do I force formulas to recalculate?
I looked at the previous answers and they seem like they're working too hard. Maybe scripting support has simply improved. I think the function is expressed like this:
function lastValue(myRange) {
lastRow = myRange.length;
for (; myRange[lastRow - 1] == "" && lastRow > 0; lastRow--)
{ /*nothing to do*/ }
return myRange[lastRow - 1];
}
In my spreadsheet I then use:
= lastValue(E17:E999)
In the function, I get an array of values with one per referenced cell and this just iterates from the end of the array backwards until it finds a non-empty value or runs out of elements. Sheet references should be interpreted before the data is passed to the function. Not fancy enough to handle multi-dimensions, either. The question did ask for the last cell in a single column, so it seems to fit. It will probably die on if you run out of data, too.
Your mileage may vary, but this works for me.
function lastRow(column){
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var lastRow = sheet.getLastRow();
var lastRowRange=sheet.getRange(column+startRow);
return lastRowRange.getValue();
}
no hard coding.
In a column with blanks, you can get the last value with
=+sort(G:G,row(G:G)*(G:G<>""),)
This gets the last value and handles empty values:
=INDEX( FILTER( H:H ; NOT(ISBLANK(H:H))) ; ROWS( FILTER( H:H ; NOT(ISBLANK(H:H)) ) ) )
The answer
$ =INDEX(G2:G; COUNT(G2:G))
doesn't work correctly in LibreOffice. However, with a small change, it works perfectly.
$ =INDEX(G2:G100000; COUNT(G2:G100000))
It always works only if the true range is smaller than (G2:G10000)
Is it acceptable to answer the original question with a strictly off topic answer:)
You can write a formula in the spreadsheet to do this. Ugly perhaps? but effective in the normal operating of a spreadsheet.
=indirect("R"&ArrayFormula(max((G:G<>"")*row(G:G)))&"C"&7)
(G:G<>"") gives an array of true false values representing non-empty/empty cells
(G:G<>"")*row(G:G) gives an array of row numbers with zeros where cell is empty
max((G:G<>"")*row(G:G)) is the last non-empty cell in G
This is offered as a thought for a range of questions in the script area that could be delivered reliably with array formulas which have the advantage of often working in similar fashion in excel and openoffice.
function getDashboardSheet(spreadsheet) {
var sheetName = 'Name';
return spreadsheet.getSheetByName(sheetName);
}
var spreadsheet = SpreadsheetApp.openByUrl(SPREADSHEET_URL);
var dashboardSheet = getDashboardSheet(spreadsheet);
Logger.log('see:'+dashboardSheet.getLastRow());
I was playing with the code given by #tinfini, and thought people might benefit from what I think is a slightly more elegant solution (note I don't think scripts worked quite the same way when he created the original answer)...
//Note that this function assumes a single column of values, it will
//not function properly if given a multi-dimensional array (if the
//cells that are captured are not in a single row).
function LastInRange(values)
{
for (index = values.length - 1; values[index] == "" && index > 0; index--) {}
return String(values[index]);
}
In usage it would look like this:
=LastInRange(D2:D)
Regarding #Jon_Schneider's comment, if the column has blank cells just use COUNTA()
=INDEX(G2:G; COUNT**A**(G2:G))
I found another way may be it will help you
=INDEX( SORT( A5:D ; 1 ; FALSE) ; 1 ) -will return last row
More info from anab here:
https://groups.google.com/forum/?fromgroups=#!topic/How-to-Documents/if0_fGVINmI
Found a slight variation that worked to eliminate blanks from the bottom of the table.
=index(G2:G,COUNTIF(G2:G,"<>"))
I'm surprised no one had ever given this answer before. But this should be the shortest and it even works in excel :
=ARRAYFORMULA(LOOKUP(2,1/(G2:G<>""),G2:G))
G2:G<>"" creates a array of 1/true(1) and 1/false(0). Since LOOKUP does a top down approach to find 2 and Since it'll never find 2,it comes up to the last non blank row and gives the position of that.
The other way to do this, as others might've mentioned, is:
=INDEX(G2:G,MAX((ISBLANK(G2:G)-1)*-ROW(G2:G))-1)
Finding the MAXimum ROW of the non blank row and feeding it to INDEX
In a zero blank interruption array, Using INDIRECT RC notation with COUNTBLANK is another option. If V4:V6 is occupied with entries, then,
V18:
=INDIRECT("R[-"&COUNTBLANK(V4:V17)+1&"]C",0)
will give the position of V6.
to get the last value from a column you can also use MAX function with IF function
=ARRAYFORMULA(INDIRECT("G"&MAX(IF(G:G<>"", ROW(G:G), )), 4)))
I have gone through way too many of these implementations of last-row for a specific column. Many solutions work but are slow for large or multiple datasets. One of my use cases requires me to check the last row in specific columns across multiple spreadsheets. What I have found is that taking the whole column as a range and then iterating through it is too slow, and adding a few of these together makes the script sluggish.
My "hack" has been this formula:
=ROW(index(sheet!A2:A,max(row(sheet!A2:A)*(sheet!A2:A<>""))))-1
Example: Add this to Cell A1, to find the last row in column A. Can be added anywhere, just make sure to manage the "-1" at the end depending on which row the formula is placed. You can also place this is another col, rather than the one you're trying to count, and you don't need to manage the -1. You could also count FROM a starting Row, like "C16:C" - will count values C16 onwards
This formula is reliably giving me the last row, including blanks in the middle of the dataset
To use this value in my GS code, I am simply reading the cell value from A1. I understand that Google is clear that spreadsheet functions like read/write are heavy (time-consuming), but this is much faster than column count last-row methods in my experience (for large datasets)
To make this efficient, I am getting the last row in a col once, then saving it as a global variable and incrementing in my code to track which rows I should be updating. Reading the cell every-time your loop needs to make an update will be too inefficient. Read once, iterate the value, and the A1 cell formula (above) is "storing" the updated value for the next time your function runs
Please let me know if this was helpful to you! If I encounter any issues I will comment on this answer.
=QUERY({G2:G9999,ARRAYFORMULA(ROW(G2:G9999))},"Select Col1 where Col1 is not null Order By Col2 desc limit 1",0)
In the query, Col1 refers to column G, and Col2 refers to a virtual column, populated with the row numbers returned by ARRAYFORMULA(ROW(G2:G9999)).
I haven't evaluated the other answers, so I can't say if this is the best way, but it worked for me.
Bonus: to return the first non-empty cell:
QUERY({G2:G9999},"Select Col1 where Col1 is not null limit 1",0)
Refs: QUERY, ARRAYFORMULA, ROW.