Autofill google forms based on user input - google-apps-script

Alright stack friends,
I'm working on my first projects using google scripts and it's been pretty fun so far. My project is to create a form for data entry that can either accept an ID number and fill in the rest of the fields, or let the user fill out the entire form. Basically my method to fill in the other fields is just to have a lookup table on the second sheet. When the user submits a form, the script runs, looks for the ID of the last row, scans the reference table for the ID, and then fills in the details.
I think the problem I'm having is the assumption that the data from the form is already in the sheet when the script runs. The problem I noticed is that the script sometimes fails to fill in the gaps. I tried creating form submissions in a loop with the same ID and they function somewhat erratically but it seems like the last sumbission always works which would make sense if the script executions are not matching up with the form submissions. Here's the script for reference:
function fillGaps() {
// First take in the appropriate spreadsheet objects and get the sheets from it
var ss = SpreadsheetApp.openById(id);
var sheet = ss.getSheets()[0];
var refSheet = ss.getSheets()[1];
// Here's the last rows' index
var lastRow = sheet.getLastRow();
var lastRowRef = refSheet.getLastRow();
// now this is an array of values for the last row and the student ID entered
var response = sheet.getRange(lastRow, 1, 1, 7).getValues();
var enteredID = response[0][1];
// Next we're going to try to load up the lookup table and scan for the ID
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()).getValues();
var row = 0;
while(enteredID != stuIDs[row] && row <= lastRowRef){
row++;
}
// Okay at this point the row variable is actually -2 from what the sheet index
// is that I'm thinking of. This is because we didn't load the first row (names)
// and the way arrays are indexed starts with 0.
row++;
row++;
// now assuming that it found a match we'll fill in the values
if(row < refSheet.getLastRow()){
// Alright now we need to wrangle that row and format the data
var matchedRow = refSheet.getRange(row, 1, 1, 6).getValues();
// modify the response
var replacement = [response[0][0],enteredID, matchedRow[0][1],matchedRow[0][0],matchedRow[0][2],matchedRow[0][4],matchedRow[0][5]];
sheet.getRange(lastRow, 1, 1, 7).setValues([replacement]) ;
}
}
So I'm wondering:
Does this seem like the right diagnosis?
If so, what would be the best way to remedy? I thought of adding a little delay into the script as well as trying to capture the submissions timestamp (not sure how to do that)
Thank you much!

The following code gives a 2D array:
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()).getValues();
Also,refSheet.getLastRow gives the last row, lets say it is 10 in this case. The syntax for getRange is getRange(row, column, numRows) and the last argument is the number of rows, not the last column. So in the above code the selected range would be row 2 - 11 rather than 2- 10. Unless that is what you intended, modify the code like so:
var stuIDs = refSheet.getRange(2, 4, refSheet.getLastRow()-1).getValues();
To access the values in stuIDs you should use stuIDs[row][0] (2D array) to check for matching ID. Assuming your ID was to be matched was in column 1.
Secondly, in the loop you are using the following to check for the last index in array row <= lastRowRef which will cause it go out of range(because array starts at 0 and sheet row at 1) instead use this row < stuIDs.length
Finally, in case you don't find a match you will end up with the last row and your code will end you taking the last row as the matched index. This can be prevented by using a boolean variable to check for a match.
var foundId = false
var row = 0;
var i = 0;
for (i in stuIDs){
if(stuIDs[i][0] == enteredID)
foundID = true
break
}
}
row = i + 2
if (foundID){
var matchedRow = refSheet.getRange(row, 1, 1, 6).getValues();
// modify the response
var replacement = [response[0][0],enteredID, matchedRow[0][1],matchedRow[0][0],matchedRow[0][2],matchedRow[0][4],matchedRow[0][5]];
sheet.getRange(lastRow, 1, 1, 7).setValues([replacement]) ;
}
PS: You can also use event objects to get the values of response (eventObj.values). As mentioned here: https://developers.google.com/apps-script/guides/triggers/events
Hope that helps!

Related

Copy data range and paste to new page, and repeat

I am trying to copy a range from sheet 'Full' and paste the values only to a new sheet, 'Dump'. While the macro below does its action once, I am regenerating the original data range (Full), so I want to copy that new set and append to the same output page, indexed down to a blank row and keeping the first pasted data. Also then to do this 100 times.
The recoded macro is below, and I need to understand the script to add in to;
repeat the copy/paste function 100 times, and also
offset the paste range by a set number of rows.
Sorry, genuine newbie at editing google sheet macros. The Excel macro I use doesn't translate over.
Appreciate any answers you have.
function xmacro() {
var spreadsheet = SpreadsheetApp.getActive();
spreadsheet.getRange('A1').activate();
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Full'), true);
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Dump'), true);
spreadsheet.getRange('Full!BK3:BT34').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);```
};
Your macro is just an automatically generated app script. You can extend its functionality by building off that with some more code. First I'll explain some of the basic concepts, if you know this, then just skip down to the code.
Sheets Concepts
Here are some basic concepts that took me forever to figure out because most of the documentation assumes you are already proficient at Javascript.
A range is a 2 dimensional array that has one array for each row, and the contents of that array are the columns:
someRange = [
[row1Col1, row1Col2, row1Col3, row1Col4],
[row2Col1, row2Col2, row2Col3, row2Col4],
[row3Col1, row3Col2, row3Col3, row3Col4]
]
To access a specific value you need to reference the row array, and then the index of the column you want.
Think about it like hotel room numbers. The first part of the number is the floor,
and the second part is the specific room on that floor.
You access arrays by calling the array name, then square brackets with the index number of the element you want.
Arrays are indexed starting at 0, so to get row 1 you would use:
someRange[0] would return the inner array [row1Col1, row1Col2, row1Col3].
But that doesn't give you a specific cell values - so you would use a second set of brackets to access the column in that row:
someRange[0][1] = 'row1Col2'
Arrays also have built in information, so you can find the length of an array by using Array.length no parenthesis.
Since the rows are in the outer array, you can get the number of rows by seeing how many inner arrays there are.
someRange.length = 3 There are 3 row arrays in the someRange array.
You can do the same with columns, since the number of columns is equal to the number of elements in an array. To get the number of elements in the first row you would use:
someRange[0].length - which would be 4
And since a range has the same number of columns for each row, you can pick any row
to get the number of columns (generally, there are always exceptions)
The Code
The first function will create a custom menu item to run the code.
// create a new menu item for your custom function
function onOpen(){
SpreadsheetApp.getUi().createMenu()
.addItem('100 Copies', 'lotsOfCopies')
.addToUi();
}
function lotsOfCopies() {
var ss = SpreadsheetApp.getActive();
var copySheet = ss.getSheetByName('yourCopySheetName');
var pasteSheet = ss.getSheetByName('yourPasteSheetName');
// the range you wish to copy, change to fit your needs
var copyRange = copySheet.getRange('A1:B7');
var copyValues = copyRange.getValues();
var copyRows = copyValues.length;
var copyCols = copyValues[0].length;
// define the first row to be pasted into
var pasteRow = 1;
// define the left side column of the range to be pasted into
var pasteCol = 1
// build a loop that does the same thing 100 times,
// and each time offsets the paste range by the number of rows in the copy range
for (var i = 0; i < 100; i++) {
// for every iteration after the first,
// add the number of rows in the copy range to the variable 'row'
// example if there are 10 rows in the copy range then
// iteration 1 row = 1 Iterartion 2 row = 11, Iteration 3 row = 21
if (i > 0) {
pasteRow = +pasteRow + +copyRows
}
// build the range to paste into - it starts on pasteRow and paste col,
// and is as many rows as the copied range, and as many columns as the copied range
let pasteRange = pasteSheet.getRange(pasteRow, pasteCol, copyRows, copyCols);
// put the values from copyValues into the pasteRange
pasteRange.setValues(copyValues);
}
}
function xmacro() {
const ss = SpreadsheetApp.getActive();
const ssh = ss.getSheetByName('Full')
const dsh = ss.getSheetByName('Dump')
ssh.getRange('BK3:BT34').copyTo(dsh.getRange('A1'), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
}

Find last row with Data in Column D starting the search at Row 4 in Google App Script

I'm trying to find a way to find the last row with data in Column D. I also want the search to start at Row 4.
I'm really struggling and would appericate any help please.
You can refer to this code:
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet2');
var rowOffset = 3;
var count = sheet.getRange('D4:D').getDisplayValues().flat().filter(String).length;
var lastRow = count+rowOffset;
Logger.log(lastRow);
What it does?
Select Range D4:D and get its value, since you want to get the last row with data in column D starting at row 4.
Use array.flat() to change 2-d array into 1-d array.
Use array.filter() to remove empty values. Then get it's array length
To get the last row index, Use the cell count that has data which we obtained in step 3 and add it to 3 (start offset since we start our search at row 4)
Note:
This solution will only work assuming you don't have empty rows in between.
Output:
Execution log
2:01:53 AM Notice Execution started
2:01:54 AM Info 13.0
2:01:55 AM Notice Execution completed
There are many ways to find the last value in a column, but here is one. See if this helps!
function myFunction() {
const spreadsheet = SpreadsheetApp.openByUrl('Insert Sheet URL Here');
const sheet = spreadsheet.getSheetByName('Insert Sheet Name Here - e.g. Sheet1');
const lastSheetRow = sheet.getLastRow();
let lastColumnRow = 'This column is empty';
let reversedColumnValues = sheet.getRange(1, 4, lastSheetRow).getValues().reverse();
for (index in reversedColumnValues) {
if (reversedColumnValues[index][0] != '') {
lastColumnRow = lastSheetRow - index;
break;
}
}
Logger.log(lastColumnRow);
}
This Apps Script Video may help you out too: https://www.youtube.com/watch?v=1Po1QElOFPk

Overwriting Google sheets (for form response) rows if duplicate entered

So, I've been trying to figure out how to stop the duplicate rows appearing in my google sheets response output from a google form. If found this link which sounds like it does exactly what I want (Form Google Script Prevent Duplicates), but cannot for the life of me work out how to edit the given answer to work on my sheet.
I have included a screenshot of my workbook to give an example of the structure of the data I'd like the edited code to run on, and also below is my attempt at making the code run correctly on my data structure.
My sheet structure that I'd like to run the code on. I want to use the email address as the 'unique' identifier, so any duplicate rows can be identified using that.
My attempt at adapting the code to work on the above data structure (I have absolutely no background with this scripting language, so please go easy on me if I've made a glaringly obvious error):
function updateExisting() {
var s = SpreadsheetApp.getActiveSheet(),
// s = ss.getSheetByName(''),
lastRow = s.getLastRow(),
lastValues = s.getRange('A'+lastRow+':C'+lastRow).getValues(),
name = lastValues[0][0],
allNames = s.getRange('B2:B').getValues(),
row, len;
// TRY AND FIND EXISTING NAME
for (row = 0, len = allNames.length; row < len - 1; row++)
if (allNames[row][0] == name) {
// OVERWRITE OLD DATA
s.getRange('A2').offset(0, 0, row,
lastValues.length).setValues([lastValues]);
// DELETE THE LAST ROW
s.deleteRow(lastRow);
break;}
}
Key words: duplicates, Google, spreadsheet, Sheets, Form, submission, edit, row, unique.
This code prevents duplicates in a Google Sheet when submitting a Google Form, by overwriting an existing row with the existing unique value, if one exists.
The code searches one column in a spreadsheet and looks for a match. I tried to make it generic so that the code doesn't need to be changed depending upon what column the unique identifier is in. You need to make a couple of settings in the "User Settings" section to make it work. But that is better than needing to rewrite the code.
function updateExisting(columnWithUniqueIdentifier,sheetTabName) {
var dataFromColumnToMatch,lastColumn,lastRow,rowWithExistingUniqueValue,rowOfDataJustSaved,
sh,ss,valueToSearchFor;
// USER SETTINGS - if the values where not passed in to the function
if (!columnWithUniqueIdentifier) {//If you are not passing in the column number
columnWithUniqueIdentifier = 2;//Hard code column number if you want
}
if (!sheetTabName) {//The sheet tab name was not passed in to the function
sheetTabName = "Put your Sheet tab name here";//Hard code if needed
}
//end of user settings
ss = SpreadsheetApp.getActiveSpreadsheet();//Get the active spreadsheet - this code must be in a project bound to spreadsheet
sh = ss.getSheetByName(sheetTabName);
lastRow = sh.getLastRow();
lastColumn = sh.getLastColumn();
//Logger.log('lastRow: ' + lastRow)
rowOfDataJustSaved = sh.getRange(lastRow, 1, 1, lastColumn).getValues();//Get the values that were just saved
valueToSearchFor = rowOfDataJustSaved[0][columnWithUniqueIdentifier-1];
//Logger.log('valueToSearchFor: ' + valueToSearchFor)
dataFromColumnToMatch = sh.getRange(1, columnWithUniqueIdentifier, lastRow-1, 1).getValues();
dataFromColumnToMatch = dataFromColumnToMatch.toString().split(",");
//Logger.log('dataFromColumnToMatch: ' + dataFromColumnToMatch)
rowWithExistingUniqueValue = dataFromColumnToMatch.indexOf(valueToSearchFor);
//Logger.log('rowWithExistingUniqueValue: ' + rowWithExistingUniqueValue)
if (rowWithExistingUniqueValue === -1) {//There is no existing data with the unique identifier
return;
}
sh.getRange(rowWithExistingUniqueValue + 1, 1, 1, rowOfDataJustSaved[0].length).setValues(rowOfDataJustSaved);
sh.deleteRow(lastRow);//delete the row that was at then end
}

Very strange Google Apps Script behavior

I am seeing some very strange behavior with Google Apps Script. All I am trying to do is parse some rows in my sheet and if for any row, if the date is > 1/1/1999 then add a dropdown which pre populates some Error Value. The strange behavior here is that this always populates the dropdown one row above the row which satisfies the date check condition. So for example if row #5 satisfies the condition, the dropdown is populated in row #4. Not sure why this is happening. I also tried to do this
var range = sheet.getRange(i+1, 14);
but that doesn't seem to show anything on the screen. I am unable to understand this strange behavior. Any help will be appreciated.
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[1];
var dataRange = sheet.getDataRange();
var values = dataRange.getDisplayValues();
for (var i = rowNum; i < values.length; i++) {
if (new Date(values[i][9]).getTime() > new Date('1/1/1999').getTime()){
SpreadsheetApp.getUi().alert(values[i][2]);
var range = sheet.getRange(i, 14);
var rule = SpreadsheetApp.newDataValidation().requireValueInList(['Error Value'], true).build();
range.setDataValidation(rule);
}
Use
var range = sheet.getRange(Number(i)+1, 14);
When you are using i+1 and when i=20, i+1 will become 201 and you will not seeing any visible difference on the screen. row 201 will be far down your screen.
In JavaScript, numbers and strings will occasionally behave in ways you might not expect. It is always good to take precaution before doing operation on variables.
Also read https://autotelicum.github.io/Smooth-CoffeeScript/literate/js-intro.html#operations-on-numbers-strings
Your data validation is set to null. It should be Range.setDataValidation(rule)
that is why you dont see a dropdown box.
This function
Range.getDisplayValues()
Returns an array and index of an array starts at 0. However, in a spreadsheet the row index starts at 1. So you need to add 1 to convert array index to a row index. Use the following to get the correct range
Sheet.getRange(i+1,14)

Incorrect Range Height - Google Script

I'm trying to create a Google sheet script that will take a list of names and resend them to the Google Sheet. I have two columns of data, the first column contains a persons name. The second column contains multiple cells that the person in the first cell is inviting to a party. This creates a problem, the name in column 1 might be on row 2, but if they invite 20 people then column one has blank spaces rows 3-21. It may sound pointless right now to most of you, but I want to be able to sort the sheet alphabetically by the name of the person who did the inviting, AND be able to sort it into a separate sheet alphabetically by the name of the guest in Column 2, while still keeping the person who invited them tracked as well. This is the only way I could think of accomplishing the task.
I'm currently stuck on writing the array back to the sheet, I keep getting "Incorrect range height, was 1 but should be 339." I've figured out how to successfully get an array of data, filled exactly how I wanted it, but can't seem to get this part. I've searched through here and tried to implement the solutions I find, but have had no luck.
This is what I have come up with so far, and it works up until the setValues(
function inviteSorter() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var current = sheet.getSheets()[0];
var lastRow = current.getLastRow();
var rangeData = current.getRange(2,1,lastRow-1,3);
var numColumns = rangeData.getNumColumns();
// Logger.log(rangeData);
var info = rangeData.getValues();
var Name = {};
// Examines the cell in the first column, if it is empty replaces it with the name from the previous cell.
for ( var i = 0; i< info.length; i++){
if (typeof(info[i][0]) == "string" && info[i][0] == ""){
Name[i] = Name[i-1];
} else{
Name[i] = info[i][0];
}
}
var data = []
for (var i = 0; i<lastRow-1; i++){
data.push(Name[i]);
}
var writeRange = current.getRange(2,1,data.length,1);
writeRange.setValues([data]);
The value you are expecting should be a 2D array, 1 column of multiple rows. What you get when using data.push(Name[i]); is a simple array of strings.
Try this way : data.push([Name[i]]); this will return an array of arrays and should satisfy the conditions for setValues(data)
( don't forget to remove the brackets in your last setValues statement )