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?
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:
Google Docs Apps Script Quickstart
Google Docs REST API
I hope this isn't too simple a question for the group. I am teaching myself Swift but I'm really struggling to get my head around a way to read a CSV text file containing a mixture of Int and Double values into a 2d array which sits inside a Class where each line in the array represents a line from the input file. I have successfully loaded an 1d array of strings, each element corresponding to a line of text in the CSV file. However I want to save time by going direct from input file to 2d array of decimals.
Read in the data. Separate the text into lines. Separate the lines into tokens. Convert each token to an Int or if it fails, a Double.
I assume you know how to read the files so here is an example with a static CSV string:
let text = """
1,2,3.0
4,5.0,6,z
"""
enum CustomError: Error {
case notAnItOrADouble(String)
}
do {
let numberRows = try text
.split(separator: "\n")
.map { line in
try line.split(separator: ",").map { substring -> Any in
let token = String(substring)
guard let value: Any = Int(token) ?? Double(token) else {
throw CustomError.notAnItOrADouble(token)
}
return value
}
}
numberRows.forEach { row in
row.forEach { number in
print("\(number) is \(type(of: number))")
}
}
} catch (let error) {
print(error)
}
Output:
1 is Int
2 is Int
3.0 is Double
4 is Int
5.0 is Double
6 is Int
Try putting in a string instead of an int or double and you will see that you get the error with he first unparsable token instead.
Need some help, writing a program that will read the text from a file, but will
put at the beginning of each line a number, in such a way that each line is numbered in ascending order
example:
file1
a
b
c
What I want to see:
1: a
2: b
3: c
Process:
Read contents of file into a String
Split by line ending into Array<String>
Iterate and mutate contents line by line
Join by line ending back into a String
Write back into file
Sample code for any sys target:
var arr = sys.File.getContent('file.txt').split("\n");
for(i in 0...arr.length) {
arr[i] = (i+1) + ": " + arr[i];
}
sys.File.saveContent('file.txt', arr.join("\n"));
I have created a 2d array where a user can input words.
I need to create 2 functions:
1st function is called Add and an Admin can add a user and his data.
2nd function is called Delete and an Admin can remove a user by typing his username. If his username is found in the 2d array created in Add function then all of user's data should be replaced by zeros. The thing is that I cannot overwrite/replace the user's data to zero.
The 2d array:
char pin[50][7][100];
each row contains a single user and each column contains his info like name, surname etc.
Here is the delete function:
printf("Enter the username of the user you want to delete: \n");
scanf("%s",&key);
for(i=0;i<50;i++){
for(j=5;j<6;j++){
if (strcmp(pin[i][j],key)==0){
k=i;
flag=1;
break;
}
}}
if (flag==1){
do{
printf("Are you sure you want to delete this user?\t (Yes or No)\n");
scanf("%s",&api);
}while((strcmp(api,"Yes")!=0) && (strcmp(api,"No")!=0) );
if (strcmp(api,"Yes")==0){
/* Here I need to replace with 0s!*/
}
else if (strcmp(api,"No")==0) {
goto dlt;
}
}
else{
printf("Error!Username not found.Please try again. \n");
}
I presume that the column 5 contains the username, and you have found the user you want to delete at row k.
You can delete the data by using strcpy to replace all the data with 0s.
for (j = 0; j < 7; j++)
{
strcpy(pin[k][j],"0");
}
So in a simple arcade/platformer game, I'm making it so I have a .csv text file set out like so:
660, 25, 0
720, 15, 1
etc..
The first number being the x coordinate, the next being the y coordinate and the last being whether the block kills you or not. Loading this data externally is not a problem and works fine but when it comes to actually running the .swf by itself obviously the .csv file is not embedded into it so I cannot access any values from it.
Therefore my question is: How can I embed a .csv file into my project and then read out 3 values per line into a multi dimensional array with each line denoting a different obstacle?
(The multi dimensional array being [obstacleID][0 for x coord/1 for y coord/2 for whether it kills or not])
How to embed a text file in Flash
then you can try:
var csv:embedded_csv = new embedded_csv();
var csvLines:Array = csv.toString().split("\n"); // \n or File.lineSeparator or \r\n
for(i=0; i<csvLines.length; i++)
{
line:Array = String(csvLines[i]).split(", ");
x = line[0];
y = line[1];
kills = line[2];
...
}