How to write to next column when running a loop? - javascript

I would like to extract all links from a folder in google drive and write two columns, one containing the link, and the next column containing the file name.
I'm new to google scripting and wrote this code:
function myFunction() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var s=ss.getActiveSheet();
var c=s.getActiveCell();
var fldr=DriveApp.getFolderById("0B37vVx5p-eGMTmJmTF9JOUwxZnc");
var files=fldr.getFiles();
var names=[],f,str;
while (files.hasNext()) {
f=files.next();
str='=hyperlink("' + f.getUrl() + '")';
names.push([str]);
}
s.getRange(c.getRow(),c.getColumn(),names.length).setFormulas(names);
}
function myFunction() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var s=ss.getActiveSheet();
var c=s.getActiveCell();
var fldr=DriveApp.getFolderById("0B37vVx5p-eGMTmJmTF9JOUwxZnc");
var files=fldr.getFiles();
var names=[],f,str;
while (files.hasNext()) {
f=files.next();
str='=" + f.getName() + "';
names.push([str]);
}
s.getRange(c.getRow(),c.getColumn(),names.length).setFormulas(names);
}
The problem is that after writing the links in the first column, it overwrites it with the names in the first column. How can I specify what column the second piece of code should be written to?

There's no real reason to grab these in separate functions, you could just push them both to the same array then insert into the sheet, the code below should do what you're expecting:
function getFolders() {
var ss=SpreadsheetApp.getActiveSpreadsheet();
var s=ss.getActiveSheet();
var c=s.getActiveCell();
var fldr=DriveApp.getFolderById("0B37vVx5p-eGMTmJmTF9JOUwxZnc");
var files=fldr.getFiles();
var names=[],f,url,name;
while (files.hasNext()) {
f=files.next();
url='=hyperlink("' + f.getUrl() + '")';
name='="' + f.getName() + '"';
names.push([url,name]);
}
s.getRange(c.getRow(),c.getColumn(),names.length, 2).setFormulas(names);
}
I had to tweak name='="' + f.getName() + '"'; because the quotes weren't quite in the right place, it was pushing that string itself to the array rather than the filename.
Pushing both of the values to the array like this means you don't need to offset the range because the array will span 2 columns when you use setValues(), hence why I had to add 2 for numColumns in the setValues() too.

On a Google Apps Script project each function should have unique names otherwise only the last function in the order that they are loaded at execution time will be executed. So, in first place, the hyperlink formulas aren't never written.
By the other hand, as was mentioned on a previous answer, there is no need to have separate functions to write on each column, one single function could fill both columns at the same time.
On this code line:
names.push([str]);
The argument por push is an array having a single value. Replace this argument by an array incluiding the values for both columns. To make the minimum changes to you first function, rename the str variable, add a second code line for the second column with a proper and unique variable name , i.e. (as was shown on the previous answer):
url='=hyperlink("' + f.getUrl() + '")';
name='=" + f.getName() + "';
names.push([url,name]);
And lastly, add the number of columns to the last statement
s.getRange(c.getRow(),c.getColumn(),names.length, 2).setFormulas(names);
Don't forget to delete the second function named myFunction

Use Range.offset:
Returns a new range that is offset from this range by the given number of rows and columns (which can be negative). The new range is the same size as the original range.

Related

Advice on how to optimise Google AppScript code [duplicate]

I've just written my first google apps scripts, ported from VBA, which formats a column of customer order information (thanks to you all of your direction).
Description:
The code identifies state codes by their - prefix, then combines the following first name with a last name (if it exists). It then writes "Order complete" where the last name would have been. Finally, it inserts a necessary blank cell if there is no gap between the orders (see image below).
Problem:
The issue is processing time. It cannot handle longer columns of data. I am warned that
Method Range.getValue is heavily used by the script.
Existing Optimizations:
Per the responses to this question, I've tried to keep as many variables outside the loop as possible, and also improved my if statements. #MuhammadGelbana suggests calling the Range.getValue method just once and moving around with its value...but I don't understand how this would/could work.
Code:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getRange("A:A").getLastRow();
var row, range1, cellValue, dash, offset1, offset2, offset3;
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
range1 = s.getRange(row + 1, 1);
//if cell substring is number, skip it
//because substring cannot process numbers
cellValue = range1.getValue();
if (typeof cellValue === 'number') {continue;};
dash = cellValue.substring(0, 1);
offset1 = range1.offset(1, 0).getValue();
offset2 = range1.offset(2, 0).getValue();
offset3 = range1.offset(3, 0).getValue();
//if -, then merge offset cells 1 and 2
//and enter "Order complete" in offset cell 2.
if (dash === "-") {
range1.offset(1, 0).setValue(offset1 + " " + offset2);
//Translate
range1.offset(2, 0).setValue("Order complete");
};
//The real slow part...
//if - and offset 3 is not blank, then INSERT CELL
if (dash === "-" && offset3) {
//select from three rows down to last
//move selection one more row down (down 4 rows total)
s.getRange(row + 1, 1, lastRow).offset(3, 0).moveTo(range1.offset(4, 0));
};
};
}
Formatting Update:
For guidance on formatting the output with font or background colors, check this follow-up question here. Hopefully you can benefit from the advice these pros gave me :)
Issue:
Usage of .getValue() and .setValue() in a loop resulting in increased processing time.
Documentation excerpts:
Minimize calls to services:
Anything you can accomplish within Google Apps Script itself will be much faster than making calls that need to fetch data from Google's servers or an external server, such as requests to Spreadsheets, Docs, Sites, Translate, UrlFetch, and so on.
Look ahead caching:
Google Apps Script already has some built-in optimization, such as using look-ahead caching to retrieve what a script is likely to get and write caching to save what is likely to be set.
Minimize "number" of read/writes:
You can write scripts to take maximum advantage of the built-in caching, by minimizing the number of reads and writes.
Avoid alternating read/write:
Alternating read and write commands is slow
Use arrays:
To speed up a script, read all data into an array with one command, perform any operations on the data in the array, and write the data out with one command.
Slow script example:
/**
* Really Slow script example
* Get values from A1:D2
* Set values to A3:D4
*/
function slowScriptLikeVBA(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
for(var row = 1; row <= 2; row++){
for(var col = 1; col <= 4; col++){
var sourceCellRange = sh.getRange(row, col, 1, 1);
var targetCellRange = sh.getRange(row + 2, col, 1, 1);
var sourceCellValue = sourceCellRange.getValue();//1 read call per loop
targetCellRange.setValue(sourceCellValue);//1 write call per loop
}
}
}
Notice that two calls are made per loop(Spreadsheet ss, Sheet sh and range calls are excluded. Only including the expensive get/set value calls). There are two loops; 8 read calls and 8 write calls are made in this example for a simple copy paste of 2x4 array.
In addition, Notice that read and write calls alternated making "look-ahead" caching ineffective.
Total calls to services: 16
Time taken: ~5+ seconds
Fast script example:
/**
* Fast script example
* Get values from A1:D2
* Set values to A3:D4
*/
function fastScript(){
const ss = SpreadsheetApp.getActive();
const sh = ss.getActiveSheet();
//get A1:D2 and set it 2 rows down
var sourceRange = sh.getRange("A1:D2");
var targetRange = sh.getRange("A3:D4");
var sourceValues = sourceRange.getValues();//1 read call in total
//modify `sourceValues` if needed
//sourceValues looks like this two dimensional array:
//[//outer array containing rows array
// ["A1","B1","C1",D1], //row1(inner) array containing column element values
// ["A2","B2","C2",D2],
//]
//#see https://stackoverflow.com/questions/63720612
targetRange.setValues(sourceValues);//1 write call in total
}
Total calls to services: 2
Time taken: ~0.2 seconds
References:
Best practices
What does the range method getValues() return and setValues() accept?
Using methods like .getValue() and .moveTo() can be very expensive on execution time. An alternative approach is to use a batch operation where you get all the column values and iterate across the data reshaping as required before writing to the sheet in one call. When you run your script you may have noticed the following warning:
The script uses a method which is considered expensive. Each
invocation generates a time consuming call to a remote server. That
may have critical impact on the execution time of the script,
especially on large data. If performance is an issue for the script,
you should consider using another method, e.g. Range.getValues().
Using .getValues() and .setValues() your script can be rewritten as:
function format() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getActiveSheet();
var lastRow = s.getLastRow(); // more efficient way to get last row
var row;
var data = s.getRange("A:A").getValues(); // gets a [][] of all values in the column
var output = []; // we are going to build a [][] to output result
//loop through all cells in column A
for (row = 0; row < lastRow; row++) {
var cellValue = data[row][0];
var dash = false;
if (typeof cellValue === 'string') {
dash = cellValue.substring(0, 1);
} else { // if a number copy to our output array
output.push([cellValue]);
}
// if a dash
if (dash === "-") {
var name = (data[(row+1)][0]+" "+data[(row+2)][0]).trim(); // build name
output.push([cellValue]); // add row -state
output.push([name]); // add row name
output.push(["Order complete"]); // row order complete
output.push([""]); // add blank row
row++; // jump an extra row to speed things up
}
}
s.clear(); // clear all existing data on sheet
// if you need other data in sheet then could
// s.deleteColumn(1);
// s.insertColumns(1);
// set the values we've made in our output [][] array
s.getRange(1, 1, output.length).setValues(output);
}
Testing your script with 20 rows of data revealed it took 4.415 seconds to execute, the above code completes in 0.019 seconds

Google Script Errror: "Incorrect range width" when using setValues

Try as I might I CANNOT decipher the problem that I'm having writing new rows to a sheet. I've done this several times and I've debugged this thoroughly using Logger.log, but I just can't solve it. Here's a summary of what I'm doing, a code snippet, and a log:
What I'm doing:
Adding rows to a sheet (below existing rows)
73 new rows are stored stored in array: Grade Rows
When attempt to write the new rows to the sheet, get this error:
Incorrect range width, was 1 should be 26
Here’s the code including some Logger.logs:
var BeginningRow = LastSGRowSheet + 1;
var EndingRow = BeginningRow + SGPushKtr -1;
Logger.log("BeginningRow =>" + BeginningRow + "<=, SGPushKtr =>" + SGPushKtr + "<=, Ending Row =>" + EndingRow + "<=");
var GradesRangeString = 'A' + BeginningRow + ':' + LastStudentGradesColumnLetter + EndingRow;
Logger.log("GradesRangeString =>" + GradesRangeString + "<=");
StudentGradeSheet.getRange(GradesRangeString).setValues(GradeRows);
The error occurs in that last line of code.
Here’s the log:
17-12-31 11:51:15:763 EST] BeginningRow =>364<=, SGPushKtr =>73<=, Ending Row =>436<=
[17-12-31 11:51:15:764 EST] GradesRangeString =>A364:Z436<=
Let's say that your data array is dA then the number of rows in that array is dA.length and assuming its a rectangular array then the number of columns is vA[0].length. So your output command has to be some thing like this.
sheet.getRange(firstRow,firstColumn,dA.length,dA[0].length).setValues(dA);
If you'd like to learn a little more about this problem check this out.
You could also append each row to the current sheet one row at a time in loop.
It's hard to know why GradeRows doesn't match your range without seeing all of your code.
Using Cooper's getRange arguments will likely reveal your problem, and will prevent you from having to update your row and column variables when you make changes to your code. Another issue that gets me sometimes is the fact that the setValues array needs to be exactly the same dimensions as the range. If one row has a different length, it will fail. If the logic I use to create row arrays can result in different lengths, I use the function below to make sure my arrays are symmetric before writing them to a sheet. It is also helpful for debugging.
/**
* Takes a 2D array with element arrays with differing lengths
* and adds empty string elements as necessary to return
* a 2D array with all element arrays of equal length.
* #param {array} ar
* #return {array}
*/
function symmetric2DArray(ar){
var maxLength;
var symetric = true;
if (!Array.isArray(ar)) return [['not an array']];
ar.forEach( function(row){
if (!Array.isArray(row)) return [['not a 2D array']];
if (maxLength && maxLength !== row.length) {
symetric = false;
maxLength = (maxLength > row.length) ? maxLength : row.length;
} else { maxLength = row.length }
});
if (!symetric) {
ar.map(function(row){
while (row.length < maxLength){
row.push('');
}
return row;
});
}
return ar
}
How about using appendRow()? That way you don't need to do lots of calculations about the range. You can loop through your data and add it row by row. Something like this:
myDataArr = [[1,2],[3,4],[5,6]]
myDataArr.forEach(function(arrayItem){
sheet.appendRow([arrayItem[0],arrayItem[1]])
})
// This will output to the sheet in three rows.
// [1][2]
// [3][4]
// [5][6]

Declare javascript static array in function

I have a table containing 21 checkboxes (rows of 7 days times three columns of time of day). I want an onClick function to toggle individual columns of three times of date on or off. I have written this JavaScript function:
function toggleCol(colnum)
{
if (typeof toggleCol.state == 'undefined') toggleCol.state = false;
toggleCol.state = !toggleCol.state;
document.getElementById("req" + colnum).checked = toggleCol.state;
document.getElementById("req" + (colnum + 7)).checked = toggleCol.state;
document.getElementById("req" + (colnum + 14)).checked = toggleCol.state;
}
This works but the obvious problem is that I need not one but seven toggleCol.state variables, one for each column, presumably as an array.
How do I declare and use a static array of seven members within the function? I've tried every way I can think of but I've run out of ideas!
if you want to select and hide all cells of one column you can try: $('td:nth-child(number_of_column)').hide();

Incrementing Number Inside String

For a client's requirement, I have set out several images as follows:
img/img1.jpg
img/img2.jpg
img/img3.jpg
...
img/img4.jpg.
Now, I need to make the function that loads images dynamic. At the moment, the current solution is as follows:
// Grab the last image path
var lastImagePath = $("lastImage").attr("src");
// Increment the value.
var nextImagePath = "img/img" + (+lastImagePath.replace("img/img").replace(".jpg") + 1) + ".jpg";
// So on.
I was wondering if there's a cleaner way to increment the number?
Slightly cleaner:
var nextImagePath = lastImagePath.replace(/\d+/, function (n) { return ++n; });
This uses the version of replace that accepts a regular expression and a function.

Modify JavaScript Object and use setRowsData to write to individual cell

I am using getRowsData to retrieve the information, but I would like to modify the values stored within a JS object and then write that modified value back into the cell. I would like to use the normalized header variable that was created with getRowsData to write back into the cell. So if I retrive the info using
var thirdEmployee = employeeObjects[2];
var stringToDisplay = "The third column is: " + thirdEmployee.firstName + " " + thirdEmployee.lastName;
stringToDisplay += " (id #" + thirdEmployee.employeeId + ") working in the ";
stringToDisplay += thirdEmployee.department + " department and with phone number ";
stringToDisplay += thirdEmployee.phoneNumber;
ss.msgBox(stringToDisplay);
Then, for example, I would assign a new value to the Javascript object thirdEmployee.phoneNumber; and then have it written into the proper location in the range (i.e., active row, column # based on header).
thirdEmployee.phoneNumber = "123-555-5555";
thirdEmployee.phoneNumber.setNewValue();
or
setNewValue.thirdEmployee.phoneNumber;
Basically, it would be a modified version of setRowsData but allow for modification of one object and then telling that object to be written into the spreadsheet based on the active row & column based on the header.
Anyone use or know how to do this?
The setRowsData() helper function from the Writing Data from JavaScript Objects to a Spreadsheet tutorial already supports the ability to write a single object. To use it, though, you need to provide both of the optional parameters optHeadersRange and optFirstDataRowIndex.
If we assume that your headers are in row 1, here's how you could update thirdEmployee:
var headersRange = sheet.getRange(1, 1, 1, sheet.getLastColumn());
var thirdEmployee = employeeObjects[2];
...
thirdEmployee.phoneNumber = "123-555-5555";
setRowsData(sheet, [thirdEmployee], headersRange, 3 );

Categories