I am trying to create a function/formula/script in Google sheets for my own formula.
I have one, where I need the value displayed in the cell above, which I type in manually. The formula is 5.5^(39-XX). So if for example cell B5 has the value 32, in cell B6 I'd like to type something like "=MYFORMULA" into the cell where the calculated value should be displayed. MYFORMULA should be programmed as 5.5^(39-cellabove).
I have already tried
function MYFORMULA() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getActiveSheet();
var cellRange = sheet.getActiveCell();
var selectedColumn = cellRange.getColumn();
var selectedRow = cellRange.getRow();
Logger.log(`selectedColumn: ${selectedColumn}`);
Logger.log(`selectedRow: ${selectedRow}`);
Logger.log(`selected cell vale: ${cellRange.getValue()}`);
cellRange.setValue('=5.5^(39-(MYFORMULA[-1;]))')
}
which does not work, unfortunately. I have the feeling, I have to program a function for the current cell as well. Any suggestions for the code? I am coding into Apps Script in Google tables.
Also, I have to activate a trigger in order to have premission using any newly created function. If you have any suggestions here, too, I would appreciate your help.
Cheers!
Rather than detect the active range and set a value directly, you can implement MYFORMULA as a custom function, which can take an input that you pass as a cell reference and return a value, which will be written into the cell containing the formula.
Suggested change
Change your function to include an input variable, and to return a value, without any reference to specific cells, like this:
function MYFORMULA(x) {
return 5.5**(39-x)
}
(Note ** is the Javascript symbol for exponent)
Then, in your spreadsheet, in cell B6, type =MYFORMULA(B5). The spreadsheet will pass the value in B5 to your function, and will write the return value in B6.
Related
In Google Sheet, I would like to take the input of a cell, make a calculation, and display the result in the same cell in a different format. This would likely always be a percentage value that I would use conditional formatting to color the cell to provide a 'dashboard' view of statistics.
Example would be usage statistics for a month.
Assets
Records
limit
50
1000
November
29
295
Assets
Records
limit
50
1000
November
58%
30%
I found a Quora post detailing how to create your own scripts, so I believe I have the baseline of taking and modifying content of a cell:
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var cell = ss.getActiveSelection();
var cell_input = cell.getValue();
var cell_address = cell.getA1Notation()
var cell2 = ss.getRange(cell_address);
cell2.setFormula('=2*'+cell_input)
}
In this example, I'm unsure how to reference the cell it should be dividing against.
Here's a general example that should give you an idea of how to manipulate the cells, formats and values in Sheets:
function onEdit(e) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var cell = ss.getActiveSelection();
var cell_input = cell.getValue();
var divisor = cell.offset(-1, 0)
if (e.range.getA1Notation()=="B3" || "C3"){
cell.setNumberFormat("#").setValue(Math.ceil((cell_input / divisor.getValue()) * 100) + "%")
}
}
If you create a sheet that looks like this you will get the behavior you were looking for in B3 and C3:
How it works:
Add the e parameter to the onEdit(e) trigger so you can use e.range to read the range that called the function.
To get the cell you're dividing against you need to either know its exact range or its relative position to the cell that called it. Since in your sample the percentage cell is below the divisor, you can just offset() it by -1, which returns the cell above it. If the relative position changes you'll need to take this into account to modify the offset but it should work in general if your table has a consistent structure.
You'll need to use an if to fire the trigger only in a specific range, otherwise the entire sheet will be affected. You do this by comparing the caller e.range to your desired range. In this example for the sake of simplicity I just had two cells so I just compared the individual B3 and C3 cells to e.range.getA1Notation(), but if you want a bigger range you'll probably want to use a more advanced technique like the ones in this question
To get around the format problem described in TheWizEd's comment I'm forcing the cell to a text value using setNumberFormat("#"). This way if you enter a value a second time it will read it as a number rather than a percent. I tested using the value elsewhere in the sheet and it still works as a percentage when needed, but watch out for unintended behavior.
Sources:
Simple triggers
Range object documentation
When working with triggers, take advantage of the event object.
Replace
function onEdit(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var cell = ss.getActiveSelection();
var cell_input = cell.getValue();
by
function onEdit(e){
var cell_input = e.value ?? 0;
Notes:
SpreasheetApp.Spreadsheet.getActiveSeletion() returns the first range that belongs to the active selection, this might cause problems because the active seleccion could include multiple cells, and the current cell could be any cell in the selection even one cell that is not part of the returned range.
SpreadsheetApp.Range.getValue() returns the value of the top left cell in the range. A user could select multiple cells and use the tab key to change the current cell, so under certain circunstances the value obtained this way might not be the value of the edited cell.
e.value ?? 0 is used to get 0 as the default value, i.e., when a cell is cleared.
As onEdit is triggered when any cell is edited your script should include a condition to only change the corresponding cells.
function onEdit(e){
const cell_input = e.value ?? 0;
if(e.range.rowStart === 3 && [2,3].includes(e.range.columnStart)){
const newValue = (100 * cell_input / e.range.offset(-1,0).getValue()).toFixed() + '%';
e.range.setValue(newValue);
}
}
The above uses
SpreadsheetApp.Range.offset to get the cell above of the edited cell.
Writes a the percentage as string, taking advange of the Google Sheets automatic data type assignation.
References
https://developers.google.com/apps-script/guides/triggers
https://developers.google.com/apps-script/guides/triggers/events
Let's say I have a cell with a specific formatting. Is there a way to assign that formatting to another cells so that when the original cell formatting is changed all the dependent cell formatting is also changed?
If there is no appropriate answer for this, then I will simplify my question with the following: imagine that both the content and formatting of the dependent cells should 100% be the same as the original cell. So when I change something (value, format) in the original cell, then it is being propagated to all the dependent cells. Is at least this possible to do?
You can use triggers like onEdit but the catch is that it can't capture events like for example changing formats. It only is triggered by value changes. So see this script below:
Script:
function onEdit(e) {
var spreadsheet = e.source;
var sheet = spreadsheet.getActiveSheet();
var range = e.range;
var referenceCell = 'A1';
var dependentCells = ['C1', 'D1'];
// if cell edited is Sheet1!A1
if(sheet.getSheetName() == 'Sheet1' && range.getA1Notation() == referenceCell) {
dependentCells.forEach(dependentCell => {
sheet.getRange(referenceCell).copyTo(sheet.getRange(dependentCell), SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false);
});
}
}
Behavior:
Dependent cells' (C1 and D1) formats will only be updated when A1 value is updated.
After triggering the script, you can always revert the value of the reference cell (A1).
Output:
This is only an alternative to your first question as format changes can't trigger functions.
For your second one, if you also want to copy including the values, then use PASTE_NORMAL instead of PASTE_FORMAT.
See other copy paste type values on the reference below.
References:
https://developers.google.com/apps-script/reference/spreadsheet/copy-paste-type
I've got a Google App Script which is copying rows from one sheet to another, performing various transformations. This logic ultimately gets rows onto the new sheet using sheet.appendRow(row detail). I would like these newly created rows to have a background colour (my intention is to hold a 'latestColour' so I can alternate the shading).
So, is there anyway to add shading within the appendRow method itself, or easily determine the range that the appendRow method processed, such that I can apply additional logic to add the shading.
You can use conditional formatting
=and(A1<>"",A2="")
Although I'm not sure whether I could correctly understand your situation, from your question, I thought that you might be using [Format] --> [Alternating colors] in Google Spreadsheet. And, when a new row is appended by putting the values, you might want to reflect "Alternating colors" in the appended row. If my guess is correct, how about the following sample script?
Sample script:
function myFunction() {
const addValues = ["sample1", "sample2", "sample3"]; // This is a sample appending value. Please replace this for your value.
const sheetName = "Sheet1"; // Please set the sheet name.
// Retrieve banding object from the data range.
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
const b = sheet.getDataRange().getBandings();
if (b.length == 0) {
console.log("Bandings are not used.");
return;
}
// Append the value.
sheet.appendRow(addValues);
// Expand the range of banding.
b[0].setRange(sheet.getDataRange());
}
When this script is run, the current banding is retrieved. And, after the value was appended, the banding is updated by including the appended row. In this sample, even when the multiple rows are appended, this script can be used.
Note:
From your question, I guessed that there is one banding in the data range in your sheet. Please be careful this.
References:
getBandings()
setRange(range)
Unfortunately the method appendRow() does not receive formatting settings as input, only an array of values.
However, here is a suggestion if you want to implement your own logic:
Sample code:
function applyColorLastRow() {
var ss = SpreadsheetApp.getActive(); //get active sheets file
var range = ss.getDataRange(); //get populated range, you may want to set a range manually if needed.
var lastRowNum = range.getLastRow(); //getting the last row index of the range.
var lastRowRange = ss.getRange(`${lastRowNum}:${lastRowNum}`); //narrowing the range (using A1 notation) to the last row only to apply color
var lastRowColor = lastRowRange.getCell(1,1).getBackgroundObject().asRgbColor().asHexString();
//Your row coloring logic here...
if (lastRowColor === '#ffffff'){ //toggling white/grey color as an example...
lastRowRange.setBackground('#cccccc'); //apply grey color to all cells in the last row range
} else {
lastRowRange.setBackground('#ffffff'); //apply white color to all cells in the last row range
};
}
I have a custom function that finds the value of another cell and displays it. When the source cell is changed, the function does not reflect.
https://docs.google.com/spreadsheets/d/1wfFe__g0VdXGAAaPthuhmWQo3A2nQtSVUhfGBt6aIQ0/edit?usp=sharing
Refreshing google sheets
function findRate() {
var accountName = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(1,1).getValue(); //determine the account name to use in the horizontal search
var rateTab = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Rates'); //hold the name of the rate tab for further dissection
var rateNumColumns =rateTab.getLastColumn(); //count the number of columns on the rate tab so we can later create an array
var rateNumRows = rateTab.getLastRow(); //count the number of rows on the rate tab so we can create an array
var rateSheet = rateTab.getRange(1,1,rateNumRows,rateNumColumns).getValues(); //create an array based on the number of rows & columns on the rate tab
var currentRow = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveCell().getRow(); //gets the current row so we can get the name of the rate to search
var rateToSearch = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(currentRow,1).getValue(); //gets the name of the rate to search on the rates tab
for(rr=0;rr<rateSheet.length;++rr){
if (rateSheet[rr][0]==rateToSearch){break} ;// if we find the name of the
}
for(cc=0;cc<rateNumColumns;++cc){
if (rateSheet[0][cc]==accountName){break};
}
var rate = rateSheet[rr][cc] ; //the value of the rate as specified by rate name and account name
return rate;
}
If I change a rate in the rate tab, I need the custom function to recognize the new rate and update its value
You want to recalculate the custom function of =findRate(), when the cells of the sheet name of Rates are edited.
If my understanding is correct, how about adding the following sample script? Please think of this as just one of several answers.
Solution:
In order to recalculate the custom function, in this answer, the formula of =findRate() is overwritten by the script running with the OnEdit event trigger (in this case, it's the simple trigger.). By this, the recalculate is executed. But, when the formula is directly replaced by the same formula, the recalculate is not executed. So I used the following flow.
Retrieve all ranges of cells which have the formula of =findRate() from the sheet of "Projected Revenue".
Clear the formulas of the ranges.
Put the formulas to the ranges.
By this flow, when the cell of the sheet of "Rates" is edited, the custom function of =findRate() is recalculated by automatically running onEdit().
Sample script:
Please copy and paste the following script to the script editor. Then, please edit the cells of sheet name of Rates. By this, onEdit() is automatically run by the OnEdit event trigger.
function onEdit(e) {
var range = e.range;
if (range.getSheet().getSheetName() == "Rates" && range.rowStart > 1 && range.columnStart > 1) {
var sheetName = "Projected Revenue"; // If you want to change the sheet name, please modify this.
var formula = "=findRate()";// If you want to change the function name, please modify this.
var sheet = e.source.getSheetByName(sheetName);
var ranges = sheet.createTextFinder(formula).matchFormulaText(true).findAll().map(function(e) {return e.getA1Notation()});
sheet.getRangeList(ranges).clearContent();
SpreadsheetApp.flush();
sheet.getRangeList(ranges).setFormula(formula);
}
}
Note:
onEdit(e) is run by the OnEdit event trigger. So when you directly run onEdit(e), an error occurs. Please be careful this.
In this sample script, as a sample, even when the row 1 and column "A" of the sheet of "Rates" are edited, the custom function is not recalculated. If you want to modify this and give the limitation of range you want to edit, please modify the above script.
References:
Simple Triggers
Class TextFinder
Class RangeList
flush()
If I misunderstood your question and this was not the result you want, I apologize.
Added:
The proposal from TheMaster's comment was reflected to the script. When sheet.createTextFinder(formula).matchFormulaText(true).replaceAllWith(formula) can be used, also I think that the process cost will be much reduced. But in my environment, it seemed that the formulas are required to be cleared once to refresh the custom function, even if flush() is used. So I have proposed above flow.
But, now I could notice a workaround using replaceAllWith() of TextFinder. So I would like to add it. The flow of this workaround is as follows.
Replace all values of =findRate() to a value in the sheet of Projected Revenue using replaceAllWith()..
In this case, as a test case, the formulas are replaced to sample.
Replace sample to =findRate() using replaceAllWith().
By this flow, I could confirm that =findRate() is recalculated. And also, it seems that flush() is not required for this situation.
Sample script:
Please copy and paste the following script to the script editor. Then, please edit the cells of sheet name of Rates. By this, onEdit() is automatically run by the OnEdit event trigger.
function onEdit(e) {
var range = e.range;
if (range.getSheet().getSheetName() == "Rates" && range.rowStart > 1 && range.columnStart > 1) {
var sheetName = "Projected Revenue";
var formula = "=findRate()";
var tempValue = "sample";
var sheet = e.source.getSheetByName(sheetName);
sheet.createTextFinder(formula).matchFormulaText(true).replaceAllWith(tempValue);
sheet.createTextFinder(tempValue).matchFormulaText(true).replaceAllWith(formula);
}
}
Crossposted # Google SS forums
I want do do this:
function memoizeDate(condition) {
if (condition) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var cell = sheet.getActiveCell();
cell.setValue(Date.now());
}
}
So that in (say) cell C2, if you set its value to =memoizeDate(eq(b2,"foo")), then as soon as b2 gets changed, C2 changes itself to the memoized value.
However, I get a "You do not have permission to call setValue" error, and I'm also not sure if the "active" cell (from the script's POV) is the one from which the script was called (i.e. C2) rather than who knows what else (B2? whatever random thing might be selected by the user?).
Have you tried like this (suggestion) :
function memoizeDate(condition) {
if (condition) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var cell = sheet.getActiveCell();
return new Date();
}else{
return 'nop or whatever'
}
}
EDIT following your comment : I keep reading your question and comments but I'm not sure what you really want to do.
On one hand you say "overriding the function itself" which means in fact returning a value (and that's what my code does) and on the other hand you try to get the active cell which is the one the user is editing (B2 in your example) and in that case you simply can't do that , please read the documentation about custom function :
Custom functions return values, but they cannot set values outside the cells they are in. In most circumstances, a custom function in cell A1 cannot modify cell A5. However, if a custom function returns a double array, the results overflow the cell containing the function and fill the cells below and to the right of the cell containing the custom function. You can test this with a custom function containing return [[1,2],[3,4]];.
All in all I think this question is rather unclear, (no personal offense) you should try to reformulate it at the least.