I'm trying to make a form in google sheets so that if on column E there's a name (or any non-null value), it checks that the rest of the fields have been filled on columns F, G, H, etc... for this I've made two arrays, one with the values of the column E and the other with the values on the row that is being checked. The problem I'm encountering is with the null condition in the second for iteration
var range = R13A.getRange("E7:E15");
var values = range.getValues();
var i = [];
for (var y = 0; y < values.length; y++)
{
if (values[y]!="" && values[y].toString().trim()!="") //here it checks if a name is there
{
i.push(y);
var valRow = Number(i)+Number(range.getRow());
var range1 = R13A.getRange("G"+valRow+":T"+valRow);
var values1 = range1.getValues();
for (var x=0;x<values1.length;x++)
{
SpreadsheetApp.getUi().alert(values1[x]); // this part is just to check what values there are on the cells, it always returns ",,,,,,,," which means the cells actually are empty
if (values1[x]=="") // And here it should check if on the same row there are any blank cells. **THIS IS THE CONDITION THAT ISN'T BEING MET, WHETHER THE CELL HAS SOMETHING WRITTEN ON IT OR NOT**
{
validator = validator+" "+R13A.getRange(String.fromCharCode(7+y)+5).getValue(); //this saves the field names that need to be filled
}
}
}
}
If I check for the values of the cells separately rather than in an array, it does work, which I guess means the problem I have is with my array of data.
Thanks in advance
I tried to check what is the value of values1[x] and upon testing, it seems it returns an array.
since getValues do return a 2D array, you need to access values1[x] as values1[0][x] instead since we are always sure that there is only a single row and traverse the columns.
Since your validator variable as of now doesn't work, I did simplify it with printing we have blank cells instead.
this is the code I tested.
for(var y = 0; y < values.length; y++){
if (values[y] && values[y].toString().trim()) {
i.push(y);
var valRow = Number(y) + Number(range.getRow()); // you used Number(i) here but it should be Number(y)
var range1 = R13A.getRange("G" + valRow + ":T" + valRow);
var values1 = range1.getValues();
for (var x = 0; x < values1[0].length; x++) { // changed values1 to values1[0] instead
if (!values1[0][x] && !values1[0][x].toString().trim()) { // changed values1 to values1[0] instead
Logger.log("we have blank cells");
}
}
}
}
As you can see, I simplified some checking on my part, feel free to use or modify it on yours.
Here is the sample sheet:
Where cells painted with red are blank (8 in total)
and here is the output of the code:
As we can see in the output, we were able to count them properly.
If you have any more questions, feel free to ask me.
Related
This works perfectly if none of the data-validated cells raise an error on the sheet being written to. However, once the script reaches a cell where the copied value doesn't follow the data validation rules, it returns an error and stops executing. How can I ignore this error, continue to write into the cell, and continue running the script?
function addNew() {
Week = "2022-01-03"
x = 4
y = 17
for (var i=0; i<21; i++)
{
var readRange = SpreadsheetApp.getActive().getRange(`Versionality!I${x}:K${y}`)
var readValues = readRange.getValues();
var writeRange = SpreadsheetApp.openById("------");
var writeValues = writeRange.getRange(`${Week}!H${x}:J${y}`)
writeValues.setValues(readValues);
x += 17
y += 17
}
}
Here is an example of how you can “disable/enable” data validation to copy data:
Sample code:
function myFunction(){
// IMPORTANT: Make sure source and target dimensions are the same.
var sourceRange = SpreadsheetApp.getActive().getRange("source!B11:D11"); //Get source range of cells to be copied
var sourceRangeDVs = sourceRange.getDataValidations(); // cache the data validations currently applied to the sourceRange
sourceRange.clearDataValidations(); //clear validations on the source range before getting source values
var sourceValues = rangeSource.getValues(); //getting source values
//Next, get target range where the data will be set.
//Note: if you have to open a new file and you are handling several ranges to be copied in a loop,
// you may want to cache the target file on a variable to avoid opening every iteration of the loop
var targetRange = SpreadsheetApp.getActive().getRange("target!E6:G6"); //Get target range of cells for the values to be set
targetRange.setValues(sourceValues); //Set values copied from source in the target range
sourceRange.setDataValidations(sourceRangeDVs); //Set Data Validations cached back to source range
}
Please note that the spreadsheet names and ranges set on the sample code above are merely to exemplify, please modify it accordingly.
Let me propose this. You are doing a lot of getValues()/setValues() which can cause a performance issue. What I like to do is use only one getValues() and then we can extract what we need from the full array and write our portions of it as needed.
Also you were repeatedly opening the same spreadsheet by id.
I didn't test it because your date structure would be too hard to set up. But I'm pretty confident it will work.
function addNew() {
let Week = "2022-01-03";
let x = 3; // array index is 1 less than row
let y = 17;
let spread = SpreadsheetApp.getActiveSpreadsheet();
let readSheet = spread.getSheetByName("Versionality");
let readValues = readSheet.getDataRange().getValues();
let writeSheet = SpreadsheetApp.openById("------").getSheetByName(Week);
for (let i=0; i<21; i++) {
let subArray = [];
let j=x;
while( j < y ) {
subArray.push(readValues[j].slice(7,10)); // columns I to K
j++;
}
writeSheet.getRange(x+1,8,14,3).setValues(subArray); // columns H to J
x += 17;
y += 17;
}
}
My goal
I'm trying to link rows in two different sheets in the same spreadsheet according to a value in a cell. The basic idea is that if a value in column B in Sheet1 has a matched value in column B in Sheet2, an hyperlink should be added to the cell with the matched value in Sheet1 linking to the whole row of the matched value in the Sheet2.
What I did
As you can see from the code below, it looks for the matches, if it's found, it edits the matched values to add the hyperlinks. I don't only want to push the "linked" matched values into the new array, I also want the non-matched values without the link. The idea is that the link will be added if a match is found, otherwise the value will still be added, just without the link.
function linkToContacts(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var Sheet1 = ss.getSheetByName("Sheet1");
var Sheet2 = ss.getSheetByName("Sheet2");
var Sheet2ID = Sheet2.getSheetId();
var arrSheet1 = Sheet1.getRange(4,2,Sheet1.getLastRow()-3).getValues(); // -3 because I have 3 row as headers before the data, which start in row 4 col B
var arrSheet2 = Sheet2.getRange(4,2,Sheet2.getLastRow()-3).getValues(); // -3 because I have 3 row as headers before the data, which start in row 4 col B
var arrOutput = [];
for(var i = 0; i<arrSheet1.length;i++){
for(var j = 0; j<arrSheet2.length;j++) {
if(arrSheet1[i][0] === arrSheet2[j][0]){
arrOutput.push(['=HYPERLINK("#gid=' + Sheet2ID + '&range=' + Sheet2.getRange(j+4,2,1,Sheet2.getLastColumn()-1).getA1Notation() + '";"' + arrSheet1[i][0] + '")']);
} else {
arrOutput.push([arrSheet1[i][0]]);
}
}
}
Sheet1.getRange(4,2,Sheet1.getLastRow()).clearContent();
Sheet1.getRange(4,2,arrOutput.length).setValues(arrOutput);
}
The problem
The two arrays contain only unique values respectively. The problem is that, because of the double loop, each item is checked by the length of arrSheet2. So for instance let's imagine this scenario:
var arrSheet1 = [apple,avocado,banana];
var arrSheet2 = [apple,banana,mango,];
the arrOutput will result in:
arrOutput = [
apple(link),apple,apple,
avocado,avocado,avocado,
banana(link),banana,banana
];
In a quick, probably unelegant, way to solve the issue I've tried to delete the duplicates frm arrOutput but obviously the value with the link and the values without are different so the best it can get with this solution is this:
arrOutput = [
apple(link),apple,
avocado,
banana(link),banana
];
Question
Is there a smarter/more efficient way to get to
arrOutput = [apple(link),avocado,banana(link)];
or in case what I'm doing actually makes sense, what I should do to get to the result above?
You could use Array.prototype.filter to create a new array that contains only matching elements. If no elements are found this will return an empty array.
var arrSheet1 = ['apple','avocado','banana'];
var arrSheet2 = ['apple','banana','mango'];
var intersect = arrSheet1.filter(function (element) {
return arrSheet2.includes(element);
});
// ['apple','banana']
Or without an Array.prototype.includes polyfill:
var intersect = arrSheet1.filter(function (element) {
return arrSheet2.indexOf(element) !== -1);
})
Issue:
Double looping and pushing array elements multiple times
Solution:
Use break and conditional if statements to control logic
Flow:
If hyperlink is pushed, break the second sheet loop
Only push Sheet1 element if there's no hyperlinks in Sheet2(i.e., Wait till the last element of Sheet2 is iterated)
Snippet:
for(var i = 0; i<arrSheet1.length;i++){
for(var j = 0, k = arrSheet2.length-1; j<=k; j++) {// modified
if(arrSheet1[i][0] === arrSheet2[j][0]){
arrOutput.push(['=HYPERLINK("#gid=' + Sheet2ID + '&range=' + Sheet2.getRange(j+4,2,1,Sheet2.getLastColumn()-1).getA1Notation() + '";"' + arrSheet1[i][0] + '")']);
break; //break j loop to continue next i loop
} else if(j === k){//push sheet1 only on the last element
arrOutput.push([arrSheet1[i][0]]);
}
}
}
Note:
Use of objects {} might be better. Convert sheet2 array to object {val1:hash,val2:hash,...}. Then you can easily check if sheet1 elements are present in sheet2 using in
I have a table that has, let's say 2 columns. Column A has names, column B has either empty or non-empty cells.
I want to create a separate list with names from column A where the opposite cell in B is empty.
When I execute the code it doesn't bring any errors, however the list that is created in the output range is only with the name of the first person that has empty cell in front of him, i.e. that name appears 10 times in my output list.
Can anyone help me understand what's wrong with my code?
Thanks in advance!
This is the code that I have.
function HLnames() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("HL test");
var ind = 0;
var fullRange = ss.getRange("A3:B26").getValues();
var rangeList = [];
for (var i = 0; i < fullRange.length; i++) {
if (fullRange[i][1] === "") {
rangeList.push(fullRange[i][0]);
ind = ind + 1;
}
}
var outputRange = ss.getRange(30, 1, ind, 1);
outputRange.setValue(rangeList);
}
It appears you should be using setValues instead of setValue. – Google's Documentation.
Hope that helps :)
I want to send an email when a cell in column B reaches 5. However, I want part of the email to have the individual's name from column A. Here's my code so far:
function ifstatement() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Total");
var value = sheet.getRange("B3:B241").getValue();
if(value === 5) {
MailApp.sendEmail("jdoe#gmail.com", "Update", "Hi," name "are you alright?");
}
}
I'm trying to define the variable "name" so that it adds the person's name from column A. Could anyone please help me!
The ideal way to get values from different ranges in a sheet is to pull all the data from the sheet as a single 2D array (A table essentially), and work through that to determine what you want to do. This avoids unnecessary complexity, and also ensures that your execution time stays low since you don't need to call the sheet.getRange() service multiple times.
On String concatenation: your string concatenation would not work. You need + between the strings. Go from "Hi," name "are you alright?" to "Hi, "+ name +" are you alright?".
Here is an example solution for you:
Using this example data:
Note: You don't need to know how the columns bit works, just how to use it, think of it as a small service to make life easier if you ever decide to add, or rearrange the spreadsheet's columns.
/*
* Run this to check the sheets values
* This is more verbose to aid with understanding
*/
function checkSheet() {
var sheet = SpreadsheetApp.getActive().getSheetByName('Total');
var range = sheet.getDataRange();
var values = range.getValues();
var columns = getColumnHeaders(values);
//Loop through all the rows in the sheet and check if the 'Number' column is 5
for(var i = 0; i < values.length; i++){
var number = values[i][columns['Number']]; //Get the number from the table of values, utalizing the columns object to get the index
if(number === 5){
var name = values[i][columns['Name']];
var email = values[i][columns['Email']];
Logger.log(name);
Logger.log(email);
//MailApp.sendEmail(email, "Update", "Hi, "+ name +" are you alright?");
}
}
}
/*
* Generates a columns object that lets you reference columns by name instead of index
* Can be used like column['headerText'] to get the index of a column
* Is just a dynamic convenience to avoid referencing columns by index
*/
function getColumnHeaders(array){
var columns = {};
for(var i = 0; i < array[0].length; i++){
columns[array[0][i]] = i;
}
return columns;
}
Condensed checkSheet():
//Condensed version
function checkSheet2() {
var values = SpreadsheetApp.getActive().getSheetByName('Total').getDataRange().getValues();
var columns = getColumnHeaders(values);
for(var i = 0; i < values.length; i++){
if(Number(values[i][columns['Number']]) === 5){
//MailApp.sendEmail(values[i][columns['Email']], "Update", "Hi, "+ values[i][columns['Name']] +" are you alright?");
}
}
}
At this line:
var value = sheet.getRange("B3:B241").getValue();
You're using the method getValue() and it only returns the value of the top-left cell of the range, and you need to to get the values of the whole column A an B, so first set the range to A3:B241 then use the method getValues() to get the values as a two-dimensional array. The line should look like this:
var values = sheet.getRange("A3:B241").getValues();
Once you have the Array you need to loop through the values and check if the element at the index 1 values[i][1] is equal to 5. The line should look like this:
for (var i = 0; i < values.length; i++) {
if(values[i][1] === 5){
// Block of code to be executed if the condition is true
}
}
Finally, the configuration of paramaters you're using for the sendEmail() method is: (recipient, subject, body) the body of the message needs to be a String, you need to concatenate the "Hi,", the name that is in the index 1 values[i][1] and "are you alright?", to achieve that you need to use the the concatenation operator (+), the line should look like this:
MailApp.sendEmail("jdoe#gmail.com", "Update", "Hi," + values[i][0] + " are you alright?");
The complete code:
function ifstatement() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Total");
var values = sheet.getRange("A3:B241").getValues();
for (var i = 0; i < values.length; i++) {
if(values[i][1] === 5){
MailApp.sendEmail("jdoe#gmail.com", "Update", "Hi," + values[i][0] + " are you alright?");
}
}
I ask the same matrix table (multiple answer) question twice in my Qualtrics survey. When I display the question to the user the second time, I want the answers to be auto populated from the first time the user answered the question. I've looked at the code snippets in the documentation, so I know how to select the checkboxes in the current question, e.g.
for (var i = 1; i <= rows; i++) {
for (var j = 1; j <= cols; j++) {
this.setChoiceValue(i, j, true);
}
}
My issue is that I can't figure out how to get the selected answers from the previous question. I have tried something like this but it doesn't work (cannot read property 'attr' of null):
var isChecked = $("#QR~QID2~" + i + "~" + j").attr('checked');
This page from the documentation suggests using piped text, something along the lines of:
var selectedChoice = "${q://QID2/ChoiceGroup/SelectedChoices}";
This seems to give me the unique statements in the rows of the survey that have been selected, but I need to get the selected answer (col) for each statement (row) in the question. I'm not sure how to formulate the correct piped text.
Any advice would be much appreciated! Thanks.
Edit 1: Here is the code I ended up using.
Qualtrics.SurveyEngine.addOnload(function()
{
/*Place Your Javascript Below This Line*/
var numChecks = $(this.getQuestionContainer()).select('input[type="checkbox"]');
var numCols = 4;
var numRows = numChecks.length / numCols;
var map = {};
//I won't have more than 20 rows in the matrix
map[1] = "${q://QID53/SelectedAnswerRecode/1}";
map[2] = "${q://QID53/SelectedAnswerRecode/2}";
map[3] = "${q://QID53/SelectedAnswerRecode/3}";
map[4] = "${q://QID53/SelectedAnswerRecode/4}";
map[5] = "${q://QID53/SelectedAnswerRecode/5}";
map[6] = "${q://QID53/SelectedAnswerRecode/6}";
map[7] = "${q://QID53/SelectedAnswerRecode/7}";
map[8] = "${q://QID53/SelectedAnswerRecode/8}";
map[9] = "${q://QID53/SelectedAnswerRecode/9}";
map[10] = "${q://QID53/SelectedAnswerRecode/10}";
map[11] = "${q://QID53/SelectedAnswerRecode/11}";
map[12] = "${q://QID53/SelectedAnswerRecode/12}";
map[13] = "${q://QID53/SelectedAnswerRecode/13}";
map[14] = "${q://QID53/SelectedAnswerRecode/14}";
map[15] = "${q://QID53/SelectedAnswerRecode/15}";
map[16] = "${q://QID53/SelectedAnswerRecode/16}";
map[17] = "${q://QID53/SelectedAnswerRecode/17}";
map[18] = "${q://QID53/SelectedAnswerRecode/18}";
map[19] = "${q://QID53/SelectedAnswerRecode/19}";
map[20] = "${q://QID53/SelectedAnswerRecode/20}";
for (var i = 1; i <= numRows; i++) {
//Get the recode values for row i
var rowValues = map[i].split(",");
//Loop through all the recode values for the current row
for (var c = 0 ; c < rowValues.length; c++) {
var val = parseInt(rowValues[c].trim());
//Select the current question's checkboxes corresponding to the recode values
this.setChoiceValue(i, val, true);
}
}
});
Edit 2:
I'm getting some strange behavior now. I'm trying to populate a table with only 3 rows, so I would think that
"${q://QID53/SelectedAnswerRecode/1}";
"${q://QID53/SelectedAnswerRecode/2}";
"${q://QID53/SelectedAnswerRecode/3}";
would give me the values for the first three rows from the previous table for question "QID53" . But actually those return empty strings, and it's not until calling
"${q://QID53/SelectedAnswerRecode/5}";
"${q://QID53/SelectedAnswerRecode/6}";
"${q://QID53/SelectedAnswerRecode/7}";
that I get the first three values.
For a table of 14 rows, nothing returns until calling
"${q://QID53/SelectedAnswerRecode/4}";
and it leaves the last 3 rows in the table empty.
Am I wrong in assuming that the number after "SelectedAnswerRecode" is the row number? Is there something about an offset that I'm missing?
I think you'll want to pipe in the recode values. Something like:
var r1ar = parseInt("${q://QID2/SelectedAnswerRecode/1}");
var r2ar = parseInt("${q://QID2/SelectedAnswerRecode/2}");
Then set the values:
this.setChoiceValue(1, r1ar, true);
this.setChoiceValue(2, r2ar, true);
Make sure your recode values match the column ids.
Edits/additions based on comments below:
Piped values in the javascript get resolved server side before the page is sent to the browser, so they are fixed values. There is no way to make them dynamic in javascript. If your question has a variable number of rows due to display logic or carryover, you'll have to include all the possible piped values in the javascript, then check them to see which ones are valid.
For a multiple answer matrix, you can convert the comma separated list into an array using str.split(',') then loop through the array. Qualtrics includes spaces after the commas in comma separated lists, so you'll have to trim() the strings in the array.