I am building a function - an ArrayFormula that filters data from multiple sheets. It is built using TextJoin (or Concatenate, it doesn't matter) - that way I can dynamically change the range as I add more sheets.
In order to execute this function, I copy the formula, go to a cell, press "="+ Paste + Enter and it will be executed.
My question is how can I execute it automatically. I thought maybe I can use Indirect but can't figure out how to do it.
For example, the formula I built is:
arrayformula(filter({Sheet2!A2:H;Sheet3!A2:H;Sheet4!A2:H},len({Sheet2!A2:A;Sheet3!A2:A;Sheet4!A2:A})))
In this spreadsheet there's an example: https://docs.google.com/spreadsheets/d/12PO66ymfPpnO5tNfXr0MHjTY8rIzHfwVY5gtV6vccD0/edit#gid=0
cell A3 and A4 are TextJoin functions to create the range dynamically
cell A1 concatenates A3+A4 to create the function
cell A9 is where I executed it by pressing "="+ Paste + Enter
As far as I know, this is not possible
But there is a workaround (or better way to do it, depends how you look at it).
There is no INDIRECT specifically for formulas, however:
Workaround
A function exists in Apps Script:
formulaCell.setFormula(formulaString)
You don't need much Apps Script to get this working in the way you envision it, the Tutorials can be helpful to get started.
I would also recommend this approach as formulae will quickly become limiting once you cover more complex ground, and will also be much, much easier to maintain and modify. You also get the added benefit of longer execution times.
Sample script
function createFormula() {
// initializing variables for the Spreadsheet file and the sheet
let file = SpreadsheetApp.getActive();
let sheet = file.getSheetByName("Sheet1");
// This is the range that represents the sheets to filter
let sheetsToProcessRange = sheet.getRange("C2:C");
let sheetsToProcess = sheetsToProcessRange.getValues();
// filter out the empty ones
sheetsToProcess = sheetsToProcess.filter(row => row[0])
// At the moment its a 2D array
// [["Sheet1"],["Sheet2"],["Sheet3"]]
// This will change it to a 1D array (just to make code easier to read)
// ["Sheet1","Sheet2","Sheet3"]
sheetsToProcess = sheetsToProcess.map(row => row[0])
// initializing the array notation strings
let filterArrayString = "{";
let lenArrayString = "{"
// for each sheet in the array, add SheetX!Range to the strings
sheetsToProcess.forEach(sheetName => {
filterArrayString += `${sheetName}!A2:H;`
lenArrayString += `${sheetName}!A2:A;`
})
// Remove trailing semicolon
filterArrayString = filterArrayString.slice(0, filterArrayString.length - 1)
lenArrayString = lenArrayString.slice(0, lenArrayString.length - 1)
// Add in final bracket
filterArrayString += "}"
lenArrayString += "}"
// construct whole formula string
let formulaString = `=arrayformula(filter(${filterArrayString},len(${lenArrayString})))`
// Choose the cell for it and set it as a formula
let formulaCell = sheet.getRange("A9")
formulaCell.setFormula(formulaString)
}
This, run from the script editor will insert your desired formula in cell A9 in Sheet1.
I have used some more advanced features of JavaScript to do this succinctly but you don't have to do it this way.
You already know how to construct most of the formula with formulae. So you could have a script that just grabs this value and so would shorten the script considerably.
function createFormulaSimplified(){
// Initializing variables
let file = SpreadsheetApp.getActive();
let sheet = file.getSheetByName("Sheet1");
// Range where your already constructed formula lives
let formulaCell = sheet.getRange("A1")
let formulaString = formulaCell.getValue()
// Constructing and setting the formula
let formulaString = "=" + formulaString
let formulaCell = sheet.getRange("A9")
formulaCell.setFormula(formulaString)
}
In effect, this does the same thing as the formula above, but less of the work is done by the script. Basically the only thing it does is grab the formula you have already constructed, adds an = to the start and assigns it to cell A9.
Of course, you could also do all the work that the formula does in Apps Script, but that is for another answer of another question.
References
Apps Script Tutorials
setFormula(formula)
Handling text in JavaScript
Related
I have little to no coding knowledge, so apologies if the solution is too obvious!
I am trying to add a Last Modified column to a Google Sheets file. To do this, I am using an AppScript function with the following code:
function setTimestamp(x) {
if(x != ""){
return new Date();
}
}
This works fine when I use setTimestamp(x) in my file. However, I am combining this with a Zapier action that creates a new row whenever new media is added. Every time a new row is created, any existing formulas are removed.
I assume I need to use ARRAYFORMULA to apply the setTimestamp formula to newly-created rows, but it must only apply to rows that aren't blank.
I have tried the following:
={"Last Modified";ARRAYFORMULA(setTimestamp(A2:A))} -> Only worked on first row
={"Last Modified";ARRAYFORMULA(B2:B=setTimestamp(A2:A))} -> Broke the file
={"Last Modified";ARRAYFORMULA(IF(A2:A)=1,setTimestamp(A2:A),"")} -> Expected 1 argument, got 3
Is there a way I can combine the IF into the script or a better way to solve the problem?
A public version of my file is available here: https://docs.google.com/spreadsheets/d/13zkVRPr2Wh5bHjCT8cenInHnBk7qkMkuEMdwUxC_cRU/edit?usp=sharing
All data is dummy data and stock photos.
Unfortunately, arrayformula does not function as an array map function for custom functions. (Even for native functions where you may expect it to work that way, it does not always, sadly.)
To handle array range, we need the custom function to handle array range directly. That also limits the number of individual calls to custom functions, which materially saves execution time.
To handle array range, there are 2 ways. I'll comment on both.
Array range directly as input of custom function
If the input is a single cell, it is read directly
If the input range spans more than a single cell, the data is read as nested lists: a list of lists of rows.
For example, A1 will be read as the data in A1. A1:B2 will be read as [[A1, B1], [A2, B2]].
You can remember it as columns of rows.
As for the input data format, numbers are taken without the display format. Texts are taken as strings.
If output is an array range, the result will automatically expend.
Thus, in your example, in B2 you can almost do
=setTimestamp(A2:A)
where setTimestamp() has been modified to
function out = setTimestamp(arr) {
out=Array(mat.length);
for (i=0;i<mat.length;i++){
j=0
if(arr[i][j] != ""){
out[i]=new Date();
}
}
return out
}
For more details, see the official help page. (Over the years, more details have become available.)
Almost, but not quite. For your direct question, above provides the answer. However, you seem to have an implicit requirement that your custom function is executed every time a new URL is found. Be careful that what happens here is that every time Google Sheet updates cell content, a new Date() is created and outputted.
Array range read within custom function
Since you know your URLs are in A2:A, and you want the output of your custom function to be B2:B, you can read and modify those ranges directly within your custom function via the Range Class.
In this route, you may find getLastRow(), getLastColumn() in Sheet and getNextDataCell() in Range convenient.
When you need to execute your custom function, you can run it manually or add onEdit() trigger to your custom function. (But onEdit() itself can mean substantial UI lag when using the sheet. It's usually more appropriate for sheets that parse external data automatically. See other triggers in the link for motions.)
In your example, you can almost do
function setTimestamp() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var lastRow = sheet.getLastRow();
var row=1;
var cell = sheet.getRange(row,1).getValue();
while (row<=lastRow) {
if(cell.getValue() != ""){
sheet.getRange(row,2).setValue(new Date());
}
cell = sheet.getRange(row,1,lastRow).getNextDataCell(SpreadsheetApp.Direction.DOWN);
row=cell.getRow();
}
}
which will scan for all URLs in A2:A and write current time to B2:B when executed.
Again, your example implicitly points to updating only when a new URL is found. So be careful about that. Use triggers as needed.
As for the need to place formula in B1, you can (and should) reference the output of your other application in a different sheet so that you or a different application of yours can edit without conflict.
Thus, for what was asked, we have everything.
I am not familiar with google sheets or excel, so I'm not even sure if what I want to do is possible.
I have one thousand rows in a google sheets document. I need a column that has a number to represent the row. Starting at an arbitrary row (lets say row 5 in the document), That cell value needs to be 1. The next row needs that column value to be 2. Then 3, 4, 5, 6... all the way to 1000. It's not feasible to do that manually, so is there a way to automatically fill in the cells with the values I need?
This next part is what I can't figure out. I've found a few solutions to the first part, but none that work with this extra condition. After I generate these numbers, I need to reorder the rows (reordering can't be done before). The problem is that if I use some formula in google sheets, as soon as I generate the numbers then reorder the rows, the formula either breaks or recalculates the numbers, setting them back to 1, 2, 3..., when (for example) after the reorder I would expect 42, 815, 934...
Is what I want to do possible, and if so how can I accomplish this?
Besides the solution that has already been provided, you can also make use of Apps Script and write your own script in order to change the cell values.
Some methods which can be of help to you are:
getRange() - in order to retrieve the range from the sheet;
setValue() - in order to set the value of the cell/s;
Thererefore, your script will end up something similar to:
function setCellValues() {
let spreadsheet = SpreadsheetApp.openById("SS_ID");
let sheet = spreadsheet.getSheetByName("SHEET_NAME");
let startRow = "START_ROW";
let startCol = "START_COL";
for (let i = 0; i < 1000; i++) {
sheet.getRange("START_ROW" + i, "START_COL").setValue(i + 1);
}
}
As for reordering the rows, you can use Apps Script for this again or a simple SORT function.
Reference
Apps Script Sheet Class - getRange();
Apps Script Range Class - setValue();
SORT function.
I want a formula to generate random data of birth dates for a specific years (Example: 1995 to 2002) and make it Array like this:
Sheet URL: https://docs.google.com/spreadsheets/d/1XHoxD-hNmpUOMVm_u-cz-4ESrabodsrS0fIfaN-n4js/edit
That might not be the best approach but it will get you closer to what you want:
=DATE(RANDBETWEEN(1995,2002),RANDBETWEEN(1,12),RANDBETWEEN(1,31))
There are two issues with this approach:
you might get a day that does not exist for the particular month. For example, 2/28/2021 exists, but 2/29/2021 does not exist.
I wasn't able to generate an array but only drag down formulas. When I generate an array, the same random numbers are used and as a result the dates are the same.
For the first issue, you can use isdate to check if the random date returned is correct. For example, 2/29/2021 is a wrong date (I hardcopied that date).
but I guess you can filter out the FALSE cases.
I really hope other people can come up with a better approach.
You could try (as I demonstrated in your sheet):
=ARRAY_CONSTRAIN(SORT(SEQUENCE(DATE(1992,12,31)-DATE(1900,1,1),1,DATE(1900,1,1)),RANDARRAY(DATE(1992,12,31)-DATE(1900,1,1)),1),COUNTA(A2:A),1)
SEQUENCE(DATE(1992,12,31)-DATE(1900,1,1),1,DATE(1900,1,1)) - Is used to create an array of valid numeric representations of true dates between 1-1-1900 and 31-12-1992.
SORT(<TheAbove>,RANDARRAY(DATE(1992,12,31)-DATE(1900,1,1)),1) - Is used to sort the array we just created randomly.
ARRAY_CONSTRAIN(<TheAbove>, COUNTA(A2:A),1) - Is used to only return as many random birth-dates we need according to other data.
Note that this is volatile and will recalculate upon sheet-changes and such. Also note that this is just "slicing" a given array and may fall short when you try to use it on a dataset larger than the given array.
As Google Sheets can deal with dates as integers (~ number of days since 1900), choosing a random date between two dates can be a single call to RANDBETWEEN (with the output formatted as Date).
With your initial date written in B1 and your end date in B2, the formula is simply:
=RANDBETWEEN($B$1,$B$2)
You can paste this formula in as many cells as you want, to generate N different random dates.
Of course, as other answers involving random generators in your sheet, the formula will be recomputed at each change. My suggestion to overcome this would simply be to copy/paste the output, using the "Paste special > Values only" option (right click or "Edit" menu).
Script Solution
Just for sake of completeness, here is a solution using a script
Initial Considerations
This cannot function like a in sheet function/formula.
https://developers.google.com/apps-script/guides/sheets/functions
Custom function arguments must be deterministic. That is, built-in spreadsheet functions that return a different result each time they calculate — such as NOW() or RAND() — are not allowed as arguments to a custom function. If a custom function tries to return a value based on one of these volatile built-in functions, it will display Loading... indefinitely.
A custom function cannot affect cells other than those it returns a value to. In other words, a custom function cannot edit arbitrary cells, only the cells it is called from and their adjacent cells. To edit arbitrary cells, use a custom menu to run a function instead.
So a normal script is needed.
The Script
/**
* Sets the values of a range to random dates.
*/
function generateRandomBdays(range, start, end) {
let height = range.getHeight();
let width = range.getWidth();
let output = [];
for (let i = 0; i != height; i++) {
let row = [];
for (let j = 0; j != width; j++) {
row.push(randomBday(start, end));
}
output.push(row)
}
range.setValues(output);
}
/**
* Generates a random date beween start and end
*/
function randomBday(start, end) {
if (start < 2000) start = start - 1900
start = new Date(`${start}`);
if (end < 2000) end = end - 1900
end = new Date(`${end}`);
let bday = new Date(
start.getTime() + (Math.random() * (end.getTime() - start.getTime()))
);
return bday;
}
/**
* Gets active selection and fills with random dates
*/
function main(){
let file = SpreadsheetApp.getActive();
let sheet = file.getActiveSheet()
let range = sheet.getActiveRange();
// ============
generateRandomBdays(range, 1995, 2002); // Change these years to your liking
// ============
}
/**
* Creates menu when sheet is opened.
*/
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Generate Birthdays')
.addItem('Generate!', 'main')
.addToUi();
}
Which works like this:
Installation
You will have to copy it into your script editor and then run one of the functions to authorize the script with the permissions it needs. Then next time you open the sheet you should have the menu available.
Alternatively you can delete the onOpen function and just use it from the script editor.
Within the main function, customize the range of years you need.
References
Apps Script
Overview of Spreadsheet Service in Apps Script
I have a sheet with a column of data where the entry is one of two strings, for simplicity's sake we'll say "A" and "B". I want another column of calculated data which is the differential between the number of "A"s and the number of "B"s up to that point, so just a countif()-countif() with the range increasing by one row. I can do this using =countif(A$2:A2, "A") - countif(A$2:A2, "B"), but it means I have to keep filling that equation down to cover any newly entered data, so I figured an array formula would be the best option to not have to do that. However when I try to use =arrayformula(countif(A$2:A, "A") - countif(A$2:A, "B")), it only populates a single cell with the difference counting the entire column. Is there a way to use an array formula so that it increases the range by one row for every row it populates?
=ARRAYFORMULA("A2:A"&ROW(A2:A))
This will give a dynamic string for the right range, which in theory, you should be able to wrap with INDIRECT, to plug into COUNTIF.
But COUNTIF only works in some circumstances with ARRAY FORMULA
Unfortunately, AFAIK, only if you use COUNTIF like this:
=ARRAYFORMULA(COUNTIF(A1:A100,A1:A100))
will it work as an array formula. Note how both the ranges in the arguments are the same.
MMULT
There may be a way along these lines with MMULT:
=ARRAYFORMULA(
MMULT(
(A2:A100 = TRANSPOSE(A2:A100)) * (ROW(A2:A100) >= TRANSPOSE(ROW(A2:A100))),
SIGN(ROW(A2:A100))
)
)
But I couldn't get that to work because I am not 100% sure on how to use MMULT in this way, but I have seen that type of solution elsewhere.
An Apps Script Workaround
As you have seen, what you want to achieve with sheet functions quickly gets very complicated. If you wanted to use more letters or different rules, it would get tough to maintain. Apps Script can make these things much simpler.
So here is a custom Apps Script function:
function myFunction() {
// Initializing
let file = SpreadsheetApp.getActive();
let sheet = file.getSheetByName("Sheet1");
var lastRow = sheet.getLastRow();
// This is the range of the As and Bs
let range = sheet.getRange(2,1, parseInt(lastRow) - 1,1)
let rows = range.getValues();
// Creating an object to keep track of the count
let tracker = {}
// This will be the output column
let newCol = []
rows.forEach(row => {
tracker[row[0]] += 1
// Adding a row to the output
newCol.push([tracker.A - tracker.B])
})
return newCol
}
Paste that into the script editor and you can use it like this:
Demo:
You can change the name of the function in the script editor according to what you need it for, just remember to call it with that name from the sheet.
References
Main Page
Sheets Guide
Tutorials
Sheets Reference
INDIRECT
MMULT
try:
=ARRAYFORMULA(
COUNTIFS(A2:A, A2:A, A2:A, "A", ROW(A2:A), "<="&ROW(A2:A))-
COUNTIFS(A2:A, A2:A, A2:A, "B", ROW(A2:A), "<="&ROW(A2:A)))
Alternatively, you may this IF statement:
=ARRAYFORMULA(IF(K2:K<>"",K2:K*B294,""))
Results (Above formula is applied on L2 row onwards):
Source: https://blog.sheetgo.com/google-sheets-formulas/arrayformula-google-sheets/
What I am trying to accomplish is I would like to search for a term in one cell, if that cell has the term write text to another cell. My specific example would be I would like to search for the term 'DSF' in column 4. If I find 'DSF' it would then write 'w' in column 5 & write '1.2' in column 3. This is searched per row.
I do understand the the .setvalue will write the needed text, but I do not understand how to create a search function. Some help would be greatly appreciated.
EDIT
Here is the code I am working with at the moment. I am modifying it from something I found.
function Recalls()
{
var sh = SpreadsheetApp.getActiveSheet();
var data = sh.getDataRange().getValues(); // read all data in the sheet
for(n=0;n<data.length;++n){ // iterate row by row and examine data in column D
if(data[n][3].toString().match('dsf')=='dsf'){ data[n][4] = 'w'}{ data[n][2] = '1.2'};// if column D contains 'dsf' then set value in index [4](E)[2](C)
}
//Logger.log(data)
//sh.getRange(1,1,data.length,data[3].length).setValues(data); // write back to the sheet
}
With the Logger.log(data) not using the // It works properly but it overwrites the sheet, which will not work since I have formulas placed in a lot of the cells. Also, Maybe I did not realize this but Is there a way to do a live update, as in once I enter text into a cell it will research the sheet? Otherwise having to 'run' the macro with not save me much time in the long run.
Try this. It runs when the sheet is edited. It only captures columns C,D,&E into the array and only writes back those columns. That should solve overwriting your formulas. It looks for 'DSF' or 'dsf' in column D (or contains dsf with other text in the same cell either case). Give it a try and let me know if I didn't understand your issue.
function onEdit(){
var sh = SpreadsheetApp.getActiveSheet();
var lr = sh.getLastRow()// get the last row number with data
var data = sh.getRange(2,3,lr,3).getValues(); // get only columns C.D,& E. Starting at row 2 thur the last row
//var data = sh.getDataRange().getValues();// read all data in the sheet
for(n=0;n<data.length-1;++n){ // iterate row by row and examine data in column D
// if(data[n][0].toString().match('dsf')=='dsf'){
if(data[n][1].match(/dfs/i)){ //changed to find either upper or lower case dfs or with other text in string.
data[n][2] = 'w';
data[n][0] = '1.2'};
}
sh.getRange(2,3,data.length,data[3].length).setValues(data); // write back to the sheet only Col C,D,& E
}