compare two spreadsheet and output the difference using google app scripts - google-apps-script

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

Related

Attachments: undefined Apps Script

I am trying to pull an attachment from an email which is a zip file and send that file to Google Analytics to upload the data. I am struggling with the getAttachment part in the script as it is showing as undefined. I have no idea what I am doing wrong. Any help would be greatly appreciated. I have now fixed where it is pulling the right attachment by using: var attachments = messages[0].getAttachments(); However I am now getting a new error where it seems to be looking for a title of the attachment but it is still showing as undefined.
function refundImport() {
/// use custom report to schedule the email - will need to adjust the processCsv() function based on your schema
/// if you are using a non-bing data source - you will probably need to adjust the findCsvAttachments() function as it grabs a zip file now
var CONFIG = {
'emailSubject': 'Refunded or Partially Refunded Orders - TKS',
'customDataSourceId': 'xxxxxxxxxxxxxxxxxxxxx',
'now': new Date(),
'zipFileName': 'refunded_or_partially_refunded_orders.zip',
//'csvFileName': 'refunded_or_partially_refunded_orders.csv',
'analyticsAccountId': '12345678',
'analyticsPropertyId': 'UA-12345678-1'
}
//adds one whole day to a date object - can take negative days if you want yesterday etc
function addDaysToDate(DATE, DAYS) {
var newDate = DATE.getTime() + DAYS * 3600000 * 24;
var newDate1 = new Date(newDate);
return newDate1;
}
//takes a date object and formats it as a string
function formatDateAsString(DATE) {
var dateString = Utilities.formatDate(DATE, 'GMT+12:00', 'yyyy/MM/dd');
return dateString;
}
//after and before must be date strings - use the above function
function grabEmailAttachments(SUBJECT, AFTER, BEFORE) {
var query = 'subject:' + SUBJECT + ' ' + 'has:attachment after:' + AFTER + ' ' + 'before:' + BEFORE;
//assumes only 1 will match - if more than 1 - will match the first one
var thread = GmailApp.search('in:inbox from:"noreply#highviewapps.com"');
var messages = thread[0].getMessages();
var content = messages[0].getPlainBody();
//var attachments = thread.getAttachments()[0];
var attachments = messages[0].getAttachments();
//thread.moveToTrash();
return attachments;
}
// finds csv attachment and creates 2d array of row,column e.g. csv[0][1] = value in row 0 column 1 of csv
function findCsvAttachment(attachments, zipFileToSearch, fileNameToSearch) {
var counter = 0;
for (i = 0; i < attachments.length; i++) {
if (attachments[i].getName().search(zipFileToSearch) != -1) {
var unzip = Utilities.unzip(attachments[i]);
var csvData = Utilities.parseCsv(unzip[0].getDataAsString(), ",");
counter = counter + 1;
}
}
if (counter == 0) {
Logger.log('No file with ' + fileNameToSearch + ' in its name was found.');
}
if (counter == 1) {
return csvData;
}
if (counter > 1) {
Logger.log('More than 1 file with ' + fileNameToSearch + ' in its name was found - the last one was used.');
}
}
function processCsv(csvData, date) {
var headers = 'ga:transactionId,ga:productSku,ga:productPrice,ga:quantityRefunded,ga:transactionRevenue';
var dataForUpload = headers;
return dataForUpload;
}
//assumes media dataType for upload https://developers.google.com/analytics/devguides/config/mgmt/v3/mgmtReference/management/uploads/uploadData
function uploadDataToAnalytics(data, accountId, webPropertyId, customDataSourceId) {
var dataBlob = Utilities.newBlob(data, "application/octet-stream");
var upload = Analytics.Management.Uploads.uploadData(accountId, webPropertyId, customDataSourceId, dataBlob);
return upload;
}
///////// ACTUAL IMPLEMENTATION OF SCRIPT /////////////
var tomorrowString = formatDateAsString(addDaysToDate(CONFIG.now, 1));
var yesterdayString = formatDateAsString(addDaysToDate(CONFIG.now, -1));
var todayString = formatDateAsString(CONFIG.now);
var emailAttachments = grabEmailAttachments(CONFIG.emailSubject, yesterdayString, tomorrowString);
var csv = findCsvAttachment(emailAttachments, CONFIG.zipFileName, CONFIG.csvFileName);
var csvForUpload = processCsv(csv, yesterdayString);
var analyticsUpload = uploadDataToAnalytics(csvForUpload, CONFIG.analyticsAccountId, CONFIG.analyticsPropertyId, CONFIG.customDataSourceId);
}
Try changing these lines of code
From:
var attachments = messages.getAttachments();
and
var unzip = Utilities.unzip(attachments[i]);
To:
var attachments = messages[0].getAttachments();
and
var unzip = Utilities.unzip(attachments[i].copyBlob());

I need help to clean up a script that blocks rows

The code below works but requires too much time to work. In fact, the script cannot finish the code, since the time to process the script has passed several times.
I gladly received help to fine-tune this code
The script was created to block (grouped) rows, if a value is entered in column E. This code is activated every 24 hours as a trigger.
Objective: The worksheet with 1000 rows, is accessible to 250 people, whether or not registered with a google account, and serves to record recreational tennis matches.
var MaxRow = Sheet.getDataRange().getNumRows();
var RowCount = 1;
var Cell = Sheet.getRange("D"+ RowCount);
var CellValue = Cell.getValue();
var BlockStart = 0;
var BlockEnd = 0;
var LockRange = Sheet.getRange(59,6,1,2);
for (RowCount = 4; RowCount <= MaxRow ; RowCount++) {
Cell = Sheet.getRange("D"+ RowCount);
CellValue = Cell.getValue();
if (CellValue != "") {
if (BlockStart == 0) {
BlockStart = RowCount;
BlockEnd = RowCount;
}
else {
BlockEnd = RowCount;
}
}
else {
if (BlockStart > 0) {
LockRange = Sheet.getRange(BlockStart,6,BlockEnd-BlockStart + 1,2);
var Protection = LockRange.protect().setDescription('Rows ' +
BlockStart + ' To ' + BlockEnd + ' Protected');
Protection.removeEditors(Protection.getEditors());
BlockStart = 0;
BlockEnd = 0;
}
}
}
Without a complete function I can't really figure out what you're trying to do. But let's say that you want to read the values in column D from line 4 to the bottom of data, then here's a simple and fast way to do it. This isn't the only way. But it's a lot faster than using getValue() on each row. This will probably run about 10000 times faster.
function getDataInColumnD() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg=sh.getRange(4,4,Sheet.getLastRow()-3,1);
var vA=rg.getValues();
for (var i=0;i<vA.length;i++) {
var value=vA[i][0];
var row=i+4;
Logger.log('col: 4, row: %s, value: %s',row,value);
}
}

Creating a google form from a google spreadsheet

I have created a google spreadsheet to automatically convert into a google form, so i don't have to manually enter all the questions into the google form.
I am writing google app script and managed to get all the questions.I am trying to divide the form in to sections depending on the first column of the sheet. So if the first column is "1" questions corresponding to it should be on the first section and if it is "2" it should create another section.And so on.
How can i do that? what will be the code? I have attached the google sheet as here Google spreadsheet
function myFunction()
{
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
var range = ss.getDataRange();
var data = range.getValues();
var numberRows = range.getNumRows();
var numberColumns = range.getNumColumns();
var firstRow = 1;
var form = FormApp.openById('1hIQCLT_JGLcvjz44vXTvP5ziia6NnwCqWBxYT4h2uCk');
var items = form.getItems();
var ilength = items.length;
for (var i=0; i<items.length; i++)
{
form.deleteItem(0);
}
for(var i=0;i<numberRows;i++)
{
Logger.log(data);
var questionType = data[i][0];
if (questionType=='')
{
continue;
}
//choose the type of question from the first column of the spreadsheet
else if(questionType=='1')
{
var rowLength = data[i].length;
var currentRow = firstRow+i;
var currentRangeValues = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1').getRange(currentRow,1,1,rowLength).getValues();
var getSheetRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1').getDataRange();
var numberOfColumnsSheet = getSheetRange.getNumColumns();
var numberOfOptionsInCurrentRow = numberOfColumnsSheet;
var lastColumnInRange = String.fromCharCode(64 + (numberOfOptionsInCurrentRow));
var range_string = 'C' + currentRow + ":" + lastColumnInRange + currentRow;
var optionsArray = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1').getRange(range_string).getValues();
var choicesForQuestion =[];
for (var j=0;j<optionsArray[0].length;j++)
{
choicesForQuestion.push(optionsArray[0][j]);
}
form.addMultipleChoiceItem().setTitle(data[i][1]).setHelpText("").setChoiceValues(choicesForQuestion).setRequired(true);
}
else
{
continue;
}
}
form.addParagraphTextItem()
.setTitle('Please specify and attach relevant documents'); // add the text question at the last
form.addPageBreakItem().setTitle('Identity - Asset Management').setHelpText("")();
}
googleSheet
If you want to use the same exact format for the next section you can get away with a simple counter. I have written a successful script variant, but it depends on what you really want.
Some of the changes I would do
for (i = 0; i < items.length; i++) {
form.deleteItem(items[i])
}
instead of the current form.deleteItem(0);. Otherwise I see that you grab all the data, however you do not utilize it. Calling the spreadsheet app each time you want the options causes it to run a lot slower. More on that for loop: move the Logger.log(data); outside of the loop. There is no reason for you to keep logging the full data range each time you go to the next row of the data. Or change it to Logger.log(data[i]); which would make more sense.
You already do a
if (questionType=='') {
continue;
}
to skip over the empty lines, so not really sure what that last else is meant for. The loop will fall through to the next option on its own anyway.
Now the way your set up would work is that your questions in the spreadsheet must be in order. That is you cannot have
Section 1
Section 2
Section 1
as that will create 3 sections instead of 2. However let's move along with the assumption that the spreadsheet would only be set up in a way where you will only have a sequence like
Section 1
Section 1
Section 2
In that case you should utilize your data and questionType by adding a counter var sectionCount = 0 somewhere before the loop. Then inside of your for loop you do a simple
else if (questionType != sectionCount) {
form.addSectionHeaderItem().setTitle('Section ' + questionType)
sectionCount++
}
this will create the section (provided that the numbers are always increasing by 1 in Column A). Then in the same for loop you do not need any more if statements and can just use
items = data[i].slice(2, data[i].length + 1)
items = items.filter(chkEmpty)
form.addMultipleChoiceItem().setTitle(data[i][1]).setChoiceValues(items)
where
function chkEmpty(val){
return val != ''
}
function myFunction()
{
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
var ss2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet2');
var range = ss.getDataRange();
var data = range.getValues();
var numberRows = range.getNumRows();
var numberColumns = range.getNumColumns();
var firstRow = 1;
var form = FormApp.openById('1xlXDZB5jhbUWpWHxxJwY-ut5oYkh4OfIQSTGsnwGTW4');
var sectionCount = 0
// deletes the previous changes
var items = form.getItems();
var ilength = items.length;
for (i = 0; i < items.length; i++)
{
form.deleteItem(items[i])
}
for(var i=0;i<numberRows;i++)
{
var questionType = data[i][0];
if (questionType=='')
{
continue;
}
else if (questionType != sectionCount )
{
if (sectionCount != 0 )
{
// form.addParagraphTextItem()
// .setTitle('Please specify and attach relevant documents'); // add the text question at the last
// write the description here using SectionCount
}
sectionCount++ // add new section to the form
form.addSectionHeaderItem().setTitle('Section ' + questionType).setHelpText(""); // add section header and title
}
items = data[i].slice(2, data[i].length + 1)
items = items.filter(chkEmpty)
form.addMultipleChoiceItem().setTitle(data[i]
[1]).setChoiceValues(items).setRequired(true);
if ( i == (numberRows-1)){
// form.addParagraphTextItem()
// .setTitle('Please specify and attach relevant documents');
}
}
function chkEmpty(val)
{
return val != ''
}
Logger.log(data);
}

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

optimizing code and how to take selected region of active spreadsheet as input-google docs

I have written script for changing a format like 12.4/12/12.03 into 12:40:00/12:00:00/12:03:00
Here's the code:
function myFunction() {
var sheet=SpreadsheetApp.getActiveSheet();
var rows= sheet.getDataRange();
var numRows=rows.getNumRows();
var values=rows.getValues()
var column = [];
var p = 0;
var k = "H";
for (var i=0;i<numRows;i++) {
// var cell =
//Split the string a the .
var string = values[i][7].split(".");
string[0] = string[0].toString();
p = i+1;
k = "H"+p;
var cell = sheet.getRange(k);
if(string[1]){
string[1] = string[1].toString();
// if the second part is like 4 in 12.4 you set it to 40
if(string[1]!=0) {
if (string[1].length == 1 )
{ string[1] += "0";}
}
// Set the row value to the format you like, here : 12:40:00/12:40
var changed_format = string[0] + ":" + string[1] + ":00";
values[i][7]=changed_format;
p = i+1;
k = "H"+p;
cell.setValue(changed_format);
}
else {
var changed_format = values[i][7]+":00:00";
cell.setValue(changed_format);
}
}
In the above code, I have mentioned columns...i.e., I have to run this script for each column...every time... ex: values[i][7] k="H"+p for 8th column. So, can anyone plz tell me how to do...all at a time...and if possible reduce my code..(optimize)..and also..if is it possible to do like this : if I select the column in the spreadsheet and the changes done by the script applies to that selected region...I mean I want my script to take the selected region as input...is it possible to do..if how.?
One key to optimize your code is to reduce the number of calls to Google services and try getting them done using JavaScript. Here is an optimized version that you could use.
Note that I havent tested it - so if you come across minor syntax errors, feel free to fix them or give a shout if you cannot fix them.
function myFunction() {
var sheet=SpreadsheetApp.getActiveSheet();
var rows= sheet.getDataRange();
var values=rows.getValues();
var COL_H = 8;
var numRows=values.length ; // Length of array = nuymRows. rows.getNumRows();
var column = [];
//var p = 0;
//var k = "H";
var destArray = new Array();
for (var i=0;i<numRows;i++) {
// var cell =
//Split the string a the .
var string = values[i][7].split(".");
string[0] = string[0].toString();
//p = i+1;
//k = "H"+p;
//var cell = sheet.getRange(k);
if(string[1]){
string[1] = string[1].toString();
// if the second part is like 4 in 12.4 you set it to 40
if(string[1]!=0) {
if (string[1].length == 1 )
{ string[1] += "0";}
}
// Set the row value to the format you like, here : 12:40:00/12:40
var changed_format = string[0] + ":" + string[1] + ":00";
//values[i][7]=changed_format;
//p = i+1;
//k = "H"+p;
//cell.setValue(changed_format);
destArray.push([changed_format]);
}
else {
var changed_format = values[i][7]+":00:00";
//values [i][7] = changed_format;
//cell.setValue(changed_format);
destArray.push([changed_format]);
}
}
var destRange = sheet.getRange(1, COL_H, destArray.length, 1);
destRange.setValues(values);
}
TIP: Putting formatted code in the question helps readability