Setting a "subrange" list of values in a dropdown (data validation), based on selected value from a range in Google Sheets - validation

glorious Internet. I seek your help.
I've imported a spreadsheet from OpenOffice, and the "subrange" dropdown list (from data validation) is broken.
Basically, Column B = "Category", and Column C = "Subcategory" -- Column B data validation works properly, populates the standard Category dropdown list (it's just a Range within the same sheet, in the example below D2:E2). But then, based on the Category value, Column C should update a dropdown list of subcategories.
i.e.
row/col
D
E
2
Fruit
Vegetable
3
Apple
Carrot
4
Banana
Onion
B7 = dropdown list "Fruit / Vegetable"
I pick fruit
C7 should then update to a dropdown list "Apple / Banana"
I feel like a potential avenue might be using a Script (was looking to this for inspiration: Getting a range from a range in Google Apps scripting)
but never having used Google Scripting before, I need some basic help (like, even if I have a properly working function CreateSubranges defined in macros.gs -- but how do I get it to trigger every time a Fruit is selected?)
Any advice you have is greatly appreciated! Thank you :-)

If you don't need anything too fancy and it is for just one cell, then this formula will do. Just change the ranges to the correct sheet name/range and it will work for you. The formula gets the value selected from sheet 1 and checks if it matches Fruits or Vegetable. If it matches either of those then B3:B or C3:C is selected. I then constrain the array to only go as far as the count in B3:B...assuming that your two columns have the same number of elements. I then add this range to the data validation for C2 on Sheet one.
=Array_Constrain(ArrayFormula(if(Sheet1!B2="Fruit",B3:B,if(Sheet1!B2="Vegetable",C3:C,""))),countif(B3:B,"<>"),1)
Demo of the category and sub category
Showing each separate tab and what happens when something is selected.
If you are needing a dynamic subcategory for each row then the following onEdit script will set the data validation rules based on what was selected. This does require a small setup of creating the data validation for fruit and vegetable on Sheet2 D2 and E2.
function onEdit(e) {
var ss = e.source;
var value = e.value;
var activeSheet = ss.getSheetName();
var range = e.range;
var currentRow = range.getRow();
var currentColumn = range.getColumn();
if(activeSheet == 'Sheet1' && currentRow > 1 && currentColumn == 2 && value == 'Fruit') {
//IF THE ROW IS > 1 AND THE COLUMN = 2 AND FRUIT WAS SELECTED, THEN IT WILL COPY THE VALIDATION RULES FROM
//SHEET2 D2.
var fruitRule = ss.getSheetByName('Sheet2').getRange(2, 4).getDataValidation().copy();
range.offset(0,1).setDataValidation(fruitRule);
return;
} else if(activeSheet == 'Sheet1' && currentRow > 1 && currentColumn == 2 && value == 'Vegetable') {
//IF THE ROW IS > 1 AND THE COLUMN = 2 AND VEGETABLE WAS SELECTED, THEN IT WILL COPY THE VALIDATION RULES FROM
//SHEET2 E2.
var vegRule = ss.getSheetByName('Sheet2').getRange(2, 5).getDataValidation().copy();
range.offset(0,1).setDataValidation(vegRule);
return;
} else if(activeSheet == 'Sheet1' && currentRow > 1 && currentColumn == 2 && value == null) {
//IF THE ROW IS > 1 AND THE COLUMN = 2 AND VALUE WAS DELETED, THEN IT WILL CLEAR THE VALIDATION RULES
//FROM THE ADJACENT CELL.
range.offset(0,1).clearDataValidations();
}
}
Hope this helps! In the future, it would be much easier to show an example with a demo sheet provided.

Related

Google App Script: Remove blank rows from range selection for sorting

I want to sort real-time when a number is calculated in a "Total" column, which is a sum based on other cells, inputted by the user. The sort should be descending and I did achieve this functionality using the following:
function onEdit(event){
var sheet = event.source.getActiveSheet();
var range = sheet.getDataRange();
var columnToSortBy = 6;
range.sort( { column : columnToSortBy, ascending: false } );
}
It's short and sweet, however empty cells in the total column which contain the following formula, blanking itself if the sum result is a zero, otherwise printing the result:
=IF(SUM(C2:E2)=0,"",SUM(C2:E2))
It causes these rows with an invisible formula to be included in the range selection and upon descending sort, they get slapped up top for some reason. I want these blank rows either sorted to the bottom, or in an ideal scenario removed from the range itself (Without deleting them and the formula they contain from the sheet) prior to sorting.
Or maybe some better way which doesn't require me dragging a formula across an entire column of mostly empty rows. I've currently resorted to adding the formula manually one by one as new entries come in, but I'd rather avoid this.
EDIT: Upon request find below a screenshot of the sheet. As per below image, the 6th column of total points needs to be sorted descending, with winner on top. This should have a pre-pasted formula running lengthwise which sums up the preceding columns for each participant.
The column preceding it (Points for Tiers) is automatically calculated by multiplying the "Tiers" column by 10 to get final points. This column could be eliminated and everything shifted once left, but it's nice to maintain a visual of the actual points awarded. User input is entered in the 3 white columns.
You want to sort the sheet by the column "F" as the descending order.
You want to sort the sheet by ignoring the empty cells in the column "F".
You want to move the empty rows to the bottom of row.
You don't want to change the formulas at the column "F".
You want to achieve this using Google Apps Script.
If my understanding is correct, how about this answer?
Issue and workaround:
In the current stage, when the empty cells are scattered at the column "F", I think that the built-in method of "sort" of Class Range cannot be directly used. The empty cells are moved to the top of row like your issue. So in this answer, I would like to propose to use the sort method of JavaScript for this situation.
Modified script:
In order to run this function, please edit a cell.
function onEdit(event){
const columnToSortBy = 6; // Column "F"
const headerRow = 1; // 1st header is the header row.
const sheet = event.source.getActiveSheet();
const values = sheet.getRange(1 + headerRow, 1, sheet.getLastRow() - headerRow, sheet.getLastColumn())
.getValues()
.sort((a, b) => a[columnToSortBy - 1] > b[columnToSortBy - 1] ? -1 : 1)
.reduce((o, e) => {
o.a.push(e.splice(0, columnToSortBy - 1));
e.splice(0, 1);
if (e.length > 0) o.b.push(e);
return o;
}, {a: [], b: []});
sheet.getRange(1 + headerRow, 1, values.a.length, values.a[0].length).setValues(values.a);
if (values.b.length > 0) {
sheet.getRange(1 + headerRow, columnToSortBy + 1, values.b.length, values.b[0].length).setValues(values.b);
}
}
In this sample script, it supposes that the header row is the 1st row. If in your situation, no header row is used, please modify to const headerRow = 0;.
From your question, I couldn't understand about the columns except for the column "F". So in this sample script, all columns in the data range except for the column "F" is replaced by sorting. Please be careful this.
Note:
Please use this sample script with enabling V8.
References:
sort(sortSpecObj)
sort()
Added:
You want to sort the sheet by the column "F" as the descending order.
You want to sort the sheet by ignoring the empty cells in the column "F".
You want to move the empty rows to the bottom of row.
In your situation, there are the values in the column "A" to "F".
The formulas are included in not only the column "F", but also other columns.
You don't want to change the formulas.
You want to achieve this using Google Apps Script.
From your replying and updated question, I could understand like above. Try this sample script:
Sample script:
function onEdit(event){
const columnToSortBy = 6; // Column "F"
const headerRow = 1; // 1st header is the header row.
const sheet = event.source.getActiveSheet();
const range = sheet.getRange(1 + headerRow, 1, sheet.getLastRow() - headerRow, 6);
const formulas = range.getFormulas();
const values = range.getValues().sort((a, b) => a[columnToSortBy - 1] > b[columnToSortBy - 1] ? -1 : 1);
range.setValues(values.map((r, i) => r.map((c, j) => formulas[i][j] || c)));
}
A much simpler way to fix this is to just change
=IF(SUM(C2:E2)=0,"",SUM(C2:E2))
to
=IF(SUM(C2:E2)=0,,SUM(C2:E2))
The cells that are made blank when the sum is zero will then be treated as truly empty and they will be excluded from sort, so only cells with content will appear sorted at the top of the sheet.
Why your original formula doesn't work that way is because using "" actually causes the cell contain content so it's not treated as a blank cell anymore. You can test this by entering ISBLANK(F1) into another cell and check the difference between the two formulas.

Parse through rotation choices to produce class schedules

I have a Sheets project with 6 worksheets. I'm using student choices to generate a series of 4 rotations in which students can visit classes.
Form Responses 1 lists names in Column D; Columns I-N list a set of six possible rotation choices
6Rot1, 6Rot2, 6Rot3, and 6Rot4 correspond to each of the four possible rotations
mainRoster6 is a master roster produced to reflect names and rotation choices
I'm trying to parse through Form Responses 1, with the following conditions:
Start by looking at Column I and see if there are "open spots" (<47) in the corresponding column of 6Rot1. If so, add the name to that column. Otherwise, check 6Rot2 and try to perform the same operation. Etc., until all 4 rotations have been checked.
If there are no open spots in either of the 4 rotations, then check Column J and try to fit the student in the correct class's name, etc.
The intention is to assign the student's top unique choices to 4 rotations. If a student has already chosen a class for one rotation, then that class should not appear again in a different rotation.
I'm trying to solve two bugs:
If a class has already been added for a student for a rotation, he should not have the same class in a newer rotation (I tried to solve this in lines 62-67 but it didn't work). In the example sheet, for the student in row 3, once the student has been placed in "Spanish I" for rotation 2, it should skip his third choice (a repeated "Spanish I" and proceed trying to place him in "Coding" for his third rotation.
The program should assign in rotations in the order of preference (first, then second, then third, etc.). Right now, if a rotation is full for a specific class, it simply ignores that class (even if the next rotation may be available) and moves on to the next choice. In the example sheet, the first student has "coding" as third choice. Although that class is not available in the 3rd rotation, it's available in the 4th rotation. The program should try to place coding in the 4th rotation and then look at the fourth choice and try to place the student into any available rotation (in this case, rotation 3 would be available).
This is what I have currently:
function myFunction() {
var mainSheet = SpreadsheetApp.openById('1ki0Cya3IWNwdLIBe0fR1vwfz_ansmfa52NilmdFBrd0');
var firstRotation = mainSheet.getSheetByName('6Rot1');
var secondRotation = mainSheet.getSheetByName('6Rot2');
var thirdRotation = mainSheet.getSheetByName('6Rot3');
var fourthRotation = mainSheet.getSheetByName('6Rot4');
var mainRoster = mainSheet.getSheetByName('mainRoster6');
var responsesSheet = mainSheet.getSheetByName('Form Responses 1');
var destinationCell;
var column;
var columnValues;
var columnContainsStudent;
var currentSheet;
var mainRosterLastRow;
var studentName;
var compassTeacher;
var classChoice;
var rotationCounter;
var choicesCounter;
var successCounter;
var rotationOneSuccess;
var rotationTwoSuccess;
var rotationThreeSuccess;
var rotationFourSuccess;
var responsesLastRow = responsesSheet.getLastRow();
var currentRotationsArray;
// var y;
var z;
for (z = 2; z < responsesLastRow + 1; z++) {
studentName = responsesSheet.getRange(z, 4).getValues();
mainRoster.getRange(z, 2).setValue(studentName);
compassTeacher = responsesSheet.getRange(z, 6).getValues();
mainRoster.getRange(z, 1).setValue(compassTeacher);
choicesCounter = 1;
successCounter = 0;
rotationCounter = 1;
rotationOneSuccess = false;
rotationTwoSuccess = false;
rotationThreeSuccess = false;
rotationFourSuccess = false;
while (choicesCounter < 7) {
if (successCounter > 4) {
break;
}
classChoice = responsesSheet.getRange(z, 8 + choicesCounter).getValues();
//test for includes in previous choices
var currentRotationsArray = mainRoster.getRange(z, 4, 1, 4).getValues();
// for (y = 0; y < 4; y++) {
// if (currentRotationsArray[0][y] == classChoice) {
// choicesCounter++;
// break;
// }
// }
while (successCounter < 4) {
if (rotationOneSuccess == false) {
currentSheet = firstRotation;
} else if (rotationTwoSuccess == false) {
currentSheet = secondRotation;
} else if (rotationThreeSuccess == false) {
currentSheet = thirdRotation;
} else {
currentSheet = fourthRotation;
}
// Set column number and get values as an array, depending on class choice
if (classChoice == "Guitar 7") {
column = 1;
columnValues = currentSheet.getRange("A1:A").getValues();
} else if (classChoice == "Communications - Video Production") {
column = 2;
columnValues = currentSheet.getRange("B1:B").getValues();
} else if (classChoice == "Communications - Journalism") {
column = 3;
columnValues = currentSheet.getRange("C1:C").getValues();
} else if (classChoice == "Coding") {
column = 4;
columnValues = currentSheet.getRange("D1:D").getValues();
} else if (classChoice == "Art 7") {
column = 5;
columnValues = currentSheet.getRange("E1:E").getValues();
} else if (classChoice == "Latin I") {
column = 6;
columnValues = currentSheet.getRange("F1:F").getValues();
} else if (classChoice == "Spanish I") {
column = 7;
columnValues = currentSheet.getRange("G1:G").getValues();
} else if (classChoice == "French I") {
column = 8;
columnValues = currentSheet.getRange("H1:H").getValues();
} else {
column = 9;
columnValues = currentSheet.getRange("I1:I").getValues();
}
// Find column's current last row
var columnLength = columnValues.filter(String).length;
//Add the student to the class's rotation, if the student isn't already there and there's >46 people already in
if (columnLength < 48) {
for(var n in columnValues){
if(columnValues[n][0] == studentName){
columnContainsStudent = true;
rotationCounter++;
break;
}
}
if (columnContainsStudent != true) {
destinationCell = currentSheet.getRange(columnLength + 1, column);
destinationCell.setValue(studentName);
if (currentSheet == firstRotation) {
rotationOneSuccess = true;
} else if (currentSheet == secondRotation) {
rotationTwoSuccess = true;
} else if (currentSheet == thirdRotation) {
rotationThreeSuccess = true;
} else {
rotationFourSuccess = true;
}
mainRoster.getRange(z, rotationCounter + 3).setValue(classChoice);
successCounter++;
rotationCounter++;
break;
}
rotationCounter++;
} else {
break
}
rotationCounter++;
}
choicesCounter++;
}
}
}
If you look at the linked sample spreadsheet, the logic is as follows:
We're looking at Form Responses 1 row by row, with the intention of adding 4 "choices" for that person in 4 rotations (6Rot1, 6Rot2, etc.). Columns I-N list 6 possible choices, but ideally I want to put the student in his top 4 choices.
I want the program to first look at Form Responses 1 Column I. In the sample sheet it's "Guitar 7". Then the program should look at 6Rot1 and see if the column for "Guitar 7" (Column A) has any open spaces (i.e. if it has less than 48 rows already filled). In the sample sheet, there are 36 "Placeholder Names" so it adds the first student to the first blank row in Column A (row 37). Once the name has been added to one of the rotations, mainRoster6 reflects that same information (column D in this case reflects "Guitar 7", which contains the same information for 6Rot1).
The program should next look back at Form Responses 1 and consider the student's next choice: in this case it would be the "second choice" (column J, "French I"). I want to look at the first possible rotation (6Rot1, 6Rot2, etc) that doesn't include the student AND has less than 48 people already in it, so that we can put his choice there. In this case6Rot2had already 39 names in it, so it accepted the student into row 40 of column H. After adding the student name in6Rot2, the program adds "French I" to Column E ofmainRoster6` ("Rotation 2")
It next looks back at Form Responses 1 and consider the "third choice" in column K, and repeats the process.
If a class is already "full" (has more than 47 names listed already in the column), then the program should try to find the first available rotation. For example, the student in Form Responses 1 has "Coding" as his third choice. 6Rot3 already has no space for column D ("Coding"), so it should first try to add the student's name ("Billy Kid") in Column D of 6Rot4 and then try to put in the student's name in column I of 6Rot3 for "German I".
What I'm trying to do with the code is parsing row by row of Form Responses 1 and putting the first 4 possible "choices" into the 4 possible rotations, and having mainRoster6 reflect that same information as a kind of abbreviated master list.
If a student lists two or more similar choices in Form Responses 1 columns I-N, then the program should ignore the repeated values. For example, in row 3 of Form Responses 1 it should overlook the second "Spanish" in K3 and move on to the next choice for that student.
If there are no more possible choices or rotations that can account for that choice, then the space can be left blank (see for example Form Responses 1 row 4: based on the student's choices and available spots, we were only able to honor the first three choices ("French" was already filled completely in 6Rot4; "Latin I" and "Art 7" were already in the schedule).
Under these conditions, I would have expected mainRoster6 to have this intended output:

Dropdown list with conditions on other dropdown list

How can I make a drop-down list from several lists with a condition from another drop-down list?
Using my image above, let's say I on the first drop-down list (this one is a simple one) select "Furniture"... I would like the second drop-down list to only show the furniture. Same thing with the third drop-down list, would like only the color of my second choice to be shown there.
Did try to place on the criteria "Custom formula" in the "Data validation" one of this two formulas but does not work...
=FILTER(Object,Type = E2)
or
=QUERY(A:C,"SELECT B WHERE A='"&E2&"' ", 0)
Did read in some other topic here that it was not possible with formulas and I could not find an app script for it. How can i place conditional rules and make only appear the values i want on the drop-down menu instead all of them ? I think it has something to do with the "withCriteria(criteria, args)" however i am not understanding how to apply it.
About the list ... it will be compose maybe with 2k lines (each line 3 columns). First column will only have (maybe) 6 or 7 different values. Second about 70 or 80 and third all different. The order will be random cause new values could be added and i can be adding a new Furniture or Animal ...
This is the code i have now
function onEdit(e) {
var range = e.range;
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
if ( range.getRow() > 1) {
if ( range.getColumn() == 5) {
var cell_Range = ss.getRange( range.getRow(), range.getColumn() + 1);
var cell = cell_Range.getCell( 1, 1);
var rangeV = SpreadsheetApp.getActive().getRange('B2:B13');
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(rangeV).build();
cell.setDataValidation(rule);
}
else if ( range.getColumn() == 6 ) {
var cell_Range = ss.getRange( range.getRow(), range.getColumn() + 1);
var cell = cell_Range.getCell( 1, 1);
var rangeV = SpreadsheetApp.getActive().getRange('C2:C13');
var rule = SpreadsheetApp.newDataValidation().requireValueInRange(rangeV).build();
cell.setDataValidation(rule);
}
}
}
Example sheet at https://docs.google.com/spreadsheets/d/1aLpYd8fC0jpwvQOPVTj_yvY7DVKeFFnPvpJSF27if6w/edit?usp=sharing
Thanks in advance
Solution:
Use Datavalidation requireValueInList
Flow:
Get All options A1:C as a array
Filter A(Col1) if edited value in E(Col5) is present in it
Retrieve corresponding Col2(B) as options and build data validation based on it
Offset the edited range by 1 column and set DataValidation
Sample Script:
function onEdit(e) {
const SETTINGS = {
//Edited Column : Column to Check(First col in optionsDataRange is considered 1)
5: 1,
6: 2,
};
var editedRange = e.range,
editedSheet = editedRange.getSheet(),
val = e.value,
col = editedRange.columnStart,
row = editedRange.rowStart;
/*Exit clause(s)*/
if (
Object.keys(SETTINGS).indexOf(col.toString()) === -1 || //If edited col is not in settings
editedSheet.getName() !== 'Sheet1' ||
row > 5
)
return;
var optionsDataRange = editedSheet
.getRange(1, 1, editedSheet.getLastRow(), 3)
.getValues();
/*Only get options where val is present in optionsDataRange*/
var options = optionsDataRange
.map(function(e) {
return e[SETTINGS[col] - 1] == val ? e[SETTINGS[col]] : null;
})
.filter(function(e, i, a) {
return e !== null && a.indexOf(e) === i;
});
var dv = SpreadsheetApp.newDataValidation()
.requireValueInList(options)
.build();
editedRange.offset(0, 1).setDataValidation(dv);
}
References:
Range
Array

Why is LINQ query to DataTable not working?

I have a DataTable, dt, that contains the following:
As you can see, there are two sets of files here: baseline (columns 1-3) and target (columns 4-6). So in this case I have 4 baseline files, m4, m5, m1, and m3 (m1 in row 5 is copied from row 3) and one target file, m1. The problem is that the information for the baseline file m1.txt is duplicated, so I'm trying to remove it using the following LINQ statement:
var extraneousRows = dt.Rows.Cast<DataRow>().
Where(
row => row["BASELINE_FOLDER"] == baselineSubfolder
&& row["BASELINE_FILE"] == baselineFilename
&& row["BASELINE_CHECKSUM"] == baselineChecksum
&& row["STATUS"] == "remove"
).ToArray();
foreach (DataRow dr in extraneousRows)
{
dt.Rows.Remove(dr);
}
This should remove the 3rd row from the DataTable but it doesn't. The code works fine if I omit the && row["STATUS"] == "remove" line, so I know it's functional. And the values for baselineSubfolder, baselineFilename, and baselineChecksum are correct, so that's not the problem.
But for some reason, when I include that line about the status column, it doesn't detect that that row is in the DataTable, even though it clearly is according to the DataSet Visualizer (photo above). So it's never entering the foreach loop and not removing the necessary files. Why???
I maybe should mention that the baseline file information (first 4 rows) are being retrieved from a database, whereas the target file fields are being generated according to user input. I don't see how it would matter where the information is coming from, though, since I'm querying the DataTable directly...
UPDATE
Ok, after following the suggestions of idipous and Jamie Keeling, I've determined that the problem had to do with the foreach loop, which was never being populated. Since this query should only ever return a single row, I eliminated the loop altogether. My revised code looks like this:
var extraneousRows = dt.Rows.Cast<DataRow>().
Where(
row => row["BASELINE_FOLDER"] == baselineSubfolder
&& row["BASELINE_FILE"] == baselineFilename
&& row["BASELINE_CHECKSUM"] == baselineChecksum
&& row["STATUS"] == "remove"
).SingleOrDefault();
dt.Rows.Remove(extraneousRows);
For whatever reason, extraneousRows remains null and that last line is generating a runtime error: IndexOutOfRangeException: The given DataRow is not in the current DataRowCollection
Why isn't this working?
It actually turns out that the problem was that I needed to cast the column values to strings. The solution was incredibly simple: just add a .ToString() after the column names and viola! The following code worked like a charm:
var extraneousRows = dt.Rows.Cast<DataRow>().
Where(
row => row["BASELINE_FOLDER"].ToString() == baselineSubfolder
&& row["BASELINE_FILE"].ToString() == baselineFilename
&& row["BASELINE_CHECKSUM"].ToString() == baselineChecksum
&& row["STATUS"].ToString() == "remove"
).SingleOrDefault();
dt.Rows.Remove(extraneousRows);
I also found a non-LINQ way to do this, by iterating through all the rows of the DataTable. It's not the most efficient, but it works:
for (int z = 0; z < dt.Rows.Count; z++)
{
if ((dt.Rows[z]["BASELINE_FOLDER"].ToString() == baselineSubfolder)
&& (dt.Rows[z]["BASELINE_FILE"].ToString() == baselineFilename)
&& (dt.Rows[z]["BASELINE_CHECKSUM"].ToString() == baselineChecksum)
&& (dt.Rows[z]["STATUS"].ToString() == "remove"))
{
dt.Rows[z].Delete();
}
}
dt.AcceptChanges();

linq where according to selected item?

I am new to EF and LINQ, hoping to get some answers here.
I am trying to search from list with where condition that is according to the selecteditem from a combobox.
The combobox has 15 items(all bit datatypes) but let me just narrow it down to 2 for examples sake. items are( pro bono, civil)
Now I have a list called listOfAllNeutrals (object name is Neutral with properties such as pro bono (bit) and civil(bit), and I want to filter it using the where condition according to the selected item.
so if selected item = pro bono, the linq would look like this
var result = from n in listOfAllNeutrals
where n.probono==true
select n;
but my prob is how do I tell that the n.property should be according to the selecteditem?
like this:
var result = from n in listOfAllNeutrals
where getpropertyName==true
select n;
is there a simpler way, I don't want to use If conditions if possible.
You need to use if's or switch inside the getpropertyName(Neutral objNeutral) function to Map the selected item to the property you want to evaluate and evaluate it. You need to map object => property in some way.
Try
var result = from n in listOfAllNeutrals
where (selectedItem == proBono && n.probono == true)
|| (selectedItem == civil && n.civil == true)
select n;

Resources