Updating a query result when cell values change - google-apps-script

I have a query in cell I25 of my first sheet in Googlesheets:
=query({SheetsRange("A13:F17")}, "select sum(Col6) where Col1 contains '"&D7&"' ")
The "SheetsRange" is an Appscript function (thanks to the author!) that gets all of the sheet names for me:
/**
 * Returns concatened string range of all the sheets exept current sheet.
 *
 * #param {"A1:B5"} range - Input range as string that gets concatened to the sheetnames.
 * #return {string} all the sheets with range together
 * #customfunction
 */
function SheetsRange(range) {
const ss = SpreadsheetApp.getActiveSpreadsheet()
const currentSheet = ss.getActiveSheet().getName()
const sheets = ss.getSheets()
 
const output = []
 
sheets.forEach(sheet => {
const sheetname = sheet.getName()
console.log(sheetname)
if (sheetname != currentSheet) {
const values = sheet.getRange(range).getValues()
values.forEach(row => {
output.push(row)
})
}
})
 
return output
 
}
Whenever I update a cell in a sheet, the sum in I25 should reflect these changes. However, it only does this when I hit save on the Appscript.
I think I need an onEdit(e) function in order to catch any changes made on the spreadsheet and call the SheetsRange function? however I've not been able to adapt examples I've found to work for me.
For your amusement, my current attempt of using onEdit is:
function onEdit(e) {
if (!e) {
throw new Error('Please do not run the script in the script editor window. It runs automatically when you edit the spreadsheet.');
}
if (e.range.getA1Notation() !== 'F12:F') {
return;
}
SheetsRange();
}
Thank you for any help!

In your situation, how about the following modification?
Modified script:
function onEdit(e) {
if (!e) {
throw new Error('Please do not run the script in the script editor window. It runs automatically when you edit the spreadsheet.');
}
// I modified below script.
const { range, source } = e;
if (range.columnStart != 6 || range.rowStart == 1) {
return;
}
var formula = "SheetsRange";
var tempFormula = "sample";
source.createTextFinder(formula).matchCase(false).matchFormulaText(true).replaceAllWith(tempFormula);
source.createTextFinder(tempFormula).matchFormulaText(true).replaceAllWith(formula);
}
In this modification, when the cells of column "F" is edited, your showing formula of =query({SheetsRange("A13:F17")}, "select sum(Col6) where Col1 contains '"&D7&"' ") is recalculated.
References:
createTextFinder(findText) of Class Spreadsheet
Class TextFinder

You don't really need onEdit() function to do this... you just need to change the concept of the script.
Custom functions do updates the output results while input got changes.
The reason why yours do not update is because your input is a plain text that doesn't change.
If you change the formula into this:
=LAMBDA(SHEETNAMES,RANGE,
LAMBDA(RLEN,CLEN,SROW,SCOL,
LAMBDA(NAMECOUNT,
LAMBDA(ARRAY,
QUERY(ARRAY,"SELECT SUM(Col6) WHERE Col1 CONTAINS'"&D7&"'")
)(
MAKEARRAY(RLEN*NAMECOUNT,CLEN,LAMBDA(R,C,
INDIRECT("'"&INDEX(SHEETNAMES,ROUNDUP(R/RLEN))&"'!R"&IF(MOD(R,RLEN)=0,RLEN+SROW,MOD(R,RLEN)+SROW)&"C"&C+SCOL,FALSE)
))
)
)(COUNTA(SHEETNAMES))
)(
REDUCE(0,REGEXEXTRACT(RANGE,"(\d+)\D+(\d+)"),LAMBDA(A,B,B-A))+1,
REDUCE(0,BYCOL(REGEXEXTRACT(RANGE,"(\D+)\d+:(\D+)"),LAMBDA(COL,COLUMN(INDIRECT(COL&"1")))),LAMBDA(A,B,B-A))+1,
REGEXEXTRACT(RANGE,"\d+")-1,
COLUMN(INDIRECT(REGEXEXTRACT(RANGE,"\D+")&"1"))-1
)
)(getSheetNames(),"A5:B10")
And change the script into this:
function getSheetNames() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const thisSheet = ss.getActiveSheet().getSheetName();
return ss.getSheets().map(sheet => sheet.getSheetName()).filter(name => name!==thisSheet);
}
Instead of using Apps-script to get the values of each sheet, this example only return a list of sheet names with it.
And we uses INDIRECT() in R1C1 format to create the import data array, which since it is a build-in spreadsheet formula, it is always dynamic.
The results should be updated whenever the reference sheets got updated even without onEdit() trigger.

Related

put current date to a cell if it is filled with zapier

I found this script:
function onEdit () {
var s = SpreadsheetApp.getActiveSheet ();
if (s.getName () == "sheet_name") {
var r = s.getActiveCell ();
if (r.getColumn () == 1) {
var nextCell = r.offset (0, 1);
if (nextCell.getValue () === '')
nextCell.setValue (new Date());
}
}
}
It works if I fill one cell by myself and puts current date to another cell in the right.
BUT if I use Zapier to export my data from Todoist to Google Sheets this script doesn't work. It only works if I change something manually.
Is there any way to make a script which will fill a cell I need with a today date when Zapier export data and fills cells automatically?
Suggestion:
As what Tanaike mentioned, you need to rename your function to something else aside from onEdit() since onEdit is a reserved function name for App Script and use onChange trigger.
But based on how Zapier works, the reason why the current code you have provided is not working is because exports from Zapier is not detected as an active cell, so we would need to revamp the entire code.
Try this instead:
function onZapierUpdate() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Sheet2'); //my test sheet is Sheet2
var range = sheet.getRange(2,1,sheet.getLastRow()-1, 2);
var data = range.getValues();
data.forEach(x => x[0] != "" ? x[1] = new Date() : x);
range.setValues(data);
}
After saving the script, set this on an onChange trigger like so:
Now whenever Zapier exports the data, it changes the content of the spreadsheet which means onChange trigger will take effect.
Reference:
https://developers.google.com/apps-script/reference/script/spreadsheet-trigger-builder#onChange()

Vlookup with split text by Google Appscript

I am new to Appscript hence any help on below will be really appreciated.
My query is similar to the one posted in the below link,however, in that question the job is done by custom function and it is working bit slow and runs on every edit. In place of custom function I want to design an Appscript for the same which runs on the change of dropdown.
Link to similar question:
Google Appscript partial vlookup
Link of sample spreadsheet.
https://docs.google.com/spreadsheets/d/1vI22QCmixKe3aoWMLODTFzt7pNXIKO3pjXS4mT6GHT0/edit#gid=0
Any help on above will really be appreciated.
I believe your goal is as follows.
You want to run the script when the dropdown list of cell "A1" of "Sheet3" is changed to "Refresh".
You want to obtain the same result with your following script.
function MYLOOKUP(data1, data2) {
return data1
.map(([rollNo_Name, value]) => {
return (rollNo_Name !== '' && value === '') ?
data2.find(([rollNo,]) => rollNo_Name.split('*')[0] == rollNo)[1] :
''
});
}
In this case, how about using the OnEdit trigger of the simple trigger? When this is reflected in your sample Spreadsheet, the sample script is as follows.
Sample script:
Please copy and paste the following script to the script editor of Spreadsheet and save the script. When you run the script, please change the dropdown list of cell "A1" of "Sheet3" to "Refresh". By this, the script is run.
function onEdit(e) {
const sheetName = "Sheet3"; // This sheet name is from your Spreadsheet.
const { range, value, source } = e;
const sheet = range.getSheet();
if (sheet.getSheetName() != sheetName || range.getA1Notation() != "A1" || value != "Refresh") return;
const sheet1 = source.getSheetByName("Sheet1"); // This sheet name is from your Spreadsheet.
const sheet2 = source.getSheetByName("Sheet2"); // This sheet name is from your Spreadsheet.
const range1 = sheet1.getRange("A2:B" + sheet1.getLastRow());
const obj = sheet2.getRange("A2:B" + sheet2.getLastRow()).getValues().reduce((o, [a, b]) => (o[a] = b, o), {});
const values = range1.getValues().map(([a, b]) => {
const temp = obj[a.split("*")[0]];
return [temp && !b.toString() ? temp : null];
});
range1.offset(0, 2, values.length, 1).setValues(values);
range.setValue(null);
}
In this script, when the dropdown list of cell "A1" of "Sheet3" is changed to "Refresh", the script is run. And, the same result with your script is obtained. And, the value of the dropdown list is changed to null.
The result values are put to column "C" of "Sheet1". If you want to change this, please modify the above script.
Note:
In this script, when you directly run the function onEdit with the script editor, an error occurs. Please be careful about this.
In this script, in order to search the values, I used an object. By this, the process cost might be able to be reduced a little.
Updated: I reflected value in to be pulled only when there is no value in Column B.
References:
Simple Triggers
reduce()
map()

Using the onEdit function to only edit one tab

I've seen many similar questions like this asked, but I'm still stumped.
I want an onEdit script to only work on a specific tab. I've tried several things but none of them are working correctly.
The tab on my sheet is called 'WEB Graffiti'. I want to run this script on other tabs on my sheet, but the columns are in different orders. I know how to edit the script to edit different columns but I don't know how to make it only work on the specified tab.
Here is the script I'm using.
function onEdit(e) {
if (!e) {
throw new Error(
);
}
indirectTimestamp_(e);
}
/**
* Inserts a timestamp in column T when column B is edited
* and column C contains the value TRUE.
*
* #param {testing} e The onEdit() event object.
*/
function indirectTimestamp_(e) {
if (e.range.columnStart !== 2
|| e.range.offset(0, 1).getDisplayValue() !== 'TRUE') {
return;
}
const timestampCell = e.range.offset(0, 18);
timestampCell.setValue(new Date()).setNumberFormat('mmm" "d" "yyyy');
};
I tried adding
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('WEB Graffiti'), true);
I tried this and several variations in various locations of the script and it was not working properly.
It's not clear if you want to only EDIT WEB Graffiti, or only run the procedure when that WEB Graffiti is EDITED. OnEdit will always run, the only thing you can do is scope it to make changes based on where the edit was conducted.
The below code of yours will only execute the indirect function when the WEB Graffiti sheet is edited.
function onEdit(e) {
var myRange = e.range;
var theSheetName = myRange.getSheet().getName();
if (!e) {
throw new Error(
);
}
if(theSheetName == 'WEB Graffiti'){
indirectTimestamp_(e);
}
}
Do this for Sheet1
function onEdit(e) {
const sh = e.range.getSheet();
if(sh.getName() == "Sheet1") {
//perform rest of your code which is for sheet1
}
}

Creating A Googlesheet Hyperlink using GoogleScripts

Using the code below I am able to generate a hyperlink to each page within my googlesheet, with the name of that sheet:
/**
* Gets the Sheet Name of a selected Sheet.
*
* #param {number} option 0 - Current Sheet, 1 All Sheets, 2 Spreadsheet filename
* #return The input multiplied by 2.
* #customfunction
*/
function SHEETNAME(option) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet()
var thisSheet = sheet.getName();
var thisgid = sheet.getSheetId();
//Current option Sheet Name
if(option === 0){
return thisSheet;
//All Sheet Names in Spreadsheet
}else if(option === 1){
var sheet1 = [];
ss.getSheets().forEach(function(val){
sheet1.push(`=HYPERLINK("#gid=${val.getSheetId()}","${val.getName()}")`)
});
return sheet1;
//The Spreadsheet File Name
}else if(option === 2){
return ss.getName();
//Error
}else{
return "#N/A";
};
};
function GETLINK(option) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(option);
if (sheet != null) {
return(sheet.getId());
};
};
This returns a column of appropriate functions in each cells but does not evaluate the function. If I copy the function into another cell it does evaluate?!
=HYPERLINK("#gid=XXXXXXXXXXXXX","SHEET1")
I've tried also tried adding HTML but this also becomes a string and is not evaluated
sheet1.push(`${val.getName()})
When the custom formula of =SHEETNAME(0) is put to a cell, you want to put the sheet name of the active sheet on the Spreadsheet.
When the custom formula of =SHEETNAME(1) is put to a cell, you want to put the formulas of =HYPERLINK("#gid=XXXXXXXXXXXXX","SHEET1") for all sheets.
When the custom formula of =SHEETNAME(2) is put to a cell, you want to put the Spreadsheet name of the active Spreadsheet.
If my understanding is correct, how about this answer?
Issue and workaround:
Unfortunately, in the current specification of Google side, the formula cannot be put to a cell by the custom formula. When the formula is put to a cell by the custom formula, the formula is put as the string value. I think that this is the reason of your issue.
As a workaround, I would like to propose to put the formula of =HYPERLINK("#gid=XXXXXXXXXXXXX","SHEET1") using the OnEdit event trigger when the custom formula of =SHEETNAME(#) is put to a cell. I think that by this workaround, your goal can be achieved.
When your script is modified, please modify as follows.
Modified script:
In this case, the simple trigger can be used. So please copy and paste the following script to the script editor, and save the script. In order to use this script, as a test, please put =SHEETNAME(1) to a cell.
function onEdit(e) {
const spreadsheet = e.source;
const range = e.range;
const sheet = range.getSheet();
const formula = range.getFormula();
const f = formula.match(/\=SHEETNAME\((\d+)\)/i);
if (f && f.length > 0) {
if (f[1] == "0") {
range.setValue(sheet.getSheetName());
} else if (f[1] == "1") {
var formulas = spreadsheet.getSheets().map(val => [`=HYPERLINK("#gid=${val.getSheetId()}","${val.getName()}")`]);
range.offset(0, 0, formulas.length, 1).setFormulas(formulas);
} else if (f[1] == "2") {
range.setValue(spreadsheet.getName());
}
}
}
In this case, there is the function of SHEETNAME() in your script editor. So when =SHEETNAME(1) is put to a cell, at first, the function of SHEETNAME() is run, and then, the function of onEdit is automatically run by the OnEdit event trigger. If you don't want to show the values from SHEETNAME(), please replace the function of SHEETNAME() as follows.
const SHEETNAME = () => "";
Note:
In this case, when you directly run the function of onEdit at the script editor, an error occurs. Please be careful this.
This script is run under V8.
References:
Simple Triggers
Event Objects
setFormulas()

Clear a cell and fetch the Google sheet name

I want to make a script to clear a fixed cell on all sheets but the first four and fill the very same cell with the sheet name.
So far I have a script to blank out the cell and the fill it with a new function the fetches the sheet name. The first script is triggered on opening the spreadsheet. However, it just says loading… and does not fetch the sheet names.
My current (non-working script):
function sheetName() {
return SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getName();
}
function clearRange() {
SpreadsheetApp.getActive().getSheets()
.map(function (s, i) {
if (i > 3) s.getRange('B3').clearContent().setFormula('=sheetName()');
})
}
Any great ideas?
Thanks
Instead of writing a custom function to the cell, why don't you write the sheet name itself ? Like so...
function clearRange() {
SpreadsheetApp.getActive().getSheets()
.map(function (s, i) {
if (i > 3) s.getRange('B3').clearContent().setValue(s.getName());
})
}
If needed you can trigger this function to run onOpen..
NOTE: I think you can even leave out the .clearContent() part as the content will be overwritten any how.
Same method but a little modified
function clearRange(n, range) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
sheets.map(function(sheet,index){
if (index > n) sheet.getRange(range).clearContent();
});
}
// call the function
function run(){
clearRange(3, 'B3')
}
You can pass an argument n and range instead of the hard coded 3 and 'B3' in case you want to use the function with other parameters