Sorting duplicate submissions by date - google-apps-script

I have a Google form that deposits data to a google spreadsheet. All the data from the first form filled out will be deposited on row 1, then the next form filled out will be deposited on row 2, etc. A problem that I have been having is that sometimes people lose their link to edit their submitted form and they submit a whole new entry creating a duplicate row on google sheets with updated information.
I am trying to make sheet2 that will filter out the duplicate entries but still keep the order that they first appeared. I have considered using UNIQUE(range) function however the problem is that the unique function takes the first time the name appears. What I want it to do is keep the position that the first name appears in but take the data of the latest time the name appears because that will have the updated information. I cannot figure out a way to do this on my own. Is there anyone who is more knowledgeable that can help?
Below is an example of what I am hoping to achieve. The first time John appears is overwritten by the latest time it appears.

I think this should do it for you.
function findCopyDeleteDupes()
{
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sht=ss.getActiveSheet();
var rng=sht.getDataRange();
var rngA=rng.getValues();
var nodupes=[];
var dupeRows=[];
for(var i=0;i<rngA.length;i++)
{
var idx=nodupes.indexOf(rngA[i][1]);
if(idx==-1)//if not in nodupes then put it in nodupes
{
nodupes.push(rngA[i][1]);
}
else
{
for(var j=0;j<rngA[i].length;j++)
{
rngA[idx][j]=rngA[i][j];//Copy data from the latest time to the first position
}
dupeRows.push(i+1);//add this row to the delete rows array
}
}
rng.setValues(rngA);
for(var i=dupeRows.length-1;i>-1;i--)//delete rows from the bottom first
{
sht.deleteRow(dupeRows[i])
}
}

Related

Paste formula as value on a certain date

I've been digging around on line for a couple of hours now to no avail. I'm trying to write a formula(which I assume is only possible through AppScript), that based on a certain date, will re-paste contents in a cell as values only.
The issue I've had with other scripts is that in my situation I have several different values that need to be checked. I've set up a continuously updating calendar that pulls from an array with listed dates for each item. However, in that array, the row gets moved into an archive once it hits todays date. Thus, they get deleted from the calendar too.
If there is another solution to this, like pasting based off conditional formatting(this would then keep the formulas that haven't found a value yet, and paste ones that have been found as values only), that would be great too.
Any help or advice would be greatly appreciated. If you need any other info, please ask.
EDIT:
The spreadsheet link is in comments. Attached are a before and after picture to help visualize whats going on here. Since the calendar is pulling from the active sheet, and rows on the active sheet go to "archive" when J2-Today() is -1(yesterday), those then disappear from the calendar. I've tried to make a mixed sheet with both archive and active, but I get duplicates as a frequent issue.
See code below:
function archiveRows() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var mainSheet = ss.getSheetByName("Sheet1");
var archiveSheet = ss.getSheetByName("Sheet2");
// last row should be on the data itself, same with column
var lastRow = mainSheet.getLastRow();
var lastCol = mainSheet.getLastColumn();
// set time to 0 so we can only compare dates
var dateToday = new Date().setHours(0, 0, 0, 0);
var range = mainSheet.getRange(1, 1, lastRow, lastCol);
var values = range.getValues();
// remove header to only process data
var header = values.shift();
var indices = [], difference = [];
// remove dates that are earlier than today
// collect rows and indices data when condition is not met
values.forEach(function (row, index) {
if (row[9] >= dateToday)
return true;
difference.push(row);
indices.push(index);
});
// remove the row with earlier date
// offset with 2 due to 0-indexing and header
indices.forEach(index => mainSheet.deleteRow(index + 2));
// all rows that were deleted are copied to archive sheet
difference.forEach(row => archiveSheet.appendRow(row));
}
Sample Data:
Output:
Note:
If you need to have this run on a daily basis, Time Driven trigger is your friend. Just have it set on a daily basis triggering archiveRows
Make sure that the main sheet only contains the data. (I see your sheet has rows containing 9AM values below. Make sure to clear those as the script will mistakenly include it on the processing)
Adjust date condition if needed.

Searching a Google Form response (Google Sheets) for row and column numbers

I am trying to figure out how to search a google forms response (google sheets) in order to find a value of the row and column number to extract data.
I have everything else, but searching for the column and row and I'm not sure how to make a 2D search for this.
Here is the data I want to search and extract, but I don't want all of the data, just specific cells/ranges.
https://docs.google.com/spreadsheets/d/1ehA0fXBM6CPBAKfHvBbouRPjOMDxrXSdFI0zMye3G7c/edit?usp=sharing
My plan is to pull the "Employee Name" into another spreadsheet for an overview of who has completed it and at what location on a daily basis. Because the form is always receiving responses I want the script to pull data multiple times a day to show compliance of the form. I want the response to look something like this:
https://docs.google.com/spreadsheets/d/1QyK5c-vxkt5BnM-wM-m80RZCO_29KWoF_isIZBOllvQ/edit?usp=sharing
Here is what I was hoping to get my Google Script to do:
1. Script searches source sheet finds location.
2. Script returns row and column for where the location was listed.
3. Script takes row and column and subtracts 1 from the row and getValue(); of that cell. (Which in this case will be the employee who completed the form.)
4. Script returns the "employee name" to destination sheet based on employee location. (So it is in the right line.)
Result, script pulls data from the form responses and lists who completed it.
I have everything working to pull data from a specific cell/range and paste it to the destination sheet based on location and the date.
Now that I've explained what I'm trying to do, I can explain where I'm stumped. I can't figure out how the repeating for or when command works. I want to search for one specific location and than return the row and column number. Here is the code I have been trying to manipulate, but I can't get it to search in 2d or even the row to return a value int eh for/when command.
function updateVerification() {
var ss = SpreadsheetApp.openById("1ehA0fXBM6CPBAKfHvBbouRPjOMDxrXSdFI0zMye3G7c");
var sheet = ss.getSheetByName("Sheet1");
var ss_dest = SpreadsheetApp.openById("1QyK5c-vxkt5BnM-wM-m80RZCO_29KWoF_isIZBOllvQ");
var sheet_dest = ss_dest.getSheetByName("Destination");
var rows=ss.getDataRange().getValues();
var paste = 0;
for(var r=0; r<rows.length; r++){
for(var c=0; c<rows[r].length; c++){
if(rows[r][c] == "Location #1"){
paste = sheet.getRange(rows[r]+":"+rows[c]).getValue();
sheet_dest.getRange(2,4).setValue(paste);
}
}
}
}
If anyone is able to help me I would greatly appreciate it.
Thanks for any and all help. :)
I'm guessing you want something like this:
function updateVerification() {
var ss=SpreadsheetApp.openById("1ehA0fXBM6CPBAKfHvBbouRPjOMDxrXSdFI0zMye3G7c");
var sh=ss.getSheetByName("Sheet1");
var ss_dest=SpreadsheetApp.openById("1QyK5c-vxkt5BnM-wM-m80RZCO_29KWoF_isIZBOllvQ")
var sh_dest=ss_dest.getSheetByName("Destination");
var rows=ss.getDataRange().getValues();
for(var r=0;r<rows.length;r++) {
for(var c=0;c<rows[r].length;c++) {
if(rows[r][c]=="Location #1") {
sh_dest.appendRow(rows[r]);
}
}
}
}
If I remove the cell contents and replace it with the array indices your data would look something like this:
[
["row[0][0]","rows[0][1],"rows[0][2]"],
["row[1][0]","rows[1][1],"rows[1][2]"],
["row[2][0]","rows[2][1],"rows[2][2]"],
["row[3][0]","rows[3][1],"rows[3][2]"]//rows[rows.length-1]
]

Google Apps Scripts Send automated email with trigger (not repetitively)

I'm attempting to set up automated emails through google sheets using scripts and a trigger.
How do I define only new additions to the spreadsheet should trigger an email? The spreadsheet is constantly added to.
function sendloggeremails() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var lr = ss.getLastRow()
for (var i = 72; i <= lr; i++) {
var currentEmail = ss.getRange(i, 7).getValue();
var currentClassTitle = ss.getRange(i, 3).getValue();
MailApp.sendEmail(currentEmail, "complaint: customer number " + currentClassTitle , "Please check through the log as you have a new assigned to you");
}
}
var i = 72 plainly because this is the last row, I don't want to have to manually change this constantly. Added triggers but at the moment I still need to go into the code to change var i.
Any chance anyone can help with this?
Sending Emails only Once with a Loop
You could use something like this. To get started you would want to put something in the sent column so that old lines would not resend their emails and you'd still maintain a record of past emails. I suggested putting the string "SENT". But the test just ask the question is that column empty so anything will work.
Obviously, I haven't seen you spreadsheet so I don't know where to put the sentColumn so you can change the var sentColumn = 75 to anything you wish and all of the other places it's used will change accordingly.
function sendloggeremails() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var sh=ss.getActiveSheet();//You should probably change this to getSheetByName() and then put in the correct name of the sheet
var sentColumn=75;//If this column is not empty then don't send emails. If you send any email then also put "SENT" into this column so you wont resend it next time you run the loop.
sh.getRange(1,sentColumn).setValue("Sent");//I'm assuming a header row and I'm putting a Header Title there you can pick any open column you want
var rg=sh.getDataRange();//This gets all of the sheets data into one one range
var vA=rg.getValues();//This gets all of the values in above range in a two dimension object we often refer to as a two dimensional array.
//If you have header row you can start at i=1
for(var i=1;i<vA.length; i++) {//This will loop over all of the rows on the sheet.
if(!vA[i][sentColumn-1]){//Heres the test to see if theres something in sentColumn. If sentColumn is empty meaning truthy is false then it send the email
var currentEmail=vA[i][6];//this replaces the getValue from column7 *1
var currentClassTitle=vA[i][2];//this replaces the getValue from column3 *1
MailApp.sendEmail(currentEmail, "complaint: customer number " + currentClassTitle , "Please check through the log as you have a new assigned to you");
sh.getRange(i+1,sentColumn).setValue("SENT");//After sending email we put something. In this case "SENT" into the sentColumn so that next through the loop we won send another email because its truthy will be true.
}
}
}
//*1 Array indices are 1 less that column numbers because arrays start counting from zero.
If you wish we could also delete the rows as we send them. All you have to do is keep track of how many rows you have deleted each time the loop is run with say a variable like var n=0 to start with and then the row number that gets deleted will be i-n+1. And right after the deletion you increment n by one.
You can setup the trigger using this function.
function setupEmailTrigger() {
if(ScriptApp.getProjectTriggers().indexOf('sendloggeremails')==-1){
ScriptApp.newTrigger('sendloggeremails').timeBased().everyDays(1).atHour(17).create();
}
}
It checks to see if the trigger already exists and if the trigger already exists it does nothing.

google sheets script to remove rows based on dates in multiple cells

I am working on an app to remove old content from a spreadsheet. I have found several posts that hide or delete sheets based on specific data, but it is always in a single cell. I need something that checks dates in three cells per row.
The sheet is populated by a form that asks users to enter three dates they want a bulletin to run in our video and print bulletin at the HS where I teach. I just want to remove old data, and found countless (well, maybe 6 or 8) examples of scripts that did something similar, so I grabbed one and tried to expand the logic statement to check columns C, D, and E (using index, of course).
It works, but removes any row with a date that is not today rather than dates in all three columns. I also need to have it count cells that are empty as qualifying as old.
So I tried the script suggested below by Cooper, which is a big advancement. It is still skipping rows with empty values, and in one case removes a row that I don't want to remove
In the image shown a form accepts input from teachers who submit bulletin content for a maximum of three dates. I've formatted the sheet to show old dates in red, today in green, and future dates in yellow. I want to delete rows that have all red or empty cells. A bandaid, however, would be to force them to pick a date for all three rather than allow non entries. But it would be nice to have the code recognize blank cells as qualifying.
[![Data Sheet for daily bulletin][1]][1]
Here's what eventually worked. I'm sure there are some smoother ways of doing this, but I actually grew a bit in my programming ability by working things out through the log first, then moving on once I had evidence that what I was doing worked.
/*
Here is the thinking:
I need to have a script that is run from Add-ons that checks each cell of a range
for dates. It can be done through conditional formatting bg colors, but that is
redundant.
The first thing is to create the functions that find and return row numbers that have all
old dates. In the initial build these can be returned to the log (Logger.log()) inside
of a for(loop).
'*/
function readRows() {
var sh=SpreadsheetApp.getActiveSheet();
var rg=sh.getDataRange();
var vA=rg.getValues();
var rowsDeleted=0;
var today=new Date().valueOf();
//cycle through rows. In each row check column values for date. if date is old, add 1 to oldCount
for(var i=vA.length-1;i>=0;i--) {
var oldCount=0;
if(new Date(vA[i][2]).valueOf()<today) {
oldCount++;
}
if(new Date(vA[i][3]).valueOf()<today) {
oldCount++;
}
if(new Date(vA[i][4]).valueOf()<today) {
oldCount++;
}
if(oldCount>2) {
sh.deleteRow(i+1);
rowsDeleted++;
}
}
Logger.log(rowsDeleted);
}
Next I want to figure out how to make something that will output the daily bulletin to a Doc or pdf.
Try this:
function readRows() {
var sh=SpreadsheetApp.getActiveSheet();
var rg=sh.getDataRange();
var vA=rg.getValues();
var rowsDeleted=0;
var today=new Date().valueOf();
for(var i=vA.length-1;i>=0;i--) {
if ((new Date(vA[i][2]).valueOf()<today)||(new Date(vA[i][3]).valueOf()<today)||(new Date(vA[i][4]).valueOf()<today) ){
sh.deleteRow(i+1);
rowsDeleted++;
}
}
Logger.log(rowsDeleted);
}
I'm not completely sure what you want but this might help. When your deleting rows you should start from the bottom.

Google Sheets Script - Move Rows from One Tab to Another

I'm trying to modify the code from the below forum post to help fit my needs:
https://productforums.google.com/forum/#!topic/docs/ehoCZjFPBao/discussion
function onEdit() {
// moves a row from a sheet to another when a magic value is entered in a column
// adjust the following variables to fit your needs
// see https://productforums.google.com/d/topic/docs/ehoCZjFPBao/discussion
var sheetNameToWatch = "Transfers";
var columnNumberToWatch = 15; // column A = 1, B = 2, etc.
var valueToWatch = "yes";
var sheetNameToMoveTheRowTo = "Archive";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getActiveCell();
if (sheet.getName() == sheetNameToWatch && range.getColumn() == columnNumberToWatch && range.getValue() == valueToWatch) {
var targetSheet = ss.getSheetByName(sheetNameToMoveTheRowTo);
var targetRange = targetSheet.getRange(targetSheet.getLastRow() + 1, 1);
sheet.getRange(range.getRow(), 1, 1, sheet.getLastColumn()).moveTo(targetRange);
sheet.deleteRow(range.getRow());
}
}
What this code does is moves a row from one tab to another when a specific value is entered (in the code above, it moves a row from "Transfers" to "Archive" when the word "yes" is entered in column 15 (Column O).
My needs are a little bit different. I cannot have a user moving one row at a time because there may be multiple rows involved and combined together, they will need to meet certain criteria (e.g. do the amounts in all rows balance to 0, is the same account used, etc.). This I believe is a rudimentary data validation.
Therefore, I tried to make a formula so that if I entered the word yes in cell F8, it populates the word "Send" (used to be "yes", changed for clarity) to column O. If the word yes is entered in cell F8, then every row with the word "Send" should be moved to the Archive Column. The problem is, the above code relied on a function called OnEdit, and populating the rows with the word Send via formula does not trigger the script to run. The above code needs the user to manually enter the correct keyword in order to move the rows over.
Can someone help to modify or rewrite the code so that it looks for a user to manually type in the keyword "yes" in cell F8, and then have it move any row that had the word "Send" populated by a formula in Column O?
The below Google Sheet is a simplified example of what I'm trying to do. Additional comments can be found on the "MASTER - DO NOT EDIT" tab.
https://docs.google.com/spreadsheets/d/1iajS90qvwEOGVnl2lpDbVtcI532OO8n4NLZEBDUpVzA/edit#gid=398066315
Thanks for looking. If anyone needs additional info, please let me know.
~~~~~~~~~~~~~~~~~~~~~~~~~~~
EDIT: I tried out Cooper's code below, and it seems to work. I'm a novice at this, but I've managed to add a few things to make it run onEdit, and to delete the initial trigger word.
An example of this slightly tweaked code running can be found below:
function onEdit() {
archiveRows();
clearCells();
}
function archiveRows()
{
/*installable trigger rows 10-13 https://developers.google.com/apps-script/guides/triggers/installable
if(!projectTriggerExists()
{
ScriptApp.newTrigger('myFunction')
.forSpreadsheet(ss)
.onOpen()
.create();
}//by wrapping the trigger creation like this you don't have to worry about creating unwanted triggers.
*/
var ss=SpreadsheetApp.getActive();
var sh0=ss.getSheetByName('Transfers'); //sh0 = Transfers tab
var rg0=sh0.getDataRange(); //rg0 = Range of sh0, This is functionally equivalent to creating a Range bounded by A1 and (Range.getLastColumn(), Range.getLastRow()). https://developers.google.com/apps-script/reference/spreadsheet/sheet#getdatarange
var sh1=ss.getSheetByName('Archive'); //sh1 = Archive tab
var vals=rg0.getValues(); //Returns the rectangular grid of values for this range. Returns a two-dimensional array of values, indexed by row, then by column. The values may be of type Number, Boolean, Date, or String, depending on the value of the cell. Empty cells will be represented by an empty string in the array. Remember that while a range index starts at 1, 1, the JavaScript array will be indexed from [0][0]. https://developers.google.com/apps-script/reference/spreadsheet/range#getvalues
for(var i=vals.length-1;i>11;i--) //When deleting rows it is better to start from the bottom otherwise deleted rows will mess up your loop indexing
{
if(vals[i][14]=='SEND') //If column 14 in the range has the word 'SEND' then run the next lines of code. 14 is column O. Column 0 is 15, but see note above regarding JavaScript array being indexed from [0][0]
{
sh1.appendRow(vals[i]);
sh0.deleteRow(i+1);//indexes start at zero but rows start at one
}
}
}
//This could be accomplished with SpreadsheetApp.getActive().getActiveSheet().clear();
function clearCells()
{
//https://stackoverflow.com/questions/9268570/i-need-a-button-to-clear-cells-in-a-google-spreadsheet
var ss=SpreadsheetApp.getActive();
ss.getRange('A1:A1').clearContent()
}
function projectTriggerExists(functionName)
{
if(functionName)
{
var allTriggers=ScriptApp.getProjectTriggers();
var funcExists=false;
for(var i=0;i<allTriggers.length;i++)
{
var trigger=allTriggers[i];
if(allTriggers[i].getHandlerFunction()==functionName)
{
funcExists=true;
break;
}
}
}
return funcExists;
}
The nature of the spreadsheet is that I will need to duplicate the "Transfers" tab in the original post a dozen times so that it can be used by multiple people at once (data being entered in by the user essentially over 12 "forms"). All the data put into these dozen "Transfers" tabs should rout to the same "Archive" tab. However, I'd also like to build in a rudimentary routing system, so that instead of having everything rout to one tab, different code words send the lines to different "Archive" tabs. For instance, "OK+Send+Staff1" sends the line to a tab called Staff1Archive, "OK+Send+Staff2" sends to a tab called Staff2Archive, "OK+Send+Staff3" sends to a tab called Staff3Archive, etc.
Can anyone help with this code as well?
I think this will do it for you.
function archiveRows()
{
var ss=SpreadsheetApp.getActive();
var sh0=ss.getSheetByName('Transfers');
var rg0=sh0.getDataRange();
var sh1=ss.getSheetByName('Archive');
var vals=rg0.getValues();
for(var i=vals.length-1;i>11;i--)
{
if(vals[i][14]=='SEND')
{
sh1.appendRow(vals[i]);
sh0.deleteRow(i+1)
}
}
}