0

I reviewed the following documentation from Google on how to optimize existing Google scripts here: https://developers.google.com/apps-script/guides/support/best-practices

In particular, the 'Use batch-operation' section seems more appropriate for my use case, where the optimal strategy is to 'batch' all the reading into one operation, and then writing in separate operation; not to cycle between read-and-write calls.

Here is an example of inefficient code, as given by the url above:

  // DO NOT USE THIS CODE. It is an example of SLOW, INEFFICIENT code.
  // FOR DEMONSTRATION ONLY
  var cell = sheet.getRange('a1');
  for (var y = 0; y < 100; y++) {
    xcoord = xmin;
    for (var x = 0; x < 100; x++) {
      var c = getColorFromCoordinates(xcoord, ycoord);
      cell.offset(y, x).setBackgroundColor(c);
      xcoord += xincrement;
    }
    ycoord -= yincrement;
    SpreadsheetApp.flush();
  }

Here is an example of efficient and improved code:

  // OKAY TO USE THIS EXAMPLE or code based on it.
  var cell = sheet.getRange('a1');
  var colors = new Array(100);
  for (var y = 0; y < 100; y++) {
    xcoord = xmin;
    colors[y] = new Array(100);
    for (var x = 0; x < 100; x++) {
      colors[y][x] = getColorFromCoordinates(xcoord, ycoord);
      xcoord += xincrement;
    }
    ycoord -= yincrement;
  }
  sheet.getRange(1, 1, 100, 100).setBackgroundColors(colors);

Now, for my particular use case:
Instead of storing values in an array, then writing/modifying them as a separate operation from reading them into an array, I want to create multiple Google documents that replaced placeholders within each document.

For context:
I'm writing a script that reads a spreadsheet of students with files to modify for each student, which is later sent as a mail merge. For example, there are 3 master files. Each student will have a copy of the 3 master files, which is used to .replaceText placeholder fields.

Here are my relevant snippets of code below:

function filesAndEmails() {
  // Import the Spreadsheet application library.
  const UI = SpreadsheetApp.getUi();

  // Try calling the functions below; catch any error messages that occur to display as alert window.
  try {
    // Prompt and record user's email draft template.
    // var emailLinkID = connectDocument(
    //   UI, 
    //   title="Step 1/2: Google Document (Email Draft) Connection", 
    //   dialog=`What email draft template are you referring to? 
    //   This file should contain the subject line, name and body.
      
    //   Copy and paste the direct URL link to the Google Docs:`,
    //   isFile=true
    // );

    // TEST
    var emailLinkID = "REMOVED FOR PRIVACY";


    if (emailLinkID != -1) {
      // Prompt and record user's desired folder location to store generated files.
      // var fldrID = connectDocument(
      //   UI, 
      //   title="Step 2/2: Google Folder (Storage) Connection", 
      //   dialog=`Which folder would you like all the generated file(s) to be stored at?
        
      //   Copy and paste the direct URL link to the Google folder:`,
      //   isFile=false
      // );

      // TEST
      var fldrID = DriveApp.getFolderById("REMOVED FOR PRIVACY");

      // Retrieve data set from database.
      var sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_1);
      // Range of data must include header row for proper key mapping.
      var arrayOfStudentObj = objectify(sheet.getRange(3, 1, sheet.getLastRow()-2, 11).getValues());
      // Establish array of attachment objects for filename and file url.
      var arrayOfAttachObj = getAttachments();

      // Opportunities for optimization begins here.
      // Iterate through array of student Objects to extract each mapped key values for Google document insertion and emailing.
      // Time Complexity: O(n^3)
      arrayOfStudentObj.forEach(function(student) {
        if (student[EMAIL_SENT_COL] == '') {
          try {
            arrayOfAttachObj.forEach(function(attachment) {
              // All generated files will contain this filename format, followed by the attachment filename/description.
              var filename = `${student[RYE_ID_COL]} ${student[FNAME_COL]} ${student[LNAME_COL]} ${attachment[ATTACH_FILENAME_COL]}`;

              // Create a copy of the current iteration/file for the given student.
              var file = DocumentApp.openById(DriveApp.getFileById(getID(attachment[ATTACH_FILEURL_COL], isFile=false)).makeCopy(filename, fldrID).getId())
              
              // Replace and save all custom fields for the given student at this current iteration/file.
              replaceCustomFields(file, student);

            });

          } catch(e) {

          }
        }
      });
      UI.alert("Script successfully completed!");
    };
  } catch(e) {
    UI.alert("Error Detected", e.message + "\n\nContact a developer for help.", UI.ButtonSet.OK);
  };
}

/**
 * Replaces all fields specified by 'attributesArray' given student's file.  
 * @param   {Object}  file    A single file object used to replace all custom fields with.
 * @param   {Object}  student A single student object that contains all custom field attributes.
 */
function replaceCustomFields(file, student) {
  // Iterate through each student's attribute (first name, last name, etc.) to change each field.
  attributesArray.forEach(function(attribute) {
    file.getBody()
      .replaceText(attribute, student[attribute]);
  });

  // Must save and close file to finalize changes prior to moving onto next student object.
  file.saveAndClose();
}

/**
 * Processes the attachments sheet for filename and file ID.
 * @return  {Array}           An array of attachment file objects.
 */
function getAttachments() {
  var files = SpreadsheetApp.getActive().getSheetByName(SHEET_2);
  return objectify(files.getRange(1, 1, files.getLastRow(), 2).getValues());
}

/**
 * Creates student objects to contain the object attributes for each student based on 
 * the header row.
 * @param   {Array}   array   A 2D heterogeneous array includes the header row for attribute key mapping.
 * @return  {Array}           An array of student objects.
 */
function objectify(array) {
  var keys = array.shift();
  var objects = array.map(function (values) {
      return keys.reduce(function (o, k, i) {
          o[k] = values[i];
          return o;
      }, {});
  });
  return objects;
}

To summarize my code, I read the Google spreadsheet of students as an array of objects, so each student has attributes like their first name, last name, email, etc. I have done the same for the file attachments that would be included for each student. Currently, the forEach loop iterates through each student object, creates copies of the master file(s), replaces placeholder text in each file, then saves them in a folder. Eventually, I will be sending these file(s) to each student with the MailApp. However, due to the repetitive external calls via creating file copies for each student, the execution time is understandably very slow...

TLDR
Is it still possible to optimize my code using "batch operations" when it is necessary for my use case to have multiple DriveApp calls to create said copies of the files for modification purposes? As opposed to reading raw values into an array and modifying them at a later operation, I don't think I could simply just store document objects into an array, then modify them at a later stage. Thoughts?

1 Answer 1

1

You could use batchUpdate of Google Docs API.

See Tanaike's answer just to have an idea on how the request object looks like.

All you need to do in your Apps Script now is build the object for multiple files.

Note:

  • You can also further optimize your code by updating:
var arrayOfStudentObj = objectify(sheet.getRange(3, 1, sheet.getLastRow()-2, 11).getValues();

into:

// what column your email confirmation is which is 0-index
// assuming column K contains the email confirmation (11 - 1 = 10)
var emailSentColumn = 10;
// filter data, don't include rows with blank values in column K
var arrayOfStudentObj = objectify(sheet.getRange(3, 1, sheet.getLastRow()-2, 11).getValues().filter(row=>row[emailSentColumn]));
  • This way, you can remove your condition if (student[EMAIL_SENT_COL] == '') { below and lessen the number of loops.

Resource:

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for great suggestions! For my particular use case/code, oddly, the filter tip performed slower than my current set up with the if (student[EMAIL_SENT_COL] == ''). However, I agree that supplying a 'cleaned' version of the array would remove the conditional. For batchUpdate, I'm not understanding the goal of "build the object for multiple files" – could you expand on that?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.