Replace INDIRECT() in data validation rule - google-apps-script

I had a working Excel spreadsheet that used indirect() in the data validation and it worked fine. I uploaded it to sheets and converted it, now the indirect does not work.
I have found a link on the support forum that explains it does not work in Chrome but appears to work in Firefox, and the answers and workarounds seem to be for generating a secondary list... which is what I want, but in a data validation across a row.
I have knocked up a simple test sheet, hopefully public and the script editor is visible:
https://docs.google.com/spreadsheets/d/1KUgrdXKIKlk1DWvDOX9cY3B2VnRH_5h_vKuZJlqUlN8/edit?usp=sharing
Hopefully you can see what I'm after. I want the validation in C8 to be the list of items in the category based in B8; C9 based on B9 etc.
EDIT and Update
The question is about a replacement to indirect() in a data validation rule. While I did find a way round this by using indirect(), I preferred the version mentioned by Desire (to whom I have attributed the answer), but I thought I'd document my solution in case the sheet above becomes unavailable, or you cannot access it, or you just wanted a bit more detail.
So, for My Demo I have this:
In A1:C5 are my lists of data with the titles.
In the range B8:B12 I applied a data validation rule of value in range of A1:C1 - this gives the first dropdown.
In Cell E8 I put the formula =transpose(filter($A$2:$C$5, $A$1:$C$1 = B8)) and then copied this down to E12
Finally I put the following in a function and ran it in the script editor.
function runMeOnce() {
var dst = SpreadsheetApp.getActive().getSheetByName('Sheet1').getRange('C8:C12');
var rules = [];
for (var i = 8; i < 13; i++) {
var src = SpreadsheetApp.getActive().getSheetByName('Sheet1').getRange("E" + i + ":H" + i);
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(src).build();
rules.push(rule);
}
dst.setDataValidations(rules);
}
That's all there is, no more onEdit() triggering.
NOTE There is one downside I bumped into with this method though. I have this in place for 6000+ rows in my actual spreadsheet, and across multiple sheets, with some dropdowns having 50-100 items in. This solution seriously eats into the (current) 2 million cell limit.
Hope this helps someone.

Data Validation rule of the type "List of items" takes only a comma-separated list of values as its parameter, and does not evaluate any formulas you try to put there. It does not matter what the function returns, because it will not be called. If you put, say "=sqrt(A10)" in the field "List of items", that only means that the validation rule will require the string "=sqrt(A10)" to be entered in the cell.
Similarly with "List from a Range". Either what you enter parses as range notation, or it does not. The string "=getValidationRange(B8)" does not parse as range notation, hence the error. The function is never called.
The only type of validation that calls a function is "Custom formula". If you use it, then the validation can be performed as intended: for example,
=match(C8, filter(A2:C5, A1:C1 = B8), 0)
requires the content of C8 to be in the column of the table A2:C5 under the heading that matches the category in B8. However, with a custom formula you do not get a dropdown in a cell.
To get a dynamic dropdown, one can either
Use an auxiliary range
For example, enter filter(A2:C5, A1:C1 = B8) in cell F1, so that the F column is for the categories currently selected. The data validation would be "List from a Range", F1:F. This is a fine workaround for one validation rule, but takes more work when you have multiple ones.
Use a triggered script
Use a script that is triggered on edit and sets data validation rules accordingly; this is discussed in How do you do dynamic / dependent drop downs in Google Sheets? among other places.

Based on the sacrificing a goat issue, I did find a simple(ish) way around the problem that still uses indirect().
Set up the named ranges as previously using the titles in CamelCase. In my example I have CatA, CatB, and CatC - i.e. the white space needs removing.
At the end of a row (or in another sheet) transpose the chosen named range (in cell E8: =transpose(indirect(substitute(B8, " ", ""))) copy this down as far as you need.
At this point it's good to note that because we are unsing builtin functions, the speed is so much better, as can be seen by my example.
Now the painful bit. For each subcategory cell (C8, C9 etc in my example), you need to add the validation independently as a range of E8:ZZ8 (obviously ZZ8 needs reigning in a bit) and E9:ZZ9 etc. It doesn't seem to do referential so if you select all the cell in the column, they all only look at the data you specifically type in the box... I might just not have worked out R1C1 notation here, however. I tried.
This can be scripted on GAS to create the R1C1 validation function and then apply it to the range.

Related

Range.SetValues() does not insert data on one sheet, on the other works. What is the reason?

I have a GoogleSheet with basically two sheets, which are very similar in terms of data collected.
I need to calculate same values for both sheets, but source data is in different columns.
Therefore I created three files in AppsScript:
Common.gs - with common function definitions
sheet1.gs
sheet2.gs - both sheet1 and sheet2 have only definitions of proper ranges in particular columns and one function to run script, which essentially calls functions defined in Common.gs, like so in sheet1.gs:
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet1")
var createdColumn = sheet.getRange("E2:E").getValues()
var ackColumn = sheet.getRange("G2:G").getValues()
var resColumn = sheet.getRange("I2:I").getValues()
var timeToAckColumn = sheet.getRange(2,14,ackColumn.length,1)
var timeToResColumn = sheet.getRange(2,15,resColumn.length,1)
var yearAndWeekRange = sheet.getRange(2,16,createdColumn.length,2)
function calculateMetricsSheet1() {
calculateTimeDiff(createdColumn, ackColumn, timeToAckColumn)
calculateTimeDiff(ackColumn, resColumn, timeToResColumn)
calculateWeek(createdColumn, yearAndWeekRange)
}
example function implementation (they are basically very similar with minor differences):
function calculateWeek(createdColumn, yearAndWeekRange) {
var arrData = []
for(var i=0;i<createdColumn.length;i++) {
if(createdColumn[i][0].toString()=="") {
arrData.push(["",""])
continue
}
var createdDate = new Date(createdColumn[i][0])
var year = createdDate.getFullYear()
var week = Utilities.formatDate(createdDate, "GMT+1", "w")
arrData.push([year, week])
}
yearAndWeekRange.setValues(arrData)
}
the sheet2.gs is basically different column definitions, the functions called within calculateMetricsSheet2() are the same.
So what is the problem?
The script works perfectly fine for sheet2.gs, but for sheet1.gs it does collect proper data, calculates proper data, but the data does not appear in proper columns after Range.setValues() call.
No exceptions or errors appear in the console.
Documentation does not provide any kind of information what could be the problem.
I have really ran out of ideas what could be the cause of the issue.
Does anyone have any idea what is going on?
edit: It may be useful to put emphasis on the fact that each script runs function calling 3 other functions -> all of them end with Range.setValues({values}). And for one sheet all of them work, and for the other - none.
That's the reason I assume there is something wrong with the sheet itself, maybe some permissions/protection? But I couldn't find anything :(
edit2: I modified my code to iterate through the sheet 10 rows at a time, because I thought maybe when I get a whole column, something bad happens with data and breaks setValues() function.
Unfortunately - even if my code iterated 1 row at a time, it still did not work on sheet1, but worked on sheet2. So not a data problem.
The code you show always puts values in yearAndWeekRange which is always in the 'sheet1' sheet. To put the data into another sheet, you need to change the target range appropriately.
The dimensions of the range must match the dimensions of the array you put there. Use this pattern:
yearAndWeekRange.offset(0, 0, arrData.length, arrData[0].length).setValues(arrData);
I found out what is the problem.
Two scripts were pretty identical, even with naming of variables - ie ackColumn, resColumn etc.
Those were stored as a global variables, so even if I was running script1.gs, it used global variables from script2.gs, effectively writing proper data to wrong sheet.
separating global variables names fixed the issue.
Perhaps a rookie mistake, but I missed the fact, that if I have a variable defined outside any function, it becomes global and could be overwritten from other file

Google Sheets - Prevent users from pasting over cells with data validation

Is it possible to prevent a user from pasting in cells that have drop-down options based on data validation?
While it could be a convenience, when one cell (with data validation) is copied and pasted to another cell (with its own validation) it rewrites the data validation to match range being pasted, if that makes sense.
Alternatively, perhaps there is a script that will accept the info being pasted but rewrite that data validation to its original range??
This may be very tricky to pull off depending on your workbook usage, and is more complex than it sounds. In the old days, GAS had ScriptDB so developers would revert this kind of thing by recreating the "UNDO" functionality. When that was sunset, one recommendation was to have a duplicate sheet, and making sure that it always stays aligned with the user's active sheet - then, when the user messes up your data validations, you can just scan all data validation cells and revert those from the duplicate sheet (https://developers.google.com/apps-script/reference/spreadsheet/data-validation-builder). In any case, I concluded that Google Sheets was not made for this specific type of client-facing solutions (this is where Google Forms is used), so if you cannot make it work using "Protected Sheets and Ranges" you will probably end up implementing a hack, as follows.
Here was my hack approach; I was unable to prevent this from happening, however, I was able to auto-revert cells to proper format by using onEdit(), and reverting all damaged named cells after each user edit. The idea is to define rules inside names of named ranges.
(1) You can create a named range for every cell that you want to protect. The name of the named range must encapsulate your data validation rules. For example, if you have a dropdown cell at [B28 on a sheet with ID "1380712296"], that feeds from [range A11-A14 (4 items in the dropdown) on sheet with ID "936278947"], you can name the dropdown cell as follows:
z_1380712296_r?936278947?A11.A14_B28
Or, in a generic form:
z_ DROPDOWN-SHEET-ID _ DATA-VALIDATION-TYPE ? DATA-VALIDATION-SOURCE-SHEET-ID ? DATA-VALIDATION-START-CELL . DATA-VALIDATION-END-CELL _ DROPDOWN-CELL-RANGE-IN-A1-FORMAT
(2) Write a trigger to execute on every user edit, as follows:
function onEdit(e) {
try {
autocorrectFormat(e.range);
}
catch (e) {}
}
function autocorrectFormat(modifiedRange) {
// Get named ranges on active sheet
var sheetNamedRanges = SpreadsheetApp.getActiveSheet().getNamedRanges();
// Fix active cells that intersect
for (var i in sheetNamedRanges) {
// we only go through the named ranges that begin with z_ since others may be declared by users
if (sheetNamedRanges[i].getName().substring(0,2) == "z_") {
// This rangesIntersect function below is necessary because onEdit's e.range is often wrong when pasting groups of cells
// so we want to check if the edited range intersects with a named range's range rather than equals exactly
if (rangesIntersect(sheetNamedRanges[i].getRange(), modifiedRange)) {
// Here parse the information on the named range's name and fix the potentially damaged cell using the data
// validation rules pulled from the named range's name
var currentCellInformation = [sheetNamedRanges[i].getRange(),sheetNamedRanges[i].getName().split("_")];
// Parsing
var part_1 = currentCellInformation[1][2].charAt(0);
var part_2 = currentCellInformation[1][2].split("?");
// Function to rebuild the dropdown cell
fixRange(...);
}
}
}
return;
}
// https://stackoverflow.com/a/36365775/7053599
function rangesIntersect(R1, R2) {
return (R1.getLastRow() >= R2.getRow()) && (R2.getLastRow() >= R1.getRow()) && (R1.getLastColumn() >= R2.getColumn()) && (R2.getLastColumn() >= R1.getColumn());
}
As you noticed, pasting overwrites the former data validation rules. You may also want to assess the effects of Ctrl+X, Drag+Drop, and Ctrl+\ and their effects on named ranges, since, for example, Ctrl+X moves the named range as well, and Ctrl+\ never executes onEdit().
I included the code for you to get familiar with the kinds functions you would be using - you may also be using DataValidationBuilder in your fixRange() function where you rebuild the dropdown cell.
In response to "Alternatively, perhaps there is a script that will accept the info being pasted but rewrite that data validation to its original range??":
This is a good idea, it had occurred to me as well, but it is not possible to intercept information being pasted with GAS. The closest we have is onEdit(). This is why I mentioned above that I could not prevent it, but rather attempt to auto-revert damaged cells.

Keeping value after reference cell has been changed

I need cell B3 to reference B1 while blank.
Once you put something in B1 it'll keep that value forever, even once B1 get's changed to something else.
This is my situation:
Basically I have a sheet that is fed by a Google form and each submission needs three key reference numbers each kept in columns a,b,c
A = Unit Number/Individuals name (There may be duplicates down the sheet as this is per submission)
B = Work Order (Imputed by me after actual work on unit has been done)
C = Cry Number/Reference number (Automatically generated per submission; no duplicates)
I then have a frozen row at the top which contains a search bar that you can search for the cry number (A1)(Which has a Data Validation set to column C so that you can only search valid cry numbers) and then a cell to add a W/O to that Cry Number (B1)
In column B3:1000, I have this formula copied down:
B3=if(isblank($C3),"",if($A$1=$C3, SUBSTITUTE($B$1,"",$B$1),""))
...which makes it so that if you select say "CN-168" (A valid cry number) and in 'B1' type "W1134" that work order number will be assigned.
Now I need that work order to stay there regardless of when 'A1' changes so that you can do the process over again on another submission.
Is it possible to do with formulas? If not, then a Google Script?
Here is a template of what I'm dealing with but not to the same scale as my Data Base
Its not possible with formulas but easily done with apps script. look at the onEdit trigger and the documentation for SpreadsheetApp to setValues to the appropiate ranges.
If you want to be 100% complete you also need a time trigger (say every 10 minutes) to check that a row wasnt missed. It can be missed during apps script errors/outages or when the sheet is changed from outside the sheets webpage/app (For example using the http spreadsheet api)

Update script cell references when columns are moved

We're migrating a lot of our business logic to scripts behind the scenes, but I'm worried that they'll be much more fragile when columns move.
On Sheet Updates Automagically
For example, If I have a formula on a spreadsheet like this:
=If(A1=5,"Yes","No")
And then I Insert 1 Column Left of A, the formula will be automatically updated like this:
=If(B1=5,"Yes","No")
Apps scripts doesn't update
For example, if I have the formula in the script section:
function myFunction() {
var value = SpreadsheetApp.getActiveSheet().getRange("A1").getValue();
var output = (value == 5) ? 'Yes' : 'No';
Logger.log(output);
}
It will not update when the sheet changes.
Q: How can I get stable references in the code behind for columns that could potentially move?
This is a general problem when hardcoding strings or numbers in code.
In general the javascript parser can't tell which strings might be used on a sheet function call. Its sometimes not trivial to solve.
Two approaches are:
If the columns/cells/ranges are known beforehand, use named ranges:
Define a named range and use NamedRange in code. Use the range to directly write to it or query its row/column position.
Another for column based ranges like yours is that your code does this naming manually by using the column header as the column names. Code uses those names and reads the header to build the mapping.

Is it possible to define a new function in Google-docs spreadsheet?

Is it possible to define a function in Google Spreadsheets that can be used in any cell?
It would be helpful if I could define and use functions that refer to other cells in the same way that I can use native functions, e.g. by entering =myfunction(C1, C2, C3)
Yes - there's a tutorial. Just use javascript functions by name in your spreadsheet. Define them using Tools > Script editor.
Watch out for name restrictions; I was confused by the behavior of functions that I created with names like "function x10() {}" not being found. Renaming to a longer name fixed it. There are probably documented rules for what isn't allowed, but I don't know where they are.
I am a "newbee". But is is my experience that you can only access a "cell"
via the "range" object. You must define the range as a single cell.
For example "A1:A1", will give you access the the cell at "A1".
A RANGE is an object associated to a "SHEET".
A SHEET is an object associated to a "SPREADSHEET".
Here is some sample code to access cell A1 in the current active sheet:
var cell_A1 = SpreadsheetApp.getActiveSheet().getRange("A1:A1");
From here you can pass the object like any other parameter.
myFunction(cell_A1);
The receiving function must "know" that it is dealing with a "range".
It can only access its values by calling "methods" associated to the
"range" object.
Be careful! A "range" can consist of more than one cell. Your called
function should test to see that it is working with a single cell.
If you pass a range of more than one cell, your function might not
act in the way you expect.
The two methods of a range object: "getNumRows()" and "getNumColumns()"
returns the numbers of Rows and Columns in a range object.
In general, if you use methods that are limited to changing or accessing
a single cell, and operate on a larger range set, the function will only be
performed on the upper-left cell member. But be careful. While you
might assume a method will only change a single cell, it may actually
affect all cells in the range. Read the documentation closely.
There is another method to obtain a range of a single cell. Its instruction
looks like this:
var cell_B2 = SpreadsheetApp.getActiveSheet().getRange(2, 2, 1, 1).
The first two parameters tell the "getRange" function the location of the
cell (in row, column format). The second two parameters define the number of
"rows" and "columns" to associated with the range. By setting them both to
"1", you access a single cell.
Hope this helps.