How do I update the content selected from a dropdown cell if the source subsequently changes? (Insert cell reference instead of literal value.) - google-sheets

I have two sheets used to track a construction project.
On the first sheet, a list of tasks is incorporated into a timeline with cost projections, etc. The tasks are something like the following:
Cut Concrete
Pour new pad
Frame
Roof
The second sheet is for tracking individual purchases, each of which is associated with a task from the first sheet (e.g., Cut Concrete). It looks something like the following:
DATE PAYEE ITEM CATEGORY COST
----- ---------- ---------- -------- ------
10/25 Home Depot (10) 2x4's Frame ▽ $54.00
Using Data Validation, the Category dropdown in the second sheet references the list of items from the first sheet. This is working perfectly. Here's the problem...
If I change the item on the first sheet (for instance, "Frame" to "Framing"), although the dropdown is updated, any previously entered rows (such as the one shown above), just show a validation error (i.e., a red indicator in the right corner of the cell).
Since a construction project can easily have hundreds of items purchased, rather manually looking for data validation errors, is there a way to have the second sheet's values updated? For instance...
Add a script that watches for content changes in the first sheet. When the user starts editing a "task" cell, its original value is noted; and upon exiting the cell, if the value has changed, the script looks through the second spreadsheet for the original value and replaces it with the new one. (Seems like a lot of hassle.)
Find some way for the dropdown to insert a cell reference to the first sheet instead of the actual value. That way, the dropdown cell is always referencing the source item (i.e., the "task" cell's current content).
A more obvious feature I don't know about.

As per my favored possibility noted in my question, I figured out how to write a script that would run after a value was selected from the dropdown, thus overwriting the literal value with a cell reference.
The following script runs after the user makes a selection in the dropdown menu:
function onEdit(event){
var activeSheet = event.source.getActiveSheet();
var activeSheetName = activeSheet.getName();
var activeCell = activeSheet.getActiveCell();
var activeColumn = activeCell.getColumn();
if (activeSheetName == "Envelope (Spent)" && activeColumn === 4) {
var destinationCell = activeCell;
var destinationContent = destinationCell.getValue();
var sourceSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var sourceSheetName = sourceSheet.getName();
var sourceRange = sourceSheet.getRange("D:D");
var sourceValues = sourceRange.getValues();
for (var i = 0; i < sourceValues.length; i++) {
if (sourceValues[i] == destinationContent) {
var sourceRow = sourceSheet.getRange("D" + i + ":D" + i).getRow() + 1;
destinationCell
.setValue("='" + sourceSheetName + "'!D" + sourceRow)
.setNote(destinationContent)
;
}
}
}
}
To allow for an easy recovery in case the two sheets somehow get out of sync, the originally selected value, derived from the dropdown's data validation, is inserted as a note. I figured it was easier to clear all the notes in the future than to find myself with a bunch of entries that don't correspond with the source list of tasks.

The data validation in Google sheets is always inserting the Literals you type or pick so you can't link it to the original cell. You have to solve the whole thing through scripts
Get a list of all data validation items through onOpen()
Create an onEdit() function that runs if the data validation range is edited, checks which field is changed and then goes through the purchases, checks which purchases have the old value and replaces them with the new one.

Related

How do I use ARRAYFORMULA and IF to apply a script to an entire column in Google Sheets?

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.

How to modify data whilst importing in google sheets?

I have more than 20k form responses (google sheet and google form) where some guys have selected the wrong data which is visible in my responses. How I know its wrong is because they needed to select the activity (an attribute) but they selected the similar activity name (let's call it X) which was for the previous year and this year's activity (let's call it Y) should have been the different one.
I know that after a certain date all the X activities are Y, so I need to modify the data while importing it from the responses.
I tried conditional formatting on the data but then the importrange doesn't work, it needs cells to be empty to work.
I learned about query statements but it doesn't allow UPDATE.
Please help me do this, I am okay if we need to use a macro. I'm looking for something like this (note that the following is the logic I'm looking for and not the actual code):
if date>"a date" and FC==X:
FC=Y
#FC being the column I wanna modify
Edit: I am unable to share the table as its confidential. Can tell you that first column is date/time of form and then there are 149 columns, one of them I need to modify based on the date. Let's Assume it has just 2 columns, A: date, B: activity (has 20 activities). So, if they have filled "X" activity after then change that activity to Y. I hope it helps in understanding.
Edit 2: Have put a dummy file as asked. So now the problem statement is after 21 May 2022 (inclusive) all "6" activity must be "2"
Try
function onOpen() {
SpreadsheetApp.getUi().createMenu('⇩ M E N U ⇩')
.addItem('👉 Update', 'myFunction')
.addToUi();
}
function myFunction() {
// parameters
var param = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Param')
var from = param.getRange('A2').getValue()
var before = param.getRange('B2').getValue()
var after = param.getRange('C2').getValue()
// data
var sh = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()
var range = sh.getDataRange()
var data = range.getValues()
data.filter(r => (r[0] >= from && r[1] == before)).forEach(r => r[1] = after)
range.setValues(data)
}
To avoid hardcoding and potential issue with dates, I put all parameters in a new tab called "Param" as follows

Creating a Macro in google spreadsheet to search and then write text

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
}

arrayformula that can "skip" rows

I need to introduce functionality into a google spreadsheet that will allow the user to edit the result of an array formula. The reason for the requirement is that an ARRAYFORMULA sets a default value for a group of cells, but the user sometimes needs to overwite these defaults. I'd like to know if this is even remotely possible.
example:
Row(#)|Array_1 |Array_2
------------------------------------
1 |a |=arrayformula(Array_1)
2 |b |""
3 |c |""
4 |d |""
So all rows in Array_2 are populated by an array formula. However the user wants to go directly to the second cell in Array_2 and change its value. Of course, by design ARRAYFORMULA will break. Is there some way to modify ARRAYFORMULA, so that it will simply skip over the cell that the user has edited and continue on its way as if nothing has happeded?
I realize this is an old problem but I was searching for this today and made a script that works for me.
This script puts a formula in an adjacent cell when a cell is edited in the second column. This way you can just overwrite the formula if you need to input something manually and you don't need to have the formulas go into all of the rows beforehand. I had people accidentally edit the formula and mess it up most of the time when they were pre-filled, so this works better for me.
function onEdit() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheetList = ["Sheet1","Sheet2","Sheet3"]; // list of sheets to run script on
for (i = 0; i < sheetList.length; i++) {
var sheetName = ss.getSheetByName(sheetList[i]);
// only runs if sheet from sheetList is found
if (sheetName != null) {
var aCell = sheetName.getActiveCell();
var col = aCell.getColumn();
var adjacentCell = aCell.offset(0, -1);
var formula = 'INPUT FORMULA HERE'; // put the formula you want in the adjacentCell here. Don't use it in an arrayformula
// only runs if active cell is in column 2, if the adjacentCell is empty, and if the active cell is not empty(otherwise it runs if you delete something in column 2)
if(col==2 && adjacentCell.getValue()=="" && aCell.getValue()!="") {
adjacentCell1.setValue(formula);
}
}
}
}
Will changing the value not throw out the output of the remaining formulas?
If not, you could set up 2 new tabs: one which will receive the user over-ride values, and another "reflection" tab which you populate with
IF(tabOverride!Rx:Cy, tabOverride!Rx:Cy, tabArray!Rx:Cy)
basically the new tabs are cloned layouts of your array tab, creating an override input layer, plus a presentation layer that uses the IF('override value exists', 'then show override', 'else show array out put') logic to return the desired values.
hope that makes sense!

Force IMPORTRANGE to update at certain intervals

I saw several complains about the delay of updating data through IMPORTRANGE in Google Sheets but I need the opposite and don't want the second sheet to get updated automatically, just update at the end of the day for example.
The code is already like this:
=IMPORTRANGE("The Key","The_Page!B:D")
I hacked around this by, on both spreadsheets by creating a refresh loop using a NOW cell, with both spreadsheets crossreferencing each other's NOW cell.
When the original sheet gets appended with a form submission or something, it updates its own NOW cell, and reupdates own IMPORTRANGE cell. The second spreadsheet follows suit, updating its own NOW cell to provide the original sheet with the correct data. Because the second spreadsheet has updated itself, it also updates the main IMPORTRANGE which refreshes the data you want it to display in the first place, as well as the IMPORTRANGE cell which gets the NOW cell from the original spreadsheet
At least, I'm pretty sure that's how it works. All I know, and all I care about, frankly, is that it works
Maybe you need to use the script editor and write a simple function of the kind:
function importData()
{
var ss = SpreadsheetApp.getActiveSpreadsheet(); //source ss
var sheet = ss.getSheetByName("The_Page"); //opens the sheet with your source data
var values = sheet.getRange("B:D").getValues(); //gets needed values
var ts = SpreadsheetApp.openById("The Key"); //target ss - paste your key
ts.getSheetByName("Name of the target sheet").getRange("B:D").setValues(values);
}
And then add a time-driven trigger to this project (Resources > Current project's triggers > Add a new one).

Resources