Google SpreadsheetApp help needed - google-apps-script

This is a program designed to move data within 1 Spreadsheet containing a few Sheets
I don't know much about JavaScript I have only learned Java.
My Problem:
The addAcceptedHours method is designed to move a cell with data of a number, to a separate sheet (containing a list of names in the first column, columns 2-5 contain numbers). I need it to move the hours to a certain column of the row (the row is given from searchCol(String, Sheet)).
The var data is 2d array of the information submitted from a form looks like this:
[[(new Date(1339776313000)), "Firstname last", "email#email.com", 2015 , "A paragraph of text.", another paragraph of text", 00, "Freshman", "yet another paragraph of text", "Even moar text", "more text :3", "Firstname last"]]
0=timestamp
1=**firstname last**
2=email
3=** int(year graduating)**
4= paragraph of text (irrelevant to part)
5=** float (a number of hours)
6= "freshman", "sophomore", "junior", or "senior" (this will determine the column the number from 5 goes)**
7= paragraph of text (irrelevant to part)
8=paragraph of text (irrelevant to part)
9=paragraph of text (irrelevant to part)
10=paragraph of text (irrelevant to part)
11=a name onceagain
I need to add the number (5), to a row in another sheet in the column of 2-5 depending on (6)
for some reason it skips everything after the if statement in addAcceptedHours and adds the hours into the the 2nd column every time
//made with `enter code here`Google SpreadsheetApp Scripts
//PUBLIC VARIABLES
var data;
//please ignore missing methoods
function onOpen(){
var menu = [{name: 'Manage Hours', functionName: 'run'}];
SpreadsheetApp.getActive().addMenu('Manage Hours', menu);
}
//starts the program
function run(){
log('open');
getNext();
}
//opens window with options
function getNext(){
data = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Pending Hours').getSheetValues(2, 1, 1, 12);
var cont = Browser.msgBox('Manage Hours', 'Would you like to get the next submission?', Browser.Buttons.YES_NO);
if (cont == 'yes'){
{
var msg = data[0][1] + "\\nClass of " + data[0][3] + "\\n Submitted " + data[0][6] + " Hours for their " + data[0][7] + " Year." + "\\nDescription of Service:\\n" + data[0][4] + "\\nContact Information:\\n" + data[0][5] + "\\nRecommendation: " + data[0][8] + "\\n" + data[0][9] + "\\n\\nAccept?";
var ans = Browser.msgBox('Manage Hours', msg , Browser.Buttons.YES_NO_CANCEL);
log(ans);
}
if (ans == 'yes')
accept();
else if (ans == 'no')
deny();
else cancel();
}
else
log("close");
}
function accept(){
addAcceptedHours();//adds hours to accepted place
addChesedOpportunity();//adds to opportunitys
archive('yes');//archives the row
mailUser('yes');//mails user Accpeted
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Pending Hours').deleteRow(2);//deletes row for next use
getNext();//starts over
}
function addAcceptedHours() {
//get data
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Pending Hours');
var sss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Accepted Hours');
var numhr = parseFloat(ss.getRange(2, 7).getValue().toString());
//asks how many hours to accept
var hrs = Browser.inputBox("how many hours will you accept (out of " + numhr + " hours)", Browser.Buttons.OK);
if (hrs.length > 0){
numhr = parseFloat(hrs);
ss.getRange(2, 7).setValue(numhr);}
//finds the row with the students name to add hours to
var name = data[0][1];
var row = searchCol(name,sss);
if (row == -1)// if not found, look to see if simaler names (check for misspelled names), then if the name is the same person set row to that row
row = checkForSim(name);
if (row == -1) {//if not misspelled then add the students name to the list, put it in order, get the row #
addStudent(name);
sss.sort(1);
row = searchCol(name,sss);}
//finds wich column to add years to
var col = 2;
if (data[0][8] == 'Sophmore')
col=3;
else if (data[0][8] == 'Junior')
col=4;
else if (data[0][8] == 'Seinor')
col=5;
//sets info for hours
sss.getRange(row, col).setValue(parseFloat(sss.getRange(row, col).getValue()) + numhr);
}
function searchCol(str, ss){//returns the row "str" is found in the first column of the sheet("ss")
var data = ss.getRange(1, 1, ss.getLastRow()+1, 1).getValues();
var found = false;
for (var i=0; i < data.length;i++){
if (data[i].toString().equalsIgnoreCase(str)){
found = true;
return i+1;}}
return -1;
}
function checkForSim(name){//looks for simaler names, and suggestes them, if they say it is the same person, return that row #
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Accepted Hours');
var count =0;
for (var x=2; x < ss.getLastRow(); x++){
for (var i=0; i<ss.getRange(x, 1).getValue().toString().length - 2; i++){
if (ss.getRange(x, 1).getValue().toString().substr(i,i+2) == name.substr(i,i+2))
count++;}
if (count > 0 && Browser.msgBox('Is this the same person?', ss.getRange(x, 1).getValue().toString() + '\nAnd,\n' + name , Browser.Buttons.YES_NO) == 'yes')
return x;
count =0;}
return -1;
}
function addStudent(name){
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Accepted Hours');
var row = ss.getLastRow();
ss.getRange(row, 1).setValue(name);
for (var x = 2; x<5;x++){
ss.getRange(row, x).setValue(0);
}
ss.sort(1);
}

fixed:
function addAcceptedHours() {
//get data
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Pending Hours');
var sss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Accepted Hours');
var numhr = parseFloat(ss.getRange(2, 7).getValue().toString());
//asks how many hours to accept
var hrs = Browser.inputBox("how many hours will you accept (out of " + numhr + " hours)", Browser.Buttons.OK);
if (hrs.length > 0){
numhr = parseFloat(hrs);
ss.getRange(2, 7).setValue(numhr);}
data[0][6]=numhr;
//finds the row with the students name to add hours to
var name = data[0][1];
var row = searchCol(name,sss);
if (row == -1)// if not found, look to see if simaler names (check for misspelled names), then if the name is the same person set row to that row
row = checkForSim(name);
if (row == -1) {//if not misspelled then add the students name to the list, put it in order, get the row #
addStudent(name);
sss.sort(1);
row = searchCol(name,sss);}
//finds wich column to add years to
var col = 2;
if (data[0][7].toString() == 'Freshman')
col=2;
else {
if (data[0][7].toString() == 'Sophmore')
col=3;
else{
if (data[0][7].toString() == 'Junior')
col=4;
else{
if (data[0][7].toString() == 'Senior')
col=5;
}}}
sss.getRange(row, col).setValue(parseFloat(sss.getRange(row, col).getValue()) + numhr);
}
function addChesedOpportunity(){
//gets info
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Chesed Opportunities')
var private = data[0][11];
//if private? is not awnsered, ask if they want it private
if (private==null)
private = Browser.msgBox("Would you like to make this information Private?",'Name:\\n' + data[0][2] + '\\nDescription of Service:' + data[0][5] + '\\nReccomends:' + data[0][9] + ' ' + data[0][10], Browser.Buttons.YES_NO);
//adds opportunity to oportunity sheet if the info is not private
if (private.equalsIgnoreCase('no')){
var rec = data[0][9] + " " + data[0][10];
var row = ss.getLastRow();
ss.getRange(row, 1).setValue(data[0][2]);
ss.getRange(row, 2).setValue(data[0][5]);
ss.getRange(row, 3).setValue(rec);
ss.getRange(row, 4).setValue(data[0][6]);}
}

Related

How do I automate sending gmails row by row when the cells in a specific column have either a yes or no entered

function findTextsendupdate(){
var sheet = SpreadsheetApp.getActiveSheet()
var startRow = 3// First row of data to process
var numRows = 50 // Number of rows to process
// if a yes is entered in column M
var sendUpdateRange = sheet.getRange(startRow, 1, numRows, 14);
// Fetch values for each row in the Range.
var yes = "yes"
var no = "no"
var send = sendUpdateRange.createTextFinder(yes);
send.matchCase(false); //{Boolean} -> match target text's case or not;
send.matchEntireCell(true); //{Boolean} -> check the whole Range or within;
send.ignoreDiacritics(true); //{Boolean} -> ignore diacretic signs during match;
send.matchFormulaText(true); //{Boolean} -> search in formulas (if any) or values;
var dontSend = sendUpdateRange.createTextFinder(no)
dontSend.matchCase(false); //{Boolean} -> match target text's case or not;
dontSend.matchEntireCell(true); //{Boolean} -> check the whole Range or within;
dontSend.ignoreDiacritics(true); //{Boolean} -> ignore diacretic signs during match;
dontSend.matchFormulaText(true); //{Boolean} -> search in formulas (if any) or values;
//invoke search;
var res = send.findNext();
var tes = dontSend.findNext();
{ //do something with result;
if (tes) {
var emailNotSent = "Email Not Sent"
sheet.getRange(startRow, 14, numRows, 1).setValue(emailNotSent)}
if(res) {
var sendEmail = sendUpdateRange.getValues();
var emailSent = "Email Sent";
for (var i = 0; i < sendEmail.length; i++) {
var row = sendEmail[i];
var Name = row[1]
var job = row[2]
var model = row[4]
var info = "Job number: " + job + "\n"
var emailAddress = row[0];
var isEmailSent = row[14]
if (isEmailSent != emailSent && no) {
var message = "Dear " + Name + "\n" + "
var body = info + "\n" + message
var subject = "email";
MailApp.sendEmail(emailAddress, subject, body);
sheet.getRange(startRow, 14, numRows, 1).setValue(emailSent);
} }}}}
I have dedicated column M for the edit trigger. But, every time I enter a yes or a no in the first row, it sends the emails for all the other rows. If I put no on the first row, and yes on the send, it changed from not sending to sending all emails whereas if I put yes on the first row and no on the second, it flips between sending and not sending all emails.
I want to make the script look at column M and decide on a row by row basis on whether to send or not to send an email. This is the part I'm having trouble with and I've exhausted my search options.
Assuming everything you had in there works then this might be close.
function findTextsendupdate(e){
const sh=e.range.getSheet();
if(sh.getName()=="My Email Sheet" && e.range.columnStart==13 && e.value=="TRUE") {
const row=sh.getRange(e.range.rowStart,1,1,sh.getLastColumn()).getValues()[0];
var Name = row[1];
var job = row[2];
var model = row[4];
var info = "Job number: " + job + "\n";
var emailAddress = row[0];
var isEmailSent = row[14];
if (isEmailSent != "EmailSent" && MailApp.getRemainingDailyQuota()>0) {
var message = "Dear " + Name + "\n";
var body = info + "\n" + message;
var subject = "email";
MailApp.sendEmail(emailAddress, subject, body);
sh.getRange(e.range.rowStart,14).setValue('EmailSent');
}
}
}

How to send several rows of google sheet table via email if cell match today's date

daily I have to send different qty of google sheet table rows (it depends on how many trucks were departed) via e-mail to a few recipients.
Could you help me, please, with an example of how to collect several rows of google table if cell match today's date and then send it via email?
For example, I need to grab all rows with today's date and send data from columns A, B, C, E via e-mail.
Thanks for any help in advance :)
Let's go step by step. I'm supposing the column A contains the dates. Change it if it's not correct:
Declare the variables we will use for this
function main() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = sheet.getActiveSheet();
var row_count = sheet.getLastRow() - 1;
var today = Utilities.formatDate(new Date(), "UTC", "dd.MM.yyyy"); //You can use the date format you prefer
var data = sheet.getRange("A2:E"+ (row_count + 1)).getValues(); //We get all the values from A to E and from row 2 to the last one.
countToday(sheet, today, row_count, data);
sendEmails(data);
}
Since we have each row in each position of data, we will compare
each date with "today", and we will remove (splice) the rows from
other days. The rows with valid dates will remain in the variable:
function countToday(sheet,today, row, data){
var lastrow = "A" + (row + 1);
var col_A = sheet.getRange('A2:'+lastrow).getValues();
for (var i = row; i >= 0; i--){
if (col_A[i] != today){
data.splice(i, 1); //
}
}
}
Now that we got each row of elements, we can send the email. We will create an html table so it will be easier to understand. The Mailapp function makes it very simple:
function sendEmails(data){
MailApp.sendEmail({
to: "example1#mail.com" + "example2#mail.com", //You can put as many emails you want
subject: "Example",
htmlBody:"<html><body>" + createTable(data)+ "</body></html>"});
}
To create the html table, we just make a string coded in html with
the names of the columns from the Sheet. Then we make a table array,
where we will split each element from data separated by coma (that's each sheet cell) and just add it to the end of the variable. < /td> will create the rows for us.
function createTable(data){
var cells = [];
var table = "<html><body><br><table border=1><tr><th>Date</th><th>Column B</th><th>Column C</th><th>Column D</th><th>Column E</tr></br>";
for (var i = 0; i < data.length; i++){
cells = data[i].toString().split(",");
table = table + "<tr></tr>";
for (var u = 0; u < cells.length; u++){
table = table + "<td>"+ cells[u] +"</td>";
}
}
table=table+"</table></body></html>";
return table;
}
Here is my example of code. But I can't figure out how to implement preventing function for duplicating emails sending. EMAIL_SENT is working only with the "for" cycle. But in that case, if the cell doesn't contain "EMAIL_SENT" string, the email is sending as many times as there are the empty cells.
function main() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = sheet.getSheetByName("HR-SI B2C");
var row_count = sheet.getLastRow() - 1;
var today = Utilities.formatDate(new Date(), "UTC", "dd/MM/yyyy"); //You can use the
date format you prefer
var data = sheet.getRange("A3:K"+ (row_count + 1)).getValues(); //We get all the
values from A to E and from row 2 to the last one.
var lastrow = "A" + (row_count + 1);
var col_A = sheet.getRange('A3:'+lastrow).getDisplayValues();
var row = data[i];
for (var i = row_count; i >= 0; i--){
if (col_A[i] != today){
data.splice(i, 1);
}
}
if (("K" + (row_count+2)) == "EMAIL_SENT"){
}else if(("D" + (row_count)) == "" || ("F" + (row_count)) == ""){
dontSendEmail();
}else{
sendEmailsQHNBRGR(data);
}
}
//-----------------------------------------------------------------------------------
function dontSendEmail(){
var today = Utilities.formatDate(new Date(), "UTC", "dd/MM/yyyy"); //You can use the
date format you prefer
MailApp.sendEmail({
to: "xxxxxxxxxx#gmail.com", //You can put as many emails you want
subject: "Brak wpisanych ilości dla HR i SI za " + today,
htmlBody:"<html><body> Witam,<br><br>" +
"<span><b>Proszę bardzo wpisać ilości dla HR i SI do tabeli</b></span>" +
"</body></html>"});
}
//-----------------------------------------------------------------------------------
function sendEmailsQHNBRGR(data, platesNum){
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = sheet.getSheetByName("HR-SI B2C");
var row_count = sheet.getLastRow() - 1;
var today = Utilities.formatDate(new Date(), "UTC", "dd/MM/yyyy"); //You can use the date format you prefer
var data = sheet.getRange("A3:G"+ (row_count + 1)).getDisplayValues(); //We get all the values from A to D and from row 2 to the last one.
var cells = [];
var table = "<html><body><table border=3><tr><th>Date</th><th>Day</th><th>HR [pcs]</th><th>HR [ep]</th><th>SI [pcs]</th><th>SI [ep]</th></tr>";
var lastrow = "A" + (row_count + 1);
var col_A = sheet.getRange('A3:'+lastrow).getDisplayValues();
MailApp.sendEmail({
to: "xxxxxxx#gmail.com", //You can put as many emails you want
subject: "(HR+SI) Orsay prealert " + today,
htmlBody:"<html><body> Dear All,<br><br>" +
"<span style=background-color:rgb(217,234,211)><b>Today we've sent e-com goods in listed quantity:</b></span>" +
createTable(data) + "<br>" +
"<span style=background-color:rgb(255,242,204)><b>#DPD team, could you pick it up
tomorrow at 02:00 PM?</b></span><br>" +
"</body></html>"});
}
//-----------------------------------------------------------------------------------
function createTable(data){
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = sheet.getSheetByName("HR-SI B2C");
var row_count = sheet.getLastRow() - 1;
var today = Utilities.formatDate(new Date(), "UTC", "dd/MM/yyyy"); //You can use the date format you prefer
var data = sheet.getRange("A3:G"+ (row_count + 1)).getDisplayValues(); //We get all the values from A to D and from row 2 to the last one.
var cells = [];
var table = "<html><body><table border=3><tr><th>Date</th><th>Day</th>
<th>Plates number</th><th>HR [pcs]</th><th>HR [ep]</th><th>SI [pcs]</th>
<th>SI [ep]</th></tr>";
var lastrow = "A" + (row_count + 1);
var col_A = sheet.getRange('A3:'+lastrow).getDisplayValues();
for (var i = row_count; i >= 0; i--){
if (col_A[i] != today){
data.splice(i, 1);
}
}
for (var i = 0; i < data.length; i++){
cells = data[i].toString().split(",");
table = table + "<tr></tr>";
for (var u = 0; u < cells.length; u++){
table = table + "<td>"+ cells[u] +"</td>";
sheet.getRange("K3:K"+ (row_count + 1)).setValue("EMAIL_SENT");
}
}
table=table+"</table></body></html>";
return table;
}
}

compare two spreadsheet and output the difference using google app scripts

well, i'm trying to do what described in title. Both spreadsheets have only one sheet that are the ones i'm comparing. One spreadsheet is and update of the other, so i'm trying to get only new content. (if it were a fc (dos command) like function this would be easy...)
After doing some search, i have the folloing script that should work on most cases, that uses arrays for each sheet.
function test() {
var Folder = DriveApp.getFoldersByName('theFolder').next();
var FolderId =Folder.getId();
//call old_spreadsheet
var searchFor ="fullText contains 'sheet_old' and '" + FolderId + "' in parents";
var files = DriveApp.searchFiles(searchFor);
var old_file = files.next();
var old_spreadsheet = SpreadsheetApp.openById(old_file.getId());
var old_sheet = old_spreadsheet.getSheets()[0];
var old_sheetname = old_sheet.getName();
var old_array = old_sheet.getDataRange().getValues();
Logger.log(old_file.getName() + ' : ' + old_sheetname + ' : ' + old_array.length);
//call spreadsheet
var searchFor ="fullText contains 'sheet' and '" + FolderId + "' in parents";
var files = DriveApp.searchFiles(searchFor);
var file = files.next();
var spreadsheet = SpreadsheetApp.openById(file.getId());
var sheet = spreadsheet.getSheets()[0];
var sheetname = sheet.getName();
var array = sheet.getDataRange().getValues();
Logger.log(file.getName() + ' : ' + sheetname + ' : ' + array.length);
var newarray = getNewData(array,old_array);
Logger.log('there are ' + newarray.length + 'different rows');
}
function getNewData(array1,array2){
var diff =array2;
for (var i = 0; i<array1.length; i++){
var duplicate = false;
for (var j = 0;j<diff.length;j++){
if (array1[i].join() == diff[j].join()){
Logger.log('duplicated line found on rows ' + i + ':' + j);
diff.splice(j,1);
var duplicate= true;
break;
}
}
if (duplicate==false) {
Logger.log('not duplicated line found on row ' + i);
diff.push(array1[i]);
}
}
return diff;
}
The thing is that the files are too big, almost 30000 rows, so the scripts exceed 5 minutes limit for execution.
Is there a way to improve this, like for instance, eliminate the inner for loop?
Or there is a way to do it in parts? like first the first 5000 rows, and so on.
Regards,
EDIT: after analizing the spreadsheet a little, i found out that there is a ID for every row, so now i can concentrate the search only in one column of each spreadsheet. So here is my new implementation:
function test(){
var Folder = DriveApp.getFoldersByName('theFolder').next();
var FolderId =Folder.getId();
//call old_spreadsheet
var searchFor ="fullText contains 'sheet_old' and '" + FolderId + "' in parents";
var files = DriveApp.searchFiles(searchFor);
var old_file = files.next();
var old_spreadsheet = SpreadsheetApp.openById(old_file.getId());
var old_sheet = old_spreadsheet.getSheets()[0];
var old_sheetname = old_sheet.getName();
var old_array = old_sheet.getDataRange().getValues();
Logger.log(old_file.getName() + ' : ' + old_sheetname + ' : ' + old_array.length);
//call spreadsheet
var searchFor ="fullText contains 'sheet' and '" + FolderId + "' in parents";
var files = DriveApp.searchFiles(searchFor);
var file = files.next();
var spreadsheet = SpreadsheetApp.openById(file.getId());
var sheet = spreadsheet.getSheets()[0];
var sheetname = sheet.getName();
var array = sheet.getDataRange().getValues();
Logger.log(file.getName() + ' : ' + sheetname + ' : ' + array.length);
//The COlumn has an indicator, so i search for that. I don't control the formatting of the files, so i search in both spreadsheet for the indicator
var searchString = 'NAME';
for (var i = 0; i < old_array.length; i++) {
for (var j = 0; j < old_array[i].length; j++) {
if (old_array[i][j] == searchString) {
var Row_old = i+1;
var Column_old = j;
break;
}
}
if (Row_old != undefined){
break;
}
}
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < array[i].length; j++) {
if (array[i][j] == searchString) {
var Row = i+1;
var Column = j;
break;
}
}
if (Row != undefined){
break;
}
}
Logger.log(Row_old+':::'+Column_old+'\n'+Row+':::'+Column);
var diff_index =[];
var row_ind = 0;
for (var i=Row;i<array.length;i++){
Logger.log(i);
var existe = ArrayLib.indexOf(old_array, Column_old, array[i][Column]);
if (existe==-1){
Logger.log(row_ind+'!!!');
diff_index[row_ind]=i;
row_ind++;
}
}
Logger.log(diff_index);
}
This still run out of time... I will now try to incorporate your comments.
Your script has a few major bottlenecks that slow it down massively:
Starting both loops at 0 every time makes its runtime explode
splicing every time you find a duplicate requires to move the array around
string concatenating an array on every iteration
We can circumvent these issues by:
sorting the second range once
I'm sure there's something clever to be done by iteratively binary searching through every column but we'd have to resort every time so we'll binary search the first column and then do a linear search.
We will use ArrayLib for the sorting (I hope it's a fast sorting algorithm).
Let's start with a function to find the first row where the first column matches a value (the first column of the current row):
function firstRowMatchingCol1(target, lookupRange) {
var min = 0;
var max = lookupRange.length - 1;
var guess;
var guessVal;
while(min <= max) {
guess = (min + max) / 2 | 0;
guessVal = lookupRange[guess][0];
if (guessVal < target) {
min = guess + 1;
} else if (guessVal > target) {
max = guess - 1;
} else {
while (guess > 0 && lookupRange[guess - 1][0] === target) {
guess -= 1;
}
return guess;
}
}
return -1;
}
Now we can go linearly go through every row and check if the columns match until the first column doesn't match anymore.
function matchExists(row, lookupRange) {
var index = firstRowMatchingCol1(row[0], lookupRange);
if (index === -1) {return false;}
while (index < lookupRange.length && lookupRange[index][0] === row[0]) {
for (var col = 1; col < row.length; col++) {
if (row[col] !== lookupRange[index][col]) {break;}
if (col === row.length - 1) {return true;} // This only works if the ranges are at least two columns wide but if they are one column wide you can just check if index > -1
}
index += 1;
}
return false;
}
And finally we can get the duplicates like this:
function getNonDuplicates(r1, r2) {
r2 = ArrayLib.sort(r2, 0, true);
return r1.filter(function(row) {return !matchExists(row, r2);});
}
Like mTorres' code this is untested
The solution I'm proposing is a "hack" around the time limit. But if you want a cleaner solution, you could, if possible, reorganize and make your code more efficient by having the arrays ordered somehow.
You don't specify the data inside array1 and array2, if rows had some sort of ID field you could order by this ID and check row i on array1 and row i on array2 instead of comparing every row in array1 with every row in array2 (which is extremely inefficient with 30000 rows).
If your data does not have an ID field to order the rows, then what you could is something based on my proposed solution: add a track for every compared row on array1. When the run reaches the time limit then you run again the function but starting from the last compared row (you would know which was because you'll be tracking the compared rows), and when the second run times out you repeat, and so on.
Every time you run your comparison you ask if it's the first run (or use a boolean - I prefer to ask the user, this way you won't forget to change the boolean), if it's the first run, you delete the tracking
column, if it's not the first run, you'll start with the next to last tracked row so basically continuing your script where it ended. I've been using this technique with good results.
In code (untested, so check it out before running it with real data):
/**
* Only checks if it's the first run and calls the real work function
*/
function test() {
var firstRun = "yes" === Browser.msgBox("Question", "Is this the first run?", Browser.Buttons.YES_NO);
doTest(firstRun);
}
/**
* Gets the data of the 2 spreadsheets and also the starting
* row
*/
function doTest(firstRun) {
var Folder = DriveApp.getFoldersByName('theFolder').next();
var FolderId = Folder.getId();
//call old_spreadsheet
var searchFor ="fullText contains 'sheet_old' and '" + FolderId + "' in parents";
var files = DriveApp.searchFiles(searchFor);
var old_file = files.next();
var old_spreadsheet = SpreadsheetApp.openById(old_file.getId());
var old_sheet = old_spreadsheet.getSheets()[0];
var old_sheetname = old_sheet.getName();
var old_array = old_sheet.getDataRange().getValues();
/**
* Here is the code to create the tracking hability
*/
var strartFromRow = 0; // 0 because row 1 is array 0 index when you getValues();
var trackSheet = old_spreadsheet.getSheetByName("Tracking");
if (trackSheet === null) {
trackSheet = old_spreadsheet.insertSheet("Tracking");
}
if (firstRun) {
trackSheet.getRange("A:A").clearContent(); // make sure there no row is tracked yet
}
else {
// we have to continue from the previous row, keep in mind you're making the comparison
// with array which is 0 based, but sheet is 1 based, but you want the next one so getLasRow()
// should be the first item to compare on your array
strartFromRow = trackSheet.getLastRow();
}
Logger.log(old_file.getName() + ' : ' + old_sheetname + ' : ' + old_array.length);
//call spreadsheet
var searchFor ="fullText contains 'sheet' and '" + FolderId + "' in parents";
var files = DriveApp.searchFiles(searchFor);
var file = files.next();
var spreadsheet = SpreadsheetApp.openById(file.getId());
var sheet = spreadsheet.getSheets()[0];
var sheetname = sheet.getName();
var array = sheet.getDataRange().getValues();
Logger.log(file.getName() + ' : ' + sheetname + ' : ' + array.length);
// when you call the DIFF function, pass the tracking sheet and the start Row
var newarray = getNewData(array,old_array, trackSheet, startFromRow);
Logger.log('there are ' + newarray.length + 'different rows');
}
/**
* Creates a diff array using array1 and array2
* It marks each element on array1 once it has checked if it's in array2
*/
function getNewData(array1, array2, trackingSheet, startFromRow){
var logRow = trackingSheet.getLastRow();
var diff = array2;
for (var i = startFromRow; i < array1.length; i++){
var duplicate = false;
for (var j = 0; j < diff.length;j++){
if (array1[i].join() == diff[j].join()){
Logger.log('duplicated line found on rows ' + i + ':' + j);
diff.splice(j,1);
duplicate = true;
break;
}
}
if (duplicate === false) {
Logger.log('not duplicated line found on row ' + i);
diff.push(array1[i]);
}
trackingSheet.getRange(logRow++, 1).setValue("Checked!"); // Mark i row as checked
}
return diff;
}
Here's an alternate solution that gets around the time limit. Create a new dedicated spreadsheet along with a custom sidebar. The sidebar will require you to create some HTML that will ultimately be embedded and rendered in an iframe on the client. You can embed pure javascript into the HTML via script tags.
The beauty of this approach is that these scripts will not run server-side but on the client independently of Google Apps Script's server-side environment and are not subject to the 6 minute limit. Moreover, they can also call functions in your Google Script. So one approach would be to have the client-side scripts call a Google Script function to retrieve the requisite data, do all the heavy processing in the client-side scripts, and then send the results back to the server-side script to update the sheet.
Here's a link to setting up a custom sidebar to get you started:
https://developers.google.com/apps-script/guides/dialogs#custom_sidebars
Finally, i decided to go for the Cache service option, here is the code and i'm testing it to see if i keep with this.
function getNewData() {
//deleting triggers
var triggers = ScriptApp.getProjectTriggers();
for (var i = 0; i < triggers.length; i++) {
if (triggers[i].getHandlerFunction()=='getNewData'){
ScriptApp.deleteTrigger(triggers[i]);
}
}
//max running time = 5.5 min
var MAX_RUNNING_TIME = 330000;
var startTime= (new Date()).getTime();
//get cache
var cache = CacheService.getUserCache();
var downloaded =JSON.parse(cache.get('downloaded'));
var compared =JSON.parse(cache.get('compared'));
//start
if (downloaded==1 && compared!=1){
//folder
var Folder = DriveApp.getFoldersByName('theFolder').next();
var FolderId = licitacionesFolder.getId();
//call old_spreadsheet
var searchFor ="fullText contains 'sheet_old' and '" + FolderId + "' in parents";
var files = DriveApp.searchFiles(searchFor);
var old_file = files.next();
var old_spreadsheet = SpreadsheetApp.openById(old_file.getId());
var old_sheet = old_spreadsheet.getSheets()[0];
var old_array = old_sheet.getDataRange().getValues();
//call spreadsheet
var searchFor ="fullText contains 'sheet' and '" + FolderId + "' in parents";
var files = DriveApp.searchFiles(searchFor);
var file = files.next();
var spreadsheet = SpreadsheetApp.openById(old_file.getId());
var sheet = spreadsheet.getSheets()[0];
var array = sheet.getDataRange().getValues();
Logger.log(array.length+'::'+old_array.length);
// Column
var searchString = 'NAME';
var RC = getColumn(array,searchString);
var Row = RC.Row;
var Column = RC.Column;
var RC = getColumn(old_array,searchString);
var Row_old = RC.Row;
var Column_old = RC.Column;
Logger.log(Row_old+':::'+Column_old+'\n'+Row+':::'+Column);
//compare
var diff_index =JSON.parse(cache.get('diff_index'));
var row_ind =JSON.parse(cache.get('row_ind'));
var Roww =JSON.parse(cache.get('Row'));
if (diff_index==null){var diff_index = [];}
if (row_ind==null){var row_ind = 0;}
if (Roww==null){var Roww = Row;}
Logger.log(row_ind+'\n'+Roww);
for (var i=Roww;i<array.length;i++){
var currTime = (new Date()).getTime();
if(currTime - startTime >= MAX_RUNNING_TIME){
Logger.log((currTime - startTime)/(1000*60));
Logger.log(i+'::'+row_ind);
cache.putAll({'diff_index': JSON.stringify(diff_index),'row_ind': JSON.stringify(row_ind),'Row': JSON.stringify(i-1)},21600);
ScriptApp.newTrigger('getNewData').timeBased().after(2 * 60 * 1000).create();
return;
} else {
Logger.log(i);
var existe = ArrayLib.indexOf(old_array, Column_old, array[i][Column]);
if (existe==-1){
Logger.log(row_ind+'!!!');
diff_index[row_ind]=i;
row_ind++;
}
}
}
cache.putAll({'diff_index': JSON.stringify(diff_index),'Row': JSON.stringify(Row),'compared': JSON.stringify(1)},21600);
} else {
Logger.log('file not downloaded yet or already compared');
}
}
function getColumn(array,searchString){
for (var i = 0; i < array.length; i++) {
for (var j = 0; j < array[i].length; j++) {
if (array[i][j] == searchString) {
var Row = i+1;
var Column = j;
break;
}
}
if (Row != undefined){
break;
}
}
return {Row: Row, Column: Column};
}

Need to get email notification if column in Google Spreadsheet is changed

I am trying to create a script that triggers a notification whenever a value is changed in a specific column of a Google Spreadsheet. Ideally, I would like to trigger notifications to other people, each based on changes to specific columns.
I have a test spreadsheet here:
https://docs.google.com/spreadsheets/d/1V4X1FNtYKbXhha84MzeU8kI57ck246WfvSluHlsP1eo/edit?usp=sharing
And have found a script for a custom notification elsewhere in the answers on SO. I took that and tweaked it until I got this:
function sendNotification() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var cell = ss.getActiveCell().getA1Notation();
var row = sheet.getActiveRange().getRow();
var column = sheet.getActiveRange().getColumn();
var colLetter = columnToLetter(column);
var cellvalue = ss.getActiveCell().getValue().toString();
var recipients = "email#domain.org";
var message = '';
if(cell.indexOf('G')!=-1){
message = sheet.getRange('F'+ sheet.getActiveCell().getRowIndex()).getValue()
}
var subject = 'Update to Notification TEST Sheet';
var body = 'Sheet ' +sheet.getName() + ' has been updated. Visit ' + ss.getUrl() + ' View the changes in row ' + row + ', column ' +colLetter+ '.';
MailApp.sendEmail(recipients, subject, body);
};
function columnToLetter(column) {
var temp, letter = '';
while (column > 0)
{
temp = (column - 1) % 26;
letter = String.fromCharCode(temp + 65) + letter;
column = (column - temp - 1) / 26;
}
return letter;
}
function letterToColumn(letter)
{
var column = 0, length = letter.length;
for (var i = 0; i < length; i++)
{
column += (letter.charCodeAt(i) - 64) * Math.pow(26, length - i - 1);
}
return column;
};
Once you change the receipient email, the script runs, but sends a notification for any change in any cell, not for cells with a specific column.
Can anyone help me get it to do what I am looking for?
TIA!
Imagine your trigger event is, onEdit(e)
function onEdit(e)
{
var range = e.range;
var column = range.getColumn();
if(column == `your expected column number`)
{
// call your send notification function
sendNotification();
}
}
You can read about triggers more. Event Objects

How to update an old spreadsheets with a new one, adding only new elements and changing color of subtracted elements?

I have a problem similar to this: Imagine I have a Google Sheet (eg, with students info) which I get downloading from a school site (imagine the school system is very bad, so it's more useful to use a sheet with my Google Scripts). But I have to weekly update this sheet downloading a new sheet from school site. To not lose my previous notes, I want to write a script that makes the update process, this way:
- If the most recent sheet has a new row which isn't in the previous sheet (a new student row), it add this row to the new sheet;
- If the old sheet has a row which isn't in the new sheet, it change the color of that row and adds a note "Transferred student" or something like that.
I get this sample code, but I'm wonder if there isn't a better way to do this. This is my code:
function updateSheet(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSpreadsheetID = ss.getId();
var oldSpreadsheet = SpreadsheetApp.openById(sourceSpreadsheetID);
var oldWorksheet = oldSpreadsheet.getSheetByName("students");
var oldData = oldWorksheet.getDataRange().getValues();
var newSpreadsheetUrl = Browser.inputBox("New sheet", "Put the new sheet link here:", Browser.Buttons.OK_CANCEL);
Logger.log("newSpreadsheetUrl = " + newSpreadsheetUrl);
var newSpreadsheetID = newSpreadsheetUrl.split('d/')[1].split('/')[0];
var newSpreadsheet = SpreadsheetApp.openById(newSpreadsheetID);
var newWorksheet = newSpreadsheet.getSheetByName("students");
var newData = newWorksheet.getDataRange().getValues();
// Iterates through the new sheet rows
for(i=1; i<newData.length; i++){
var alreadyInOldSheet = false;
var remainsInNewSheet = false;
Logger.log("newData[i][0] = " + newData[i][0]);
// Iterates through the old sheet rows
for(j=1; j<oldData.length; j++){
// compares the firs cell (student name)
Logger.log("oldData[j][0] = " + oldData[j][0]);
Logger.log("newData[i][0] == oldData[j][0] = " + (newData[i][0] == oldData[j][0]) );
if (newData[i][0] == oldData[j][0]) {
alreadyInOldSheet = true;
break; // This student is already in the old sheet, so, jump to the next row
}
// After iterates through all old sheet rows, the student name isn't found, so we add it
}
Logger.log("Last condition 'alreadyInOldSheet =' " + alreadyInOldSheet);
Logger.log("newData =' " + newData);
if (alreadyInOldSheet == false) {
oldWorksheet.appendRow(newData[i]);
}
}
absentInNewSheet(oldData, newData, oldWorksheet);
}
function absentInNewSheet(oldData, newData, workSheet) {
for(i=1; i<newData.length; i++){
var alreadyInOldSheet = false;
Logger.log("newData[i][0] = " + newData[i][0]);
// Iterates through the old sheet rows
for(j=1; j<oldData.length; j++){
// compares the firs cell (student name)
Logger.log("oldData[j][0] = " + oldData[j][0]);
Logger.log("newData[i][0] == oldData[j][0] = " + (newData[i][0] == oldData[j][0]) );
if (newData[i][0] == oldData[j][0]) {
alreadyInOldSheet = true;
break; // This student is already in the old sheet, so, jump to the next row
}
// After iterates through all old sheet rows, the student name isn't found, so we add it
}
Example Code
function updateSheet()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var masterSheet = ss.getSheetByName("Master");
var masterRange = masterSheet.getDataRange();
var masterData = masterRange.getValues();
masterData.shift();
var masterBackgrounds = masterRange.getBackgrounds();
var masterNotes = masterRange.getNotes();
var masterLength = masterNotes.length;
var updateData = ss.getSheetByName("Update").getDataRange().getValues();
updateData.shift();
for (var i = 0; i < (masterLength - 1); i++)
{
masterData[i].unshift(i + 1);
}
masterData.sort(function(a,b) {return (a[1] > b[1]) ? 1 : ((a[1] < b[1]) ? -1 : 0 );});
updateData.sort(function(a,b) {return (a[0] > b[0]) ? 1 : ((a[0] < b[0]) ? -1 : 0 );});
var addedData = [];
while (masterData.length || updateData.length)
{
if (!masterData.length || (updateData.length && masterData[0][1] > updateData[0][0]))
{
addedData.push(updateData.splice(0, 1)[0]);
}
else if (!updateData.length || (masterData.length && masterData[0][1] < updateData[0][0]))
{
for (var k = 0; k < masterBackgrounds[0].length; k++)
{
masterBackgrounds[masterData[0][0]][k] = "#dcdcdc";
}
masterNotes[masterData[0][0]][0] = "This student was transfered from this school";
masterData.shift();
}
else
{
masterData.shift();
updateData.shift();
}
}
var extraRows = masterLength + addedData.length - masterSheet.getMaxRows();
if (extraRows > 0) masterSheet.insertRowsAfter(masterLength, extraRows);
if (addedData.length > 0) masterSheet.getRange(masterLength + 1, 1, addedData.length, addedData[0].length).setValues(addedData);
masterRange.setBackgrounds(masterBackgrounds).setNotes(masterNotes);
}
Test spreadsheet (feel free to try out - hopefully you can adjust this to your specific spreadsheets)
Thanks for re-posting; I just happened to be helped with a similar problem I posted when the Google Apps Script forum was over at GPF (I can't find that thread at all, but credit to +ScampMichael for helping me out; I have fine-tuned the script a bit since).
In general, the algorithm is to sort both the master and update arrays, and work through each array simultaneously, comparing the first element of each and performing some action then, including shift()-ing that element off each array when you're done with it. Before sorting the master array, in your case you would need to also append an index column so you know which cells to apply the note/background to - which with this method you do in one batch set for each, which should also improve performance.
Anyway, I'm not suggesting there aren't even more efficient algorithms, but when I had thousands of rows in my situation, it took about 8 seconds, while the "multiple looping" method would often time out. I hope it helps.
This is my actual code to perform this task. Maybe it isn't the more efficient solution (any suggestions?) but it is working for this problem.
function updateSheet(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceSpreadsheetID = ss.getId();
var oldSpreadsheet = SpreadsheetApp.openById(sourceSpreadsheetID);
var oldWorksheet = oldSpreadsheet.getSheetByName("students");
var oldData = oldWorksheet.getDataRange().getValues();
var newSpreadsheetUrl = Browser.inputBox("New sheet", "Put the new sheet link here:", Browser.Buttons.OK_CANCEL);
// Logger.log("newSpreadsheetUrl = " + newSpreadsheetUrl);
var newSpreadsheetID = newSpreadsheetUrl.split('d/')[1].split('/')[0];
var newSpreadsheet = SpreadsheetApp.openById(newSpreadsheetID);
var newWorksheet = newSpreadsheet.getSheetByName("students");
var newData = newWorksheet.getDataRange().getValues();
// Iterates through the new sheet rows
for(i=1; i<newData.length; i++){
var alreadyInOldSheet = false;
var remainsInNewSheet = false;
// Logger.log("newData[i][0] = " + newData[i][0]);
// Iterates through the old sheet rows
for(j=1; j<oldData.length; j++){
// compares the firs cell (student name)
// Logger.log("oldData[j][0] = " + oldData[j][0]);
// Logger.log("newData[i][0] == oldData[j][0] = " + (newData[i][0] == oldData[j][0]) );
if (newData[i][0] == oldData[j][0]) {
alreadyInOldSheet = true;
break; // This student is already in the old sheet, so, jump to the next row
}
// After iterates through all old sheet rows, the student name isn't found, so we add it
}
// Logger.log("Last condition 'alreadyInOldSheet =' " + alreadyInOldSheet);
// Logger.log("newData =' " + newData);
if (alreadyInOldSheet == false) {
oldWorksheet.appendRow(newData[i]);
}
}
absentInNewSheet(oldWorksheet.getDataRange().getValues(), newData, oldWorksheet);
}
function absentInNewSheet(oldData, newData, workSheet) {
for(i=1; i<oldData.length; i++){
var remainsInNewSheet = false;
Logger.log("oldData[i][0] = " + oldData[i][0]);
// Iterates through the old sheet rows
Logger.log("oldData.length = " + oldData.length);
for(j=1; j<newData.length; j++){
// compares the firs cell (student name)
Logger.log("newData[j][0] = " + newData[j][0]);
Logger.log("first condition oldData[i][0] == newData[j][0] = " + (oldData[i][0] == newData[j][0]) );
if (oldData[i][0] == newData[j][0]) {
remainsInNewSheet = true;
Logger.log("break");
break; // This student is already in the old sheet, so, jump to the next row
}
// After iterates through all old sheet rows, the student name isn't found, so we add it
}
Logger.log("Last condition 'remainsInNewSheet =' " + remainsInNewSheet);
Logger.log(" ");
//Logger.log("newData =' " + newData);
if (remainsInNewSheet == false) {
var lastColumn = workSheet.getLastColumn();
var firstColumn = workSheet.getLastColumn();
var currentRow = workSheet.getRange(i+1, 1, 1, lastColumn);
currentRow.setBackgroundRGB(220, 220, 220).setNote("This student was transfered from this school");
}
}
}