Trouble getting Google Sheets to Push to Calendar - google-apps-script

I'm having a terrible time getting this code to work. I'm learning a lot, but I'm very much a beginner.
Here's the script:
//push new events to calendar function pushToCalendar() {
//spreadsheet variables
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(1,1,5,25);
var values = range.getValues();
//calendar variables
var calendar = CalendarApp.getCalendarById('xxxx')
var numValues = 0; for (var i = 0; i < values.length; i++) {
//check to see if name and type are filled out - date is left off because length is "undefined"
if ((values[i][0].length > 0) && (values[i][2].length > 0)) {
//check if it's been entered before
if (values[i][16]!= 'y') {
//create event https://developers.google.com/apps-script/class_calendarapp#createEvent
var newEventTitle = 'values[i][1]' + ' ' + values[i][15] + ' - ' + values[i][3] + ' - ' + values[i][10] + ' Driver: Needed';
var newEvent = calendar.createAllDayEvent(newEventTitle, values[i][8]);
//get ID
var newEventId = newEvent.getId();
//mark as entered, enter ID
sheet.getRange(i+2,16).setValue('y');
sheet.getRange(i+2,17).setValue(newEventId);
} //could edit here with an else statement
}
numValues++; }
And here's a link to a dummy:
https://docs.google.com/spreadsheets/d/1gBXtHjEhnNSEkJYq2upCWtwkj1ZLWiUYv6KSuMH8Nds/edit?usp=sharing

There are few errors with code:
1.//push new events to calendar function pushToCalendar(). As you included the function statement in the comments, the whole statment was commented out.
For loop should start from i=1. This is because, when you take i=0, this is referred to as the name of columns row.
sheet.getRange(i+2,16) should be changed to sheet.getRange(i+1,16).
In var values = range.sheet.getRange(1, 1, sheet.getLastRow(), 25).getValues();
Here the code:
function pushToCalendar() {
//spreadsheet variables
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(1,1,5,25);
var values = range.getValues();
Logger.log(values.length);
//calendar variables
var calendar = CalendarApp.getCalendarById('your calendar ID')
var numValues = 0;
for (var i = 1; i < values.length; i++) {
var check = values[i][0];
//check to see if name and type are filled out - date is left off because length is "undefined"
if ((values[i][0]) && (values[i][2].length > 0)) {
//check if it's been entered before
if (values[i][16]!= 'y') {
//create event https://developers.google.com/apps script/class_calendarapp#createEvent
var newEventTitle = 'values[i][1]' + ' ' + values[i][15] + ' - ' + values[i][3] + ' - ' + values[i][10] + ' Driver: Needed';
Logger.log(i);
Logger.log(values[i][8]);
var newEvent = calendar.createAllDayEvent(newEventTitle, values[i][8]);
//get ID
var newEventId = newEvent.getId();
//mark as entered, enter ID
sheet.getRange(i+1,16).setValue('y');
sheet.getRange(i+1,17).setValue(newEventId);
} //could edit here with an else statement
}
}
}
I tried the above code and was able to create events in my calendar.Let me know if you have any questions

Related

Comparing Dates in Google Sheets Script Editor

I am very new to this, so please bear with me. I am trying to get my code to look at each cell in a column, then compare that date to the current date, and if they match send an email. I am aware I will need a loop of some sort to get it to look through the column, but I haven't even gotten that far. I've tried every method I can find online to just get it to compare one cell to the date and send the email.
I tested the email function prior to adjusting it to compare the date, so I know that is working. Put something definitely isn't working...
function sendEmail() {
//Fetch the date
var removalDateRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Expirations").getRange("E2");
var removalDate = removalDateRange.getValue();
var currentDateRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Alerts").getRange("C2");
var currentDate = new Date();
var ui = SpreadsheetApp.getUi();
//Check Date
if (removalDate == currentDate) {
//Fetch the email address
var emailRange =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Alerts").getRange("B2");
var emailAddress = emailRange.getValue();
//Fetch Item Brand
var itemBrandRange =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Expirations").getRange("B2");
var itemBrand = itemBrandRange.getValue();
//Fetch Item Name
var itemNameRange =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Expirations").getRange("A2");
var itemName = itemNameRange.getValue();
//Fetch Item Location
var itemLocationRange =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Expirations").getRange("H2");
var itemLocation = itemLocationRange.getValue();
// Send Alert Email
var message = 'The ' + itemBrand + ' ' + itemName + ' in ' + itemLocation + ' will expire in 2 months. Please use and replace item.';
// Second Column
var subject = 'Pantry Alert';
MailApp.sendEmail(emailAddress, subject, message);
}
}
EDIT
Okay, I had it working late last night and even got it to loop through, and somehow I've broken it again. I've been looking at answers for hours trying to adjust things to make it work. What am I doing wrong?
function emailAlert() {
// today's date information
var today = new Date();
var todayMonth = today.getMonth() + 1;
var todayDay = today.getDate();
var todayYear = today.getFullYear();
// 2 months from now
var oneMonthFromToday = new Date(todayYear, todayMonth, todayDay);
var oneMonthMonth = oneMonthFromToday.getMonth() + 2;
var oneMonthDay = oneMonthFromToday.getDate();
var oneMonthYear = oneMonthFromToday.getYear();
// getting data from spreadsheet
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Exp");
var startRow = 2; // First row of data to process
var numRows = 500; // Number of rows to process
var dataRange = sheet.getRange(startRow, 1, numRows, 999);
var data = dataRange.getValues();
//looping through all of the rows
for (var i = 0; i < data.length; ++i) {
var row = data[i];
var expireDateFormat = Utilities.formatDate(new Date(row[5]),
'ET',
'MM/dd/yyyy'
);
//email Information
var subject = 'Pantry Item Needs Attention!';
var message1 =
row[6] + ' ' + row[3] + ' of ' + row[2] + ' ' + row[1] + ' will expire on ' + expireDateFormat + '. Item can be found in ' + row[7]
+ '. Please Remove and Replace Item.' +
'\n' + 'Thanks Steve!';
var message2 =
row[6] + ' ' + row[3] + ' of ' + row[2] + ' ' + row[1] + ' will expire on ' + expireDateFormat + '. Item can be found in ' + row[7] +
'. Please ensure item has been replaced, removed from the pantry, and deleted from inventory.' +
'\n' + 'Thanks Steve!'
//expiration date information
var expireDateMonth = new Date(row[5]).getMonth() + 1;
var expireDateDay = new Date(row[5]).getDate();
var expireDateYear = new Date(row[5]).getYear();
//checking for today
if (
expireDateMonth === todayMonth &&
expireDateDay === todayDay &&
expireDateYear === todayYear
) {
ui.alert(message1);
}
}
}
Dates can be frustrating to work with, so consider doing the whole thing (loop, if then statement...) with an integer first and then returning to the date part if you're having trouble. That said, try adjusting the top of your code to look like the following:
var ss =SpreadsheetApp.getActiveSpreadsheet();
var removalDateVal = ss.getSheetByName("Expirations").getRange("E2").getValue();
var removalDate = new Date(removalDateVal);
var currentDateVal = ss.getSheetByName("Alerts").getRange("C2").getValue();
var currentDate = new Date(currentDateVal);
That will give you two date objects. But BEWARE! These dates contain time as well as calendar date so they may not equal each other even when they appear to. Use setHours() to zero out the date as seen below.
currentDate.setHours(0,0,0);
removalDate.setHours(0,0,0);
Other notes, it's best practice to set a variable for a spreadsheet and worksheet as shown by Google here. It makes the code much more readable.

Move multiple rows at once with multiple conditions

I want to iterate through each row at once with the for loop. When the row has the value "Done" in column P and "Update" in column M, I want to move the row to Done Sheet, if the row just has "Done" in P, I want it to move to Control Sheet.
function done() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var numColumns = s.getLastColumn();
for(var i = 1; i < 50; i++) {
var cP = s.getRange(i, 13).getValue();
var cM = s.getRange(i, 10).getValue();
if (cP == "Done" && cM == "Update") {
var targetSheet = ss.getSheetByName("Done Sheet"); // Logger.log(targetSheet); —> no output
var target = targetSheet.getRange(targetSheet.getLastRow() + 1, 1);
s.getRange(i, 1, 1, numColumns).moveTo(target);
s.deleteRow(i);
}
else if (cP == "Done") {
var targetSheet = ss.getSheetByName("Control Sheet");
var target = targetSheet.getRange(targetSheet.getLastRow() + 1, 1);
s.getRange(i, 1, 1, numColumns).moveTo(target);
s.deleteRow(i);
}
}
}
Please check if this code works for you:
function done() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Main Sheet'); // ss.getActiveSheet(); <= better call by name
var numColumns = s.getLastColumn();
var values = s.getDataRange().getValues(); // values keeps an array from the whole Sheet
var doneSheet = ss.getSheetByName("Done Sheet");
var ctrlSheet = ss.getSheetByName("Control Sheet");
var rows_to_delete = []; // will receive the indexes to delete
var doneSheetInputs = []; // will receive data to be inputed on doneSheet
var ctrlSheetInputs = []; // will receive data to be inputed on ctrlSheet
for(var i = 0; i < values.length; i++) {
var cP = values[i][15]; //s.getRange(i, 13).getValue();
var cM = values[i][12]; //s.getRange(i, 10).getValue();
if (cP == "Done" && cM == "Update") {
doneSheetInputs.push(values[i]);
rows_to_delete.push(i + 1); // +1 because of difference in Array versus Google Sheets index
Logger.log('Moving row ' + (i + 1) + ' to DONE');
}
else if (cP == "Done") {
ctrlSheetInputs.push(values[i]);
rows_to_delete.push(i + 1);
Logger.log('Moving row ' + (i + 1) + ' to CONTROL');
}
}
Logger.log('doneSheetInputs has ' + doneSheetInputs.length + ' entries');
if (doneSheetInputs.length > 0) {
doneSheet.getRange(doneSheet.getLastRow() + 1, 1, doneSheetInputs.length, doneSheetInputs[0].length).setValues(doneSheetInputs);
}
Logger.log('ctrlSheetInputs has ' + ctrlSheetInputs.length + ' entries');
if (ctrlSheetInputs.length > 0) {
ctrlSheet.getRange(ctrlSheet.getLastRow() + 1, 1, ctrlSheetInputs.length, ctrlSheetInputs[0].length).setValues(ctrlSheetInputs);
}
Logger.log('rows_to_delete has ' + rows_to_delete.length + ' entries');
var rev = rows_to_delete.reverse(); // delete from bottom to top
for (var i = 0; i < rev.length; i++) {
s.deleteRow(rev[i]);
}
}
It's not recommended to call getRange().getValues() so many times, specially inside a long for statement as described in Google Apps Script Best Practices. I always try to call getValues before doing any iterations and return the values at the end of the code, in a batch operation.
EDIT: found out that the indexes for P and M columns were wrong - didn't check that out. Here is a working example: https://docs.google.com/spreadsheets/d/1n68KqmI7HongEzJ_MSwmeQUqlAExjvCu2epEjEBZrlg/edit#gid=0 (send me request to access if you need it).

Google Sheets Script: email with a HTML Table that only has a set of

In Google Spreadsheet I want to be able to send a email with a HTML Table within the Email. I have accomplished this below and would like to now extend the functionality more. Now I am trying to understand / find a way on how to filter the getRange based on a variable.
Example:
If Column C = Todays Date I want to return all rows with Todays date into the HTML Table in the Email. I have been playing with the GetRange but when adjusting it something other then a Range it breaks. How should I go about doing this?
function sendEmail() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var range = sheet.getDataRange();
var recipient = 'email#gmail.com'
var subject = 'Subject'
var date = Utilities.formatDate(new Date(), "GMT+1", "dd/MM/yyyy")
var schedRange = sheet.getRange("A1:L21"); // Trying to understand
//var schedRange = sheet.getRange(Col == 3 && compare == date);
// Put Name & Date into email first.
// We only want the schedule within borders, so
// these are handled separately.
var body = '<div style="text-align:center;display: inline-block;font-family: arial,sans,sans-serif">'
body += '<H1>'+ 'Deployment Table Header ' +'</H1>';
body += '<H2>'
body += getHtmlTable(schedRange);
body += '</div>';
debugger;
recipient = 'email#gmail.com'; // For debugging, send only to self
GmailApp.sendEmail(recipient, subject, "Requires HTML", {htmlBody:body})
}
/**
* Return a string containing an HTML table representation
* of the given range, preserving style settings.
*/
function getHtmlTable(range){
var ss = range.getSheet().getParent();
var sheet = range.getSheet();
startRow = range.getRow();
startCol = range.getColumn();
lastRow = range.getLastRow();
lastCol = range.getLastColumn();
// Read table contents
var data = range.getValues();
// Get css style attributes from range
var fontColors = range.getFontColors();
var backgrounds = range.getBackgrounds();
var fontFamilies = range.getFontFamilies();
var fontSizes = range.getFontSizes();
var fontLines = range.getFontLines();
var fontWeights = range.getFontWeights();
var horizontalAlignments = range.getHorizontalAlignments();
var verticalAlignments = range.getVerticalAlignments();
// Get column widths in pixels
var colWidths = [];
for (var col=startCol; col<=lastCol; col++) {
colWidths.push(sheet.getColumnWidth(col));
}
// Get Row heights in pixels
var rowHeights = [];
for (var row=startRow; row<=lastRow; row++) {
rowHeights.push(sheet.getRowHeight(row));
}
// Build HTML Table, with inline styling for each cell
var tableFormat = 'style="font-size: 10px; border:1px solid black;border-collapse:collapse;text-align:center" border = 1 cellpadding = 1';
var html = ['<table '+tableFormat+'>'];
// Column widths appear outside of table rows
for (col=0;col<colWidths.length;col++) {
html.push('<col width="'+colWidths[col]+'">')
}
// Populate rows
for (row=0;row<data.length;row++) {
html.push('<tr height="'+rowHeights[row]+'">');
for (col=0;col<data[row].length;col++) {
// Get formatted data
var cellText = data[row][col];
if (cellText instanceof Date) {
cellText = Utilities.formatDate(
cellText,
ss.getSpreadsheetTimeZone(),
'M/d');
}
var style = 'style="'
+ 'color: ' + fontColors[row][col]+'; '
+ 'font-family: ' + fontFamilies[row][col]+'; '
+ 'font-size: ' + fontSizes[row][col]+'; '
+ 'font-weight: ' + fontWeights[row][col]+'; '
+ 'background-color: ' + backgrounds[row][col]+'; '
+ 'text-align: ' + horizontalAlignments[row][col]+'; '
+ 'vertical-align: ' + verticalAlignments[row][col]+'; '
+'"';
html.push('<td ' + style + '>'
+cellText
+'</td>');
}
html.push('</tr>');
}
html.push('</table>');
return html.join('');
}
As stated in the comments, you can filter out any rows you don't want when you build your HTML table:
function getHtmlTable(range){
// ... your code...
var today = new Date();
for (row=0;row<data.length;row++) {
var row_date = data[row][2]; // Assuming date is in 3rd column
if(sameDay(new Date(row_date), today){ // See note
html.push('<tr height="'+rowHeights[row]+'">');
for (col=0;col<data[row].length;col++) {
// ... your code ...
}
}
}
}
function sameDay(d1, d2) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate();
}
A few notes:
Calling new Date(row_date) will only work if the date is formatted correctly for the Date constructor. If it is not, you need to parse the date before creating the Date object.
sameDay function borrowed from this answer.

Google Apps Script - Problems with Creating Calendar Event from spreadsheet to non-default calendars

I created a script earlier this year to create a calendar event from a spreadsheet line, putting it onto a shared family calendar and a secondary calendar of my own.
It was running fine earlier this year, but now instead of adding to the secondary calendars, it only adds to my default calendar, which I don't have coded into the script at all.
Did something change with the syntax of how to call a calendar by ID?
//push new events to calendar
function pushToCalendar() {
//spreadsheet variables
var book = SpreadsheetApp.getActiveSpreadsheet();
var now = new Date();
var currentYear = Utilities.formatDate(new Date(now.getTime()), 'America/Chicago', 'yyyy');
var currentSheet = ("Events" + " " + currentYear);
var sheet = book.getSheetByName(currentSheet);
var lastRow = sheet.getLastRow();
var lastCol = sheet.getLastColumn();
var range = sheet.getRange(2,1,lastRow,lastCol);
var values = range.getValues();
//calendar variables
var calendar = CalendarApp.getCalendarById('sample1#group.calendar.google.com');
var calendarShared = CalendarApp.getCalendarById('sample2#group.calendar.google.com');
var numValues = 0;
for (var i = 0; i < values.length; i++) {
//check to see if values 0,1,3,4,5,6 are filled out
if ((values[i][0]) && (values[i][1]) && (values[i][3]) && (values[i][4]) && (values[i][5]) && (values[i][6]))
{
//check if it's been entered before (values[i][8] = '')||(values[i][8] = null)
if (values[i][7].toString().toLowerCase() !== 'y') {
//create event https://developers.google.com/apps-script/class_calendarapp#createEvent
var newEventTitle = 'Game: ' + values[i][0] + ' - ' + values[i][1];
var newSharedTitle = 'Adam - Game: ' + values[i][0] + ' - ' + values[i][1];
var startDay = Utilities.formatDate(new Date(values[i][3]), 'America/Chicago', 'MMMM dd, yyyy');
var startTime = Utilities.formatDate(new Date(values[i][4]), 'America/Chicago', 'HH:mm');
var start = startDay + ' ' + startTime;
var endDay = Utilities.formatDate(new Date(values[i][5]), 'America/Chicago', 'MMMM dd, yyyy');
var endTime = Utilities.formatDate(new Date(values[i][6]), 'America/Chicago', 'HH:mm');
var end = endDay+ ' ' + endTime;
var details = values[i][9] + ' - $' + values[i][10] + '\n' + '\n' + 'Crew: ' + '\n' + values[i][14] + '\n' + values[i][15] + '\n' + values[i][16] + '\n' + '\n' +'Assessor:' + '\n' + values[i][17];
//new event on Secondary AND Shared Calendar
var newEvent = calendar.createEvent(newEventTitle, new Date(start), new Date(end), {location: values[i][13], description: details});
var newSharedEvent = calendarShared.createEvent(newSharedTitle, new Date(start), new Date(end), {location: values[i][13], description: details});
//get ID
var newEventId = newEvent.getId();
//mark as entered, enter ID
sheet.getRange(i+2,8).setValue('y');
sheet.getRange(i+2,9).setValue(newEventId);
}
else;
}
numValues++;
}
Thanks in advance for your help!
Try to create the event and after insert it into your calendar like this:
var event = {
summary:summary,
location: location,
description: '',
start: {dateTime: newdatestart.toISOString()},
end: {dateTime: newdateend.toISOString()},
attendees: [
{email: ''},{email: ''}],
// Red background. Use Calendar.Colors.get() for the full list.
colorId: 11
};
event = Calendar.Events.insert(event, calendarId);
I encountered this same problem with ID as well....apparently you need to get rid of #google.com part of the Event ID itself, and then get it as part of an array
.getId().split('#')[0];
just add this code and it worked like a charm for me

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};
}