Dynamically edit columns in google sheets - javascript

Context
I am creating a database of stock price data. I am currently using the below onEdit function:
function onEdit(e) {
// Get the spreadsheet that this will apply to, the tabs and columns
var ss = e.source.getActiveSheet();
var excludeTabs = ["Summary", "FTSE 250"];
var excludeColumns = [1,2,12,13,14,15,16]; // the columns to isolate for special reasons
var excludeCells = ["M1","I1","I2"];
// What is the criteria for action to be taken? If the terms defined in excludeTabs are met, then move onto the next criteria (indexOf used because we stored it as an array
if(excludeTabs.indexOf(ss.getName())===-1){
// The range from the spreadsheet.
// Scripts expects an event in a cell
var cell = e.range;
// For the entire column of this cell
var col = cell.getColumn();
// Within the universe of tabs that this script applies to, we want to exclude the columns in the array
if(excludeColumns.indexOf(col)===-1)
// Within the universe of tabs and columns, we want to exclude certain cells
if(excludeCells.indexOf(cell)===-1)
// Need to make sure it only applies to formulas
if(cell.getFormula() !== ""){
var destination = ss.getRange(4, col, ss.getLastRow()-1, 1);
cell.copyTo(destination);
}//End of the remaining universe of data taking out the exceptions
}//End Tab criteria
}//End function
This allows for an edit in some of the columns to be performed when I edit the cell. So far it works but with a few kinks.
Problem 1
Sometimes, when I edit a cell above the fourth row of a column, it edits the entire column despite me telling it to start from the fourth row. This happened just a few minutes ago in a cell I told it to exclude above "I2". Is there anything wrong with the code I have written to this effect?
Problem 2
I tried creating other exceptions for the code, where for some specified ranges, it will only edit from a different cell range. Not the fourth cell of every column but of say the 10th cell. I tried adding it below var destination = ss.getRange(4, col, ss.getLastRow()-1, 1) but it did not work. I also tried creating a separate onEdit function for a different cell location but it also did not work.
So far I have been using the sheet formulas like the below:
IFERROR(IFS(C4="Returns","",AND(C4="",C5="",C6="",C7="",C8=""),"",AND(ISNUMBER(C4),ISNUMBER(C5),ISNUMBER(C6),ISNUMBER(C7),ISNUMBER(C8)),COVAR($C4:$C8,'FTSE 250'!J5:J9)),""))
But this just gets messy. Sometimes there is data in the cell and so it would render formulas like the above useless. An example is the below picture.
Update I want the onEdit to start and drag down from the 10th row of the column but only for that column (this is the row in that column that I will be editing). I also want to be able to do this for other columns (start the automatic copy down process from different rows).
This is the range I am trying to edit
[
Update 2
...
if(!excludeTabs.includes(ss.getName()) &&
!excludeColumns.includes(col) &&
!excludeCells.includes(cell.getA1Notation()) &&
cell.getFormula() !== ""
){
if(col==33){
var destination = ss.getRange(8, col, ss.getMaxRows()-7, 1);
cell.copyTo(destination);
}
else if(col===30){
var destination = ss.getRange(8, col, ss.getMaxRows()-7, 1);
cell.copyTo(destination);
}
else{
var destination = ss.getRange(4, col, ss.getMaxRows()-3, 1);
cell.copyTo(destination);
}
}

Explanation:
Issues:
Your if statements don't have closed brackets with code in it.
Here if(excludeCells.indexOf(cell)===-1) there is a problem:
var excludeCells = ["M1","I1","I2"]; is an array of strings and var cell = e.range; is a range object. You are actually comparing two different things (a string vs a range object.)
Instead you want to replace: if(excludeCells.indexOf(cell)===-1) with if(excludeCells.indexOf(cell.getA1Notation())===-1).
Improvements:
Instead of using multiple if statements which at the end lead to one single code block, use one if statement with multiple conditions.
Also this range getRange(4, col, ss.getLastRow()-1, 1); does not make a lot of sense either. It makes more sense to use ss.getLastRow()-3 because you are starting from 3.
Instead of using excludeCells.indexOf(cell.getA1Notation())===-1 which is a long expression, you can use includes() like that !excludeCells.includes(cell.getA1Notation()).
Solution:
function onEdit(e) {
var ss = e.source.getActiveSheet();
var excludeTabs = ["Summary", "FTSE 250"];
var excludeColumns = [1,2,12,13,14,15,16]; // the columns to isolate for special reasons
var excludeCells = ["M1","I1","I2"];
var cell = e.range;
var col = cell.getColumn();
if(!excludeTabs.includes(ss.getName()) &&
!excludeColumns.includes(col) &&
!excludeCells.includes(cell.getA1Notation()) &&
cell.getFormula() !== ""
){
if(col==33){
var destination = ss.getRange(10, col, ss.getMaxRows()-9, 1);
cell.copyTo(destination);
}
else{
var destination = ss.getRange(4, col, ss.getMaxRows()-3, 1);
cell.copyTo(destination);
}
}
}
Please Note:
getLastRow() returns the last row with content. For example if you have 10 columns and the first 10 have last row with content to be 55 but there is a random value in column 20 at the end of the sheet let's say row 900 then 900 will be the last row in your sheet. Be careful with that, otherwise you will need other approach to get the last row with content. Formulas are content too. So a formula all the way to the bottom of the sheet might determine what getLastRow returns.

Try this:
function onEdit(e) {
var sh = e.range.getSheet();
var excludeTabs = ["Summary", "FTSE 250"];
var excludeColumns = [1,2,12,13,14,15,16];
var excludeCells = ["M1","I1","I2"];
if(excludeTabs.indexOf(sh.getName())==-1 && excludeColumns.indexOf(e.range.columnStart)==-1 && excludeCells.indexOf(e.range.getA1Notation())==-1 && e.range.getFormula()!=""){
e.range.copyTo(sh.getRange(4, e.range.columnStart, sh.getLastRow()-3, 1));//The numbers of rows to the bottom is sh.getLastRow()-3 -1 will have to roll off of the bottom of the spreadsheet which will give you out of range errors
}
}
}

Related

How to exclude the first and second rows - Apps Script / Google Sheets

Code I have allows me to automatically update the formulas in the sheet. It works.
On 1st row I have headers and they do not overwrite each other. In the 2nd I have the average formulas. Each edit overwrites entire column. Also those averages.
So I would like to exclude also 2nd row from pasting formulas.
function onEdit(e) {
var activeSheet = e.source.getActiveSheet()
var tabs = ['2023'];
if(tabs.indexOf(activeSheet.getName()) !== -1){
var cell = e.range;
var col = cell.getColumn();
if(cell.getFormula() !==""){
var destination = activeSheet.getRange(3, col, activeSheet.getLastRow()-2, 1);
cell.copyTo(destination);
}
}
}
I ask for your help.
I have tried to exclude 2nd row in the code, with no effect.

Removing duplicate rows using Google App Scripts

I have a Google Sheet with two tabs that I want to check rows and delete duplicates from. However, the requirement for both is slightly different.
On tab submittedMatches I have data imported from a Google Form into columns A:C where a script on form submit then grabs data from an API and populates D:J and finally, K:M has a custom formula inserted via a script. The range of data being A2:M
The problem I am having is that a) I'm new to all of this, and b) when I try to remove duplicates (people can submit the same thing twice from the form) the only solution I have found copies all the rows, removes the duplicates and pastes the unique rows again so I lose the formulas.
I have the script checking columns B (this just has a letter from A-Z) and C (a unique ID) for duplicates, e.g., If row 2 and 3 have an "A" in column B and "123456" in column C, then this is a duplicate. For reference, this is what I had working:
function removeDuplicates() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getDataRange().getValues();
var newData = [];
for (var i in data) {
var row = data[i];
var duplicate = false;
for (var j in newData) {
if(row[2] == newData[j][2] && row[3] == newData[j][3]){
duplicate = true;
}
}
if (!duplicate) {
newData.push(row);
}
}
sheet.clearContents();
sheet.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
}
What I need to happen is the script check for the duplicate rows based on data in columns B and C and remove them, but the formula in K:M must be preserved. Either by just removing the data from A:J or by another method.
Additionally, I'm assuming I need to make sure this is only run on the tab 'submittedMatches' and not other tabs on the sheet.
I solved this. Sorry. Always the way when you eventually ask for help after hours of trying.
I set a conditional format on B and C with: =countifs($B$1:$B1,$B1,$C$1:$C1,$C1)>1
I set the background colour to #fefefe and made the font red and bold. I then added the below script to check for cells with a #fefefe background and remove them.
function deleteDuplicateMatches() {
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheetByName('submittedMatches');
//Only get range with data (non-empty cells)
var firstCol = sheet.getDataRange();
//Save data validation rules based on the first row
var dataValRules = sheet.getRange('A2:M2').getDataValidations();
var maxRow = firstCol.getNumRows();
for (var row=1; row<=maxRow; row++){
//Delete row with background color of gray #fefefe
if(sheet.getRange(row,1).getBackground() == "#fefefe"){
sheet.deleteRow(row);
//since current row was deleted adjust current data set
//this will handle scenarios with succeeding duplicates
row--;
maxRow--;
//insert new row at the end and set data validation rules
sheet.insertRowAfter(maxRow);
sheet.getRange(maxRow,1,1,13).setDataValidations(dataValRules);
}
}
};

Is it possible to pull different data sets from one column?

I've been trying to write some code that looks down one column with strings based on some simple formulas. I can't seem to get it to recognize the different sets of data and paste them where I want them.
I have tried re writing the code a few different ways in which is looks at all the data and just offsets the destination row by 1. But it does not recognize that it is pull different data.
Below is the code that works. What it does is starts from the 1st column 2nd row (where my data starts). The data is a list like;
A
1 Customer1
2 item1
3 item2
4 Item3
5
6 Customer2
7 Item1
The formulas that I have in those cells just concatenates some other cells.
Using a loop it looks through column A and find the blank space. It then "breaks" whatever number it stops on, the numerical A1 notation of the cell, it then finds the values for those cells and transposes them In another sheet in the correct row.
The issue I am having with the code this code that has worked the best is it doesn't read any of the cells as blank
(because of the formulas?) and it transposes all to the same row.
function transpose(){
var data = SpreadsheetApp.getActiveSpreadsheet();
var input =data.getSheetByName("EMAIL INPUT");
var output = data.getSheetByName("EMAIL OUTPUT");
var lr =input.getLastRow();
for (var i=2;i<20;i++){
var cell = input.getRange(i, 1).getValue();
if (cell == ""){
break
}
}
var set = input.getRange(2, 1, i-1).getValues();
output.getRange(2,1,set[0].length,set.length) .
.setValues(Object.keys(set[0]).map ( function (columnNumber) {
return set.map( function (row) {
return row[columnNumber];
});
}));
Logger.log(i);
Logger.log(set);
}
What I need the code to do is look through all the data and separate the sets of data by a condition.
Then Transpose that information on another sheet. Each set (or array) of data will go into a different row. With each component filling across the column (["customer1", "Item1","Item2"].
EDIT:
Is it Possible to pull different data sets from a single column and turn them into arrays? I believe being able to do that will work if I use "appendrow" to tranpose my different arrays to where I need them.
Test for the length of cell. Even if it is a formula, it will evaluate the result based on the value.
if (cell.length !=0){
// the cell is NOT empty, so do this
}
else
{
// the cell IS empty, so do this instead
}
EXTRA
This code takes your objective and completes the transposition of data.
The code is not as efficient as it might/should because it includes getRange and setValues inside the loop.
Ideally the entire Output Range could/should be set in one command, but the (unanswered) challenge to this is knowing in advance the maximum number rows per contiguous range so that blank values can be set for rows that have less than the maximum number of rows.
This would be a worthwhile change to make.
function so5671809203() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var inputsheetname = "EMAIL_INPUT";
var inputsheet = ss.getSheetByName(inputsheetname);
var outputsheetname = "EMAIL_OUTPUT";
var outputsheet = ss.getSheetByName(outputsheetname);
var inputLR =inputsheet.getLastRow();
Logger.log("DEBUG: the last row = "+inputLR);
var inputrange = inputsheet.getRange(1, 1,inputLR+1);
Logger.log("the input range = "+inputrange.getA1Notation());
var values = inputrange.getValues();
var outputval=[];
var outputrow=[];
var counter = 0; // to count number of columns in array
for (i=0;i<inputLR+1;i++){
Logger.log("DEBUG: Row:"+i+", Value = "+values [i][0]+", Length = "+values [i][0].length);
if (values [i][0].length !=0){
// add this to the output sheet
outputrow.push(values [i][0]);
counter = counter+1;
Logger.log("DEBUG: value = "+values [i][0]+" to be added to array. New Array Value = "+outputrow+", counter = "+counter);
}
else
{
// do nothing with the cell, but add the existing values to the output sheet
Logger.log("DEBUG: Found a space - time to update output");
// push the values onto an clean array
outputval.push(outputrow);
// reset the row array
outputrow = [];
// get the last row of the output sheet
var outputLR =outputsheet.getLastRow();
Logger.log("DEBUG: output last row = "+outputLR);
// defie the output range
var outputrange = outputsheet.getRange((+outputLR+1),1,1,counter);
Logger.log("DEBUG: the output range = "+outputrange.getA1Notation());
// update the values with array
outputrange.setValues(outputval);
// reset the row counter
counter = 0;
//reset the output value array
outputval=[];
}
}
}
Email Input and Output Sheets

Move Row to Other Sheet Based on Cell Value

I have a sheet with rows I'd like to move to another sheet based on a cell value. I tried following this post's solution (refer below), but I'm having trouble editing the script towards what I want it to do.
I'm doing Check-Ins for an event. I would like to be able to change the value in Column F, populate Column G with the time the status changed, and for the row data to migrate to the Attendee Arrived sheet.
I believe the script already does this, but one has to run it manually. It also takes care of deleting the row data in A (Event) after migrating it to B (Attendee Arrived).
My question is could someone please help me set it up in order for script to run continuously (on edit), and also accomplish all of the above if I missed something?
I don't believe the script will respect the drop down format as it runs so I'm willing to manually type in something. It'd be cool if it could stay that way though - makes it easier for one.
Here's the sheet I'm testing on.
https://docs.google.com/spreadsheets/d/1HrFnV2gFKj1vkw_UpJN4tstHVPK6Y8XhHCIyna9TLJg/edit#gid=1517587380
Here's the solution I tried following. All credit to Jason P and Ritz for this.
Google App Script - Google Spreadsheets Move Row based on cell value efficiently
Thank you D:
function CheckIn() {
// How Many Columns over to copy
var columsCopyCount = 7; // A=1 B=2 C=3 ....
// What Column to Monitor
var columnsToMonitor = 6; // A=1 B=2 C=3 ....
//TARGET SPREAD SHEETS
var target1 = "Attendee Arrived";
//Target Value
var cellvalue = "Attendee Arrived";
//SOURCE SPREAD SHEET
var ss = SpreadsheetApp.openById('1HrFnV2gFKj1vkw_UpJN4tstHVPK6Y8XhHCIyna9TLJg');
var sourceSpreadSheetSheetID = ss.getSheetByName("Event");
var sourceSpreadSheetSheetID1 = ss.getSheetByName(target1);
var data = sourceSpreadSheetSheetID.getRange(2, 1, sourceSpreadSheetSheetID.getLastRow() - 1, sourceSpreadSheetSheetID.getLastColumn()).getValues();
var attendee = [];
for (var i = 0; i < data.length; i++) {
var rValue = data[i][6];
if (rValue == cellvalue) {
attendee.push(data[i]);
} else { //Fail Safe
attendee.push(data[i]);
}
}
if(attendee.length > 0){
sourceSpreadSheetSheetID1.getRange(sourceSpreadSheetSheetID1.getLastRow() + 1,
1, attendee.length, attendee[0].length).setValues(attendee);
}
//Will delete the rows of importdata once the data is copided to other
sheets
sourceSpreadSheetSheetID.deleteRows(2,
sourceSpreadSheetSheetID.getLastRow() - 1);
}
Try this:
I imagine that you already know that you'll need an installable onEdit Trigger and that you can't test a function of this nature by running it without the event object.
function checkIn(e) {
var sh=e.range.getSheet();
if(sh.getName()!="Event") return;
if(e.range.columnStart==6) {
if(e.value=="Attendee Arrived"){
e.range.offset(0,1).setValue(Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "M/d/yyyy HH:mm:ss"));
var row=sh.getRange(e.range.rowStart,1,1,sh.getLastColumn()).getValues()[0];
e.source.getSheetByName("Attendee Arrived").appendRow(row);
sh.deleteRow(e.range.rowStart);
}
}
}

Long processing time likely due to getValue and cell inserts

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

Categories

Resources