Script to copy from one sheet to another, needs edit - google-apps-script

I have this script which is working well, but i need to edit it to
a) only return new rows since last run
b) only return certain cells instead of whole row
any guidance would be greatly appreciated
function Copy() {
var sourceSheet = SpreadsheetApp.openById('1WAtRDYhfVXcBKQoUxfTJORXwAqYvVG2Khl4GuJEYSIs')
.getSheetByName('Jobs Log');
var range = sourceSheet.getRange(1, 1, sourceSheet.getLastRow(), sourceSheet.getLastColumn());
var arr = [];
var rangeval = range.getValues()
.forEach(function (r, i, v) {
if (r[1] == 'Amber') arr.push(v[i]);
});
var destinationSheet = SpreadsheetApp.openById('137xdyV8LEh6GAhAwSx4GmRGusnjsHQ0VGlWbsDLXf2c')
.getSheetByName('Sheet1');
destinationSheet.getRange(destinationSheet.getLastRow() + 1, 1, arr.length, arr[0].length)
.setValues(arr);
}

In order to only check new data added after last runtime we have to store .getLastRow() value in properties and retrieve it every runtime. We would also have to work under a few assumptions:
In the input data new values are only appended at the bottom and never inserted between other data
Data is never deleted from the input sheet (if you ignore this, then you must also have an update script for the last row that runs after deleting data)
The sheet is not sorted after new data is added but before this script is run.
So you would need something along the lines of
var sourceSheet = SpreadsheetApp.openById('1WAtRDYhfVXcBKQoUxfTJORXwAqYvVG2Khl4GuJEYSIs')
.getSheetByName('Jobs Log');
var lastRow = sourceSheet.getLastRow();
// note that you need to hav the script property initialized and stored
// or adjust the if to also check if prevLastRow gets a value
var prevLastRow = PropertiesService.getScriptProperties().getProperty('lastRow')
if (lastRow <= prevLastRow) {
return; // we simply stop the execution right here if we don't have more data
}
// then we simply start the range from the previous last row
// and take the amount of rows added afterwards
var range = sourceSheet.getRange(prevLastRow,
1,
lastRow - prevLastRow,
sourceSheet.getLastColumn()
);
As for the second question, inside the forEach you need to simply push an array into arr that will contain only the columns you want. So for example
if (r[1] == 'Amber') arr.push(v[i]);
changes into
if (r[1] == 'Amber') arr.push([v[i][0], v[i][3], v[i][2]]);
which will output A D C columns (in that order) for each row.
Finally, the last thing you need to run before the script ends is
PropertiesService.getScriptProperties().setProperty('lastRow', lastRow)
which will let us know where we stopped the next time we run the script. Again, keep in mind that this works only if new data will always be in new rows. Otherwise, you need to do a different method and retrieve 2 arrays of data. 1 for the entire input sheet and 1 for the output sheet. Then you would have to perform 2 if checks. First one to see if your criteria are met and a second one to see if it already exists in the output data.

Related

Need to optimize Apps Script function [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

Copy Entire Row To Another Sheet Except Column C

I'm using Google Sheets and I have this code below to copy the last row (via Form On Submit trigger) onto another sheet (not tab). I'm not sure how to copy over the last row and skip the data in Column C. It's currently copying the entire row to another sheet successfully, fyi. Any help would be appreciated, thank you!
function CopyToAnotherSheet() {
// Delaying this script so that there is enough time to get the Google Doc URL before it starts to fill out the template file
Utilities.sleep(10000); // 10 second delay
// Get Source Spreadsheet
var source = SpreadsheetApp.getActiveSpreadsheet();
// Get Source Sheet from Spreadsheet
var source_sheet = source.getActiveSheet();
// Get Last Row
var lastRow = source_sheet.getLastRow();
// Get Last Column
var lastColumn = source_sheet.getLastColumn();
// Get Last Row of Data
var lastRowOfData = source_sheet.getRange(lastRow, 1, 1, lastColumn).getValues();
// Creates a one dimensional array
var oneD_array = lastRowOfData.join().split(",");
// Get the Value of the Manufacturer Cell
var cellValue = source_sheet.getRange(lastRow,3).getValues();
// Copy Last Row to First Sheet
if ( cellValue == "First" ) {
var target = SpreadsheetApp.openById("xxxyyyzzz");
var target_sheet = target.getSheetByName("First");
target_sheet.appendRow(oneD_array);
}
// Copy Last Row to Second Sheet
if ( cellValue == "Second" ) {
var target = SpreadsheetApp.openById("aabbcc");
var target_sheet = target.getSheetByName("Second");
target_sheet.appendRow(oneD_array);
}
}
One option you can do is to remove column C value in your 1-d array. Instead of skipping it.
Sample code:
var lastRowOfData = source_sheet.getRange(lastRow, 1, 1, lastColumn).getValues().flat();
Logger.log("Before removal: "+lastRowOfData)
// Remove index 2 (Column C value) in the array
lastRowOfData.splice(2,1)
Logger.log("After removal: "+lastRowOfData)
What it does?
Using array.flat(), change 2-d array to 1-d array
Using array.splice(), remove an element in the array
Output:
Execution log
5:10:50 AM Notice Execution started
5:10:51 AM Info Before removal: a,b,c,d,e
5:10:51 AM Info After removal: a,b,d,e
5:10:52 AM Notice Execution completed
Note:
I removed the oneD_array variable in the sample code. If you really want a separate variable for your 1-d array you can use this one:
var oneD_array = lastRowOfData.flat();
oneD_array.splice(2,1);

How to edit an imported Google sheet using ImportRange?

I used =QUERY(IMPORTRANGE..) to import data from Sheet 1 to Sheet 2 for Column A, B & C. I have to take note in Column D of Sheet 2 for each entry imported. However, for any new one added, the note (for the previous ones) in Column D stays in the same cells. For example, the formula is in A2, so the new data will be added to A2,B2 & C2. The note is in D2. When a new one is imported, the previous one moved to A3, B3 & C3. However, the note is still in D2.
Is there any way to make those notes to move to the next row automatically when a new entry is added?
Here are the files the data has to be imported to and from: https://drive.google.com/drive/folders/1wbOfW9PbSfJbTBv_CwXOTiyyN_LBTiFq?usp=sharing
If my understanding is correct, you want to accomplish the following:
Import data from one spreadsheet to another using IMPORTRANGE.
Add notes manually to a column in your destination spreadsheet.
When a new row is imported to the destination spreadsheet and make previously imported data, the notes should move too.
To achieve that, you would need to keep track of which note belongs to which row of imported data. Both sets of data should be somehow attached. Considering that you have a timestamp in column A, and that this timestamp is probably unique for each row, this timestamp could be used to attach both (if that's not possible, I'd propose adding another column that will be used to identify each row without ambiguity, via some kind of id).
At this point, I would consider using Google Apps Script to accomplish your needs. With this tool, you could develop the functionality that =QUERY(IMPORTRANGE(...)) is providing right now, and you could use other Apps Script tools to reach the desired outcome. Two tools could be specially necessary to accomplish this:
onEdit triggers, to keep track of when the different spreadsheets are edited and make the appropriate changes if that's the case (basically, copying data from one spreadsheet to another).
Properties Service, to store the information about which note is attached to which row of data.
You could do something on the following lines:
Install two edit triggers, (1) one that will fire a function when the source spreadsheet is edited, and (2) another one that will fire when the destination spreadsheet is edited (a simple trigger cannot be used because you have to reference files to which your spreadsheet might not be bound). You can do this manually or programmatically.
Create a function that, for each note that is added to the destination sheet (in this code sample, that's in column D, please change according to your preferences), stores a key-value pair where the key is the value in column A (which should uniquely identify a row of data) and value is the note. This will be used later for the script to know where each note belongs to:
function storeNotes(e) {
var scriptProperties = PropertiesService.getScriptProperties();
var cell = e.range;
var sheet = cell.getSheet();
var rowIndex = cell.getRow();
var column = cell.getColumn();
var noteColumn = 4; // The column where notes are written, change accordingly
// Check whether correct sheet, column and row is edited:
if (column == noteColumn && rowIndex > 1 && sheet.getName() == "Destination") {
var row = sheet.getRange(rowIndex, 1, 1, sheet.getLastColumn()).getValues()[0];
scriptProperties.setProperty(row[0], row[noteColumn - 1]); // Store property to script properties
}
}
Create a function that, every time the source spreadsheet is edited, will delete all content in the destination spreadsheet and copy the data from the source. Then, it will look at the script properties that were store and, using this information, it will write the notes to the appropriate rows (because I see you only want to copy/paste some of the columns, in this sample some of the columns - the ones whose index is in columnsToDelete - are not copied/pasted, you can change this easily to your preferences):
function copyData(e) {
var range = e.range;
var origin = range.getSheet();
var row = range.getRow();
if (origin.getName() == "Origin" && row > 1) { // Check if edited sheet is called "Origin" and edited row is not a header.
var dest = SpreadsheetApp.openById("your-destination-spreadsheet-id").getSheetByName("Destination");
var firstRow = 2;
var firstCol = 1;
var numRows = origin.getLastRow() - 1;
var numCols = origin.getLastColumn();
var values = origin.getRange(firstRow, firstCol, numRows, numCols).getValues();
// Removing some of the columns to get copied/pasted (in this case B and D):
var columnsToDelete = [1, 3];
values = values.map(function(row) {
for (var i = row.length; i > 0; i--) {
for (var j = 0; j < columnsToDelete.length; j++) {
if (i == columnsToDelete[j]) {
row.splice(i, 1);
}
}
}
return row;
})
// Copying content from source to destination:
var firstRowDest = 2;
var firstColDest = 1;
var numRowsDest = values.length;
var numColsDest = values[0].length;
var noteColumn = 4;
var currentValues = dest.getDataRange().getValues();
if (currentValues.length > 1) dest.deleteRows(2, dest.getLastRow() - 1);
var importedRange = dest.getRange(firstRowDest, firstColDest, numRowsDest, numColsDest);
importedRange.setValues(values);
// Writing notes stored in Properties in the appropriate rows:
var properties = PropertiesService.getScriptProperties().getProperties();
for (var i = 0; i < values.length; i++) {
for (var key in properties) {
if (key == values[i][0]) {
dest.getRange(i + 2, noteColumn).setValue(properties[key])
}
}
}
}
}
Notes:
All these functions should be in the same script if you want all both functions to use Properties.
In this sample, the sheet with source data is called Origin and the sheet where it is copied is called Destination (from what I understood, they are in different spreadsheets).
In this simplified example, columns A, B, E from source sheet get copied to columns A, B, C of the destination sheet, and notes are added to column D. Please change this to fit your case by modifying the corresponding indexes.
I hope this is of any help.
Thank you everyone for helping me, especially Lamblichus & user11982798. I recently noticed that importrange will import data to the destination in the same order as that of the source file. Before I sorted the data based on the timestamp in descending order so the new entry was always on the first row. If I changed it to ascending order, the new one is added to the last row, so the note/comment order will not be affected.
Is it possible to update the note/comment in the destination file back to the source one?
If the note is string please try to put in D2 like this:
=ARRAYFORMULA(if(row(A2:A) = max(arrayformula(if(ISBLANK(A2:A),0,row(A2:A)))),"Your Note", ""))
This will automatically place your note to last row of data

Clear content in S:S when T:T contains "Copied" Google Script

I am stuck with, at first sight, simple script.
I want to clear a content from cell S when T has value "Copied".
What I have at the moment is this:
function onEdit(e) {
if(e.range.columnStart === 20) {
e.range.offset(0,-1).clearContent();
}
}
I am not sure how to include IF. Also, bear in mind that T column has a formula, so I don't edit it manually, and with this script, it doesn't work.
It doesn't have to be OnEdit, I can set a trigger to run the script every minute which is even better, but it is important to filter it by the value Copied.
To explain a bit more how my file works (example):
1) I add a comment in the cell S5.
2) My second script runs every minute where it copies values from column S to column V.
3) In the column T, I have the formula (=IF(V5<>"",IF(RegExMatch(S5,V5),"Copied",""),"")), which means if the value exist in the column V5 add Copied in cell T5.
4) I am looking for a solution that when cell T:T has "Copied", delete the cell range S:S
Thank you millions!
As #TheWizEd points out the value in T is dependant on the result in another cell. However an OnEdit function does not necessarily have to respond to the range where the change was made. I've used this code to use the OnEdit event to evaluate the values in Column T and then make the relevant change to values in Column S.
Column T uses a for loop to go through the various row, but the relevant value is pushed to array. This allows a single setValues to be executed at the end of the function.
The function should be assigned to the OnEdit trigger for the Spreadsheet.
function so_53469142() {
// Setup spreadsheet and target sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("so_53469142");
// get the last row of data
var lastRow = sheet.getLastRow();
// get range for Column S & T
var values = sheet.getRange(1, 19, lastRow, 2).getValues();
// set counter variable
var i = 0;
var dataArray = [];
var masterArray = [];
// start loop
for (i = 0; i < lastRow; i++) {
// Logger.log("i="+i+", S = "+values[i][0]+", T = "+values[i][1]);//DEBUG
// empty the array
dataArray = [];
// test value of first row in T
if (values[i][1] === "Copied") {
// If value = "Copies then push blank onto array for Column S
dataArray.push("");
} else {
// else push existing value for column S
dataArray.push(values[i][0]);
}
// make the array 2D
masterArray.push(dataArray);
}
// Update values in Column S
sheet.getRange(1, 19, lastRow).setValues(masterArray);
}

How do I search for and find the coordinates of a row in Google Spreadsheets

I've been searching for quite a while so hopefully no one else has asked this.
I have a Google Spreadsheet with two sheets, one a sort of database containing form submissions and the other a way for users to interact with submissions one at a time.
Basically I want users to be able to make changes to a submission and save them back to the same line that they came from in the original sheet.
I have the code to send the changes back but I can't figure out how to get the coordinates of the correct row:
function saveChanges() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var source = ss.getSheets()[0];
var destination = ss.getSheets()[1];
var range = source.getRange("A40:BL40");
// Find coordinates of the row where value of cell A40 matches a cell in A:A in second spreadsheet
// This copies the data in A40:BL40 in the source sheet to
// D4:F6 in the second sheet
range.copyValuesToRange(destination, 1, 64, 16, 16);
}
At the moment the data is just written to the coordinates "1, 64, 16, 16" which just points to a currently empty row - ideally I'd change that to a variable with the right coordinates.
The value of cell A40 is a unique ID and ideal for searching the second sheet but I can't figure out how.
I'm very new to Javascript so any help would be greatly appreciated.
To find your matching value in the form response sheet, you must loop through the range to find a match. There are a number of ways to do that, I'll show a couple.
Here's a version of your saveChanges() function that will get all the data from your destination sheet, look through it's column A for a match to the value in A40, then update the data in that row.
function saveChanges() {
var uniqueIdColIndex = 0; // Col "A" has unique ID, is element 0 in row array
var ss = SpreadsheetApp.getActiveSpreadsheet();
var source = ss.getSheets()[0];
var destination = ss.getSheets()[1];
var sourceData = source.getRange("A40:BL40").getValues();
var destData = destination.getDataRange().getValues();
// Find coordinates of the row where value of cell A40 matches a cell in A:A in second spreadsheet
for (var rowIndex=0; rowIndex < destData.length; rowIndex++) {
if (sourceData[0][uniqueIdColIndex] == destData[rowIndex][uniqueIdColIndex]) {
// Found our match
destination.getRange(rowIndex+1,1,sourceData.length,sourceData[0].length)
.setValues(sourceData);
break; // Done, exit loop
}
}
}
Here's another way to do it. This time, we don't read all the data in the destination sheet, only the info in column A. To be able to take advantage of array lookup methods, the two-dimensional array retrieved via .getValues() needs to be transposed first - so we use a helper function to do that. (I'm using the transpose() function from this answer.)
function saveChanges() {
var uniqueIdColIndex = 0; // Col "A" has unique ID, is element 0 in row array
var ss = SpreadsheetApp.getActiveSpreadsheet();
var source = ss.getSheets()[0];
var destination = ss.getSheets()[1];
var sourceData = source.getRange("A40:BL40").getValues();
// Get column A from destination sheet
var destDataTrans = transpose(destination.getRange(1, 1, destination.getLastRow(),1).getValues());
// Find coordinates of the row where value of cell A40 matches a cell in A:A in second spreadsheet
var destRow = destDataTrans[0].indexOf(sourceData[0]) + 1; // +1 to adjust to spreadsheet rows
if (destRow > 0) {
// Found our match
destination.getRange(destRow,1,sourceData.length,sourceData[0].length)
.setValues(sourceData);
}
}
The second approach has fewer lines of code, but should be a bit slower than the first one because of the transpose() function which touches every element in column A before performing a search with .indexOf(). (The first approach searched in place, and exited once a match was found, so it actually does less work.)
In both examples, I've tried to limit the calls to google's services as much as possible. Alternatively, you could read info from the spreadsheets inside the search loop, which would be much slower, but would avoid the +1 / -1 mental gymnastics needed to keep 0-based arrays aligned with 1-based rows and columns.