Is it possible to define a new function in Google-docs spreadsheet? - google-apps-script

Is it possible to define a function in Google Spreadsheets that can be used in any cell?
It would be helpful if I could define and use functions that refer to other cells in the same way that I can use native functions, e.g. by entering =myfunction(C1, C2, C3)

Yes - there's a tutorial. Just use javascript functions by name in your spreadsheet. Define them using Tools > Script editor.
Watch out for name restrictions; I was confused by the behavior of functions that I created with names like "function x10() {}" not being found. Renaming to a longer name fixed it. There are probably documented rules for what isn't allowed, but I don't know where they are.

I am a "newbee". But is is my experience that you can only access a "cell"
via the "range" object. You must define the range as a single cell.
For example "A1:A1", will give you access the the cell at "A1".
A RANGE is an object associated to a "SHEET".
A SHEET is an object associated to a "SPREADSHEET".
Here is some sample code to access cell A1 in the current active sheet:
var cell_A1 = SpreadsheetApp.getActiveSheet().getRange("A1:A1");
From here you can pass the object like any other parameter.
myFunction(cell_A1);
The receiving function must "know" that it is dealing with a "range".
It can only access its values by calling "methods" associated to the
"range" object.
Be careful! A "range" can consist of more than one cell. Your called
function should test to see that it is working with a single cell.
If you pass a range of more than one cell, your function might not
act in the way you expect.
The two methods of a range object: "getNumRows()" and "getNumColumns()"
returns the numbers of Rows and Columns in a range object.
In general, if you use methods that are limited to changing or accessing
a single cell, and operate on a larger range set, the function will only be
performed on the upper-left cell member. But be careful. While you
might assume a method will only change a single cell, it may actually
affect all cells in the range. Read the documentation closely.
There is another method to obtain a range of a single cell. Its instruction
looks like this:
var cell_B2 = SpreadsheetApp.getActiveSheet().getRange(2, 2, 1, 1).
The first two parameters tell the "getRange" function the location of the
cell (in row, column format). The second two parameters define the number of
"rows" and "columns" to associated with the range. By setting them both to
"1", you access a single cell.
Hope this helps.

Related

Using named ranged in google sheets as variables in custom formulas

Not sure if this is going to make sense but here I go.
What I want to do is to create a formula that isn't linked to a cell directly. In example: if I want to calculate carryweight for a tabletop game like D&D I would need the formula (strengthBonus x 5). For my current attempt I renamed the range (cell rather) strengthBonus to MOD_STR so when I put the formula =(multiply(MOD_STR,5) it works like a charm. Then I named that range "CARRYWEIGHT" and then use it elsewhere.
What I would like to be able to do is to make a new variable, similar to the way that "Define Named Range" does, but instead of relying on the variables being somewhere on the spreadsheet they would process from an internal formula. For example, if I type =carryweight into a cell it would run the equation =MULTIPLY(MOD_STR,5) in that cell and output the answer. I know nothing about code yet but have just been pointed in the direction of tutorials but I'm also asking for help here.
The code I have tried is
function CARRYWEIGHT(MOD_STR){
return MOD_STR*2}
and something else, I can't remember what but I got it to at least accept it in the spreadsheet. When I type it in I get an error stating that the outcome isn't a number.
I have no idea where to go from here.
Thank you in advanced for your help.
The difference between sheets formulas and Apps Script is that in Apps Script you need to retrieve the value of the range corresponding to the name of a named range
You cannot simply multiply the name of the range (which is a string!) with a number
Here is a sample of how to retrieve a range by name and make calculations wiht the value stored in it:
function CARRYWEIGHT(MOD_STR){
// retrieve all named ranges in the spreadsheet
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
//loop through all the results
for (var i = 0; i < namedRanges.length; i++){
var range = namedRanges[i];
//if the range with the name equal to the value of MOD_STR is found, get the cell content of this range
if(range.getName()==MOD_STR){
var value = range.getRange().getValue();
// perform the calculation with the cell content of the named range
return value*2;
}
}
}
From the cell, call the function as =CARRYWEIGHT("paste here the name of the range of interest"), do not forget the quotes (unless it is a cell reference)!
I hope this helped you to get started, for further understanding plese consult the following references.
References
Named Ranges
Loops
Conditional statements
Ranges
getValue()

Replace INDIRECT() in data validation rule

I had a working Excel spreadsheet that used indirect() in the data validation and it worked fine. I uploaded it to sheets and converted it, now the indirect does not work.
I have found a link on the support forum that explains it does not work in Chrome but appears to work in Firefox, and the answers and workarounds seem to be for generating a secondary list... which is what I want, but in a data validation across a row.
I have knocked up a simple test sheet, hopefully public and the script editor is visible:
https://docs.google.com/spreadsheets/d/1KUgrdXKIKlk1DWvDOX9cY3B2VnRH_5h_vKuZJlqUlN8/edit?usp=sharing
Hopefully you can see what I'm after. I want the validation in C8 to be the list of items in the category based in B8; C9 based on B9 etc.
EDIT and Update
The question is about a replacement to indirect() in a data validation rule. While I did find a way round this by using indirect(), I preferred the version mentioned by Desire (to whom I have attributed the answer), but I thought I'd document my solution in case the sheet above becomes unavailable, or you cannot access it, or you just wanted a bit more detail.
So, for My Demo I have this:
In A1:C5 are my lists of data with the titles.
In the range B8:B12 I applied a data validation rule of value in range of A1:C1 - this gives the first dropdown.
In Cell E8 I put the formula =transpose(filter($A$2:$C$5, $A$1:$C$1 = B8)) and then copied this down to E12
Finally I put the following in a function and ran it in the script editor.
function runMeOnce() {
var dst = SpreadsheetApp.getActive().getSheetByName('Sheet1').getRange('C8:C12');
var rules = [];
for (var i = 8; i < 13; i++) {
var src = SpreadsheetApp.getActive().getSheetByName('Sheet1').getRange("E" + i + ":H" + i);
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(src).build();
rules.push(rule);
}
dst.setDataValidations(rules);
}
That's all there is, no more onEdit() triggering.
NOTE There is one downside I bumped into with this method though. I have this in place for 6000+ rows in my actual spreadsheet, and across multiple sheets, with some dropdowns having 50-100 items in. This solution seriously eats into the (current) 2 million cell limit.
Hope this helps someone.
Data Validation rule of the type "List of items" takes only a comma-separated list of values as its parameter, and does not evaluate any formulas you try to put there. It does not matter what the function returns, because it will not be called. If you put, say "=sqrt(A10)" in the field "List of items", that only means that the validation rule will require the string "=sqrt(A10)" to be entered in the cell.
Similarly with "List from a Range". Either what you enter parses as range notation, or it does not. The string "=getValidationRange(B8)" does not parse as range notation, hence the error. The function is never called.
The only type of validation that calls a function is "Custom formula". If you use it, then the validation can be performed as intended: for example,
=match(C8, filter(A2:C5, A1:C1 = B8), 0)
requires the content of C8 to be in the column of the table A2:C5 under the heading that matches the category in B8. However, with a custom formula you do not get a dropdown in a cell.
To get a dynamic dropdown, one can either
Use an auxiliary range
For example, enter filter(A2:C5, A1:C1 = B8) in cell F1, so that the F column is for the categories currently selected. The data validation would be "List from a Range", F1:F. This is a fine workaround for one validation rule, but takes more work when you have multiple ones.
Use a triggered script
Use a script that is triggered on edit and sets data validation rules accordingly; this is discussed in How do you do dynamic / dependent drop downs in Google Sheets? among other places.
Based on the sacrificing a goat issue, I did find a simple(ish) way around the problem that still uses indirect().
Set up the named ranges as previously using the titles in CamelCase. In my example I have CatA, CatB, and CatC - i.e. the white space needs removing.
At the end of a row (or in another sheet) transpose the chosen named range (in cell E8: =transpose(indirect(substitute(B8, " ", ""))) copy this down as far as you need.
At this point it's good to note that because we are unsing builtin functions, the speed is so much better, as can be seen by my example.
Now the painful bit. For each subcategory cell (C8, C9 etc in my example), you need to add the validation independently as a range of E8:ZZ8 (obviously ZZ8 needs reigning in a bit) and E9:ZZ9 etc. It doesn't seem to do referential so if you select all the cell in the column, they all only look at the data you specifically type in the box... I might just not have worked out R1C1 notation here, however. I tried.
This can be scripted on GAS to create the R1C1 validation function and then apply it to the range.

Ability to Edit Google Sheets' Custom Function Result

I'm wondering if it is possible to edit the result of a custom function in Google Sheets when returning a 2-dimensional array.
If I try to delete a cell that is part of the returned array, it blinks but stays there.
If I enter something in a cell that is part of the returned 2-dimensional array, it works, but the function then does not return because the value is blocking the display of the 2-dimensional array (Array result was not expanded because it would overwrite data in A3.)
It's the same as for built-in functions returning an array (split, importrange, query, etc): output cannot be edited. But there is a workaround described below.
Suppose the output range is A1:B6. Elsewhere in a sheet, enter =A1 and copy this formula to a 2 by 6 range. This creates a range that is visually identical to the function output, but editable cell-by-cell. If you edit a cell in this new range, it will cease to depend on the function, but the rest will still be updated if the function input changes.

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.

Read and write cell formulas from Google Apps Script custom function

I am trying to write a GAS spreadsheet custom function that copies cell content to other cells. And I need to fill the target cells not only with the data of the source cell, but with its formula content (if it has any).
Now, I already know that this is basically impossible through custom functions as they always receive the result of cell calculations but not the cell formulas themselves, and they also cannot return formulas for their target cells.
On the other hand there are functions to read and write cell formulas, e.g. Range.getFormula() and Range.setFormula() which seem to make my endeavor possible. I simply have to find another way of calling them. UPDATE: Meanwhile I found that custom formulas in fact can read formulas using getFormula(), but they definitely don't have permission to write formulas into cells using setFormula().
My question is...
What would be the most elegant method to create something equivalent to a custom function that reads and writes formula content of cells? I think I could use an onEdit function that updates my target cells after each spreadsheet edit, but that would mean that I have to hard code the coordinates of the target cell range, which seems very hacky and would require code changes every time the target range is moved (e.g. when rows are inserted above it).
UPDATE: Example
An example would be a custom function that is able to read multiple ranges of cells (each range given as a distinct function parameter) and returns a joined range of cells.
=rangeJoin(A1:B10;D1:E15)
...would read the two ranges of size 2x10 and 2x15 and would fill a target range of size 2x25 with the subsequent cell contents of both ranges. The target range would start at the cell that contains rangeJoin and would spread 2 cells to the right and 25 cells down (as usual for a custom function). The custom function (or similar mechanism) should be able to copy formulas, so a cell containing =hyperlink("http://www.google.com";"Google") should appear in the target range as a hyperlink and not as a text cell with the naked word 'Google'.
Agree with "Mogsdad"
ie. this custom function works:
function myGrid() {
return [[1,2],[3,"http://www.google.com"]];
}
but, custom functions can't write formulas to a sheet. See https://developers.google.com/apps-script/execution_custom_functions#permissions
As a workaround, you could use a "Trigger", such as a time based trigger, as "Mogsdad" suggests.