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
Related
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.
I know this question has been asked before but the answers given are not valid for my case because it's slightly different.
I've created a formula that looks for sheets with a pattern in the name and then uses it's content to generate the output. For example
function sampleFormula(searchTerm) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spreadsheet.getSheets()
.filter(function(sheet) {
// If sheet name starts with DATA_SHEET_...
return sheet.getSheetName().indexOf('DATA_SHEET_') === 0;
});
const result = [];
sheets.forEach(function(sheet) {
// We get all the rows in the sheet
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row) => {
// If the row it's what we are looking for we pick the columns A and C
if (row[1] === searchTerm) {
result.push([ row[0], row[2] ])
}
});
});
// If we found values we return them, otherwise we return emptry string
return result.length ? result : '';
}
The thing is I need this formula to be re-calculated when a cell in a sheet with a name starting with DATA_SHEET_ changes.
I see most answers (and what I usually do) is to pass the range we want to watch as a parameter for the formula even if it's not used. But in this case it will not work because we don't know how many ranges are we watching and we don't even know the whole sheet name (it's injected by a web service using Google Spreadsheet API).
I was expecting Google Script to have something like range.watch(formula) or range.onChange(this) but I can't found anything like that.
I also tried to create a simple function that changes the value of cell B2 which every formula depends on but I need to restore it immediately so it's not considered a change (If I actually change it all formulas will break):
// This does NOT work
function forceUpdate() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const range = sheet.getRange(1, 1);
const content = range.getValue();
range.setValue('POTATO');
range.setValue(content);
}
So I don't know what else can I do, I have like a hundred formulas on multiple sheets doing this and they are not updating when I modify the DATA_SHEET_... sheets.
To force that a custom function be recalculated we could use a "triggering argument" that it's only taks will be to trigger the custom function recalculation. This triggering argument could be a cell reference that will be updated by a simple edit trigger or we could use an edit installable trigger to update all the formulas.
Example of using a cell reference as triggering argument
=sampleFormula("searchTerm",Triggers!A1)
Example of using an edit installable trigger to update all the formulas
Let say that formulas has the following form and the cell that holds the formula is Test!A1 and Test!F5
=sampleFormula("searchTerm",0)
where 0 just will be ignored by sampleFormula but will make it to be recalculated.
Set a edit installable trigger to fire the following function
function forceRecalculation(){
updateFormula(['Test!A1','Test!F5']);
}
The function that will make the update could be something like the following:
function updateFormula(references){
var rL = SpreadsheetApp.getActive().getRangeList(references);
rL.getRanges().forEach(function(r){
var formula = r.getFormula();
var x = formula.match(/,(\d+)\)/)[1];
var y = parseInt(x)+1;
var newFormula = formula.replace(x,y.toString());
r.setFormula(newFormula);
});
}
As you can imagine the above example will be slower that using a cell reference as the triggering argument but in some scenarios could be convenient.
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()
I know this question has been asked before but the answers given are not valid for my case because it's slightly different.
I've created a formula that looks for sheets with a pattern in the name and then uses it's content to generate the output. For example
function sampleFormula(searchTerm) {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheets = spreadsheet.getSheets()
.filter(function(sheet) {
// If sheet name starts with DATA_SHEET_...
return sheet.getSheetName().indexOf('DATA_SHEET_') === 0;
});
const result = [];
sheets.forEach(function(sheet) {
// We get all the rows in the sheet
const rows = sheet.getDataRange().getValues();
rows.forEach(function(row) => {
// If the row it's what we are looking for we pick the columns A and C
if (row[1] === searchTerm) {
result.push([ row[0], row[2] ])
}
});
});
// If we found values we return them, otherwise we return emptry string
return result.length ? result : '';
}
The thing is I need this formula to be re-calculated when a cell in a sheet with a name starting with DATA_SHEET_ changes.
I see most answers (and what I usually do) is to pass the range we want to watch as a parameter for the formula even if it's not used. But in this case it will not work because we don't know how many ranges are we watching and we don't even know the whole sheet name (it's injected by a web service using Google Spreadsheet API).
I was expecting Google Script to have something like range.watch(formula) or range.onChange(this) but I can't found anything like that.
I also tried to create a simple function that changes the value of cell B2 which every formula depends on but I need to restore it immediately so it's not considered a change (If I actually change it all formulas will break):
// This does NOT work
function forceUpdate() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheet = spreadsheet.getActiveSheet();
const range = sheet.getRange(1, 1);
const content = range.getValue();
range.setValue('POTATO');
range.setValue(content);
}
So I don't know what else can I do, I have like a hundred formulas on multiple sheets doing this and they are not updating when I modify the DATA_SHEET_... sheets.
To force that a custom function be recalculated we could use a "triggering argument" that it's only taks will be to trigger the custom function recalculation. This triggering argument could be a cell reference that will be updated by a simple edit trigger or we could use an edit installable trigger to update all the formulas.
Example of using a cell reference as triggering argument
=sampleFormula("searchTerm",Triggers!A1)
Example of using an edit installable trigger to update all the formulas
Let say that formulas has the following form and the cell that holds the formula is Test!A1 and Test!F5
=sampleFormula("searchTerm",0)
where 0 just will be ignored by sampleFormula but will make it to be recalculated.
Set a edit installable trigger to fire the following function
function forceRecalculation(){
updateFormula(['Test!A1','Test!F5']);
}
The function that will make the update could be something like the following:
function updateFormula(references){
var rL = SpreadsheetApp.getActive().getRangeList(references);
rL.getRanges().forEach(function(r){
var formula = r.getFormula();
var x = formula.match(/,(\d+)\)/)[1];
var y = parseInt(x)+1;
var newFormula = formula.replace(x,y.toString());
r.setFormula(newFormula);
});
}
As you can imagine the above example will be slower that using a cell reference as the triggering argument but in some scenarios could be convenient.
I'm working on creating a system for other teachers to easily track their students' progress. I've got a spreadsheet with individual sheets for each student and then a sheet for an overview of all students. The spreadsheet has the following script attached to it:
function SheetNames() {
try {
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets()
var out = new Array( sheets.length+1) ;
for (var i = 1 ; i < sheets.length ; i++ ) {
out[i] = [sheets[i-1].getName()];
}
return out
}
catch( err ) {
return "#ERROR!"
}
}
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Student List')
.addItem('Update Student List', 'SheetNames')
.addToUi();
}
In the "Overview" sheet, I have a cell that just contains =SheetNames(). When I first enter the custom function, the list populates. When I open the spreadsheet, the menu is added as it should be. However, when I click the menu item, the list of students on the "Overview" sheet is not updated. Is there anyway to make this function automatically update?
It will not update because all that function does is return an array. When you use the =SheetNames() notation, you are giving it a range to write to (the cell where you put the formula).
When you run the function via menu click, it doesn't know anything about the target range in the spreadsheet. It simply creates a variable ('out'), stores it in memory, then destroys it when the function finishes executing.
If you'd like to write to specific range, you should reference it in your function. Here's a quick example of the function that writes a random number from 0 to 100 to cell A1 of the first sheet
function populateCell() {
var number = Math.random() * 100;
var cell = SpreadsheetApp.getActiveSpreadsheet()
.getSheets()[0]
.getRange("A1");
cell.setValue(number);
}
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('menu')
.addItem('run', 'populateCell')
.addToUi();
}
It will not work as a custom function though. As per GAS documentation, if a function is being called from a cell (is custom), that function is read-only and can't set values https://developers.google.com/apps-script/guides/sheets/functions
Also, there's no need to use custom function because you seem to only need this code for specific range. If you intend to write to whatever cell the cursor is currently in, you should remove the reference to specific cell and use sheet.getActiveCell() instead. Only use custom functions for general functionality not tied to specific range.