I have the following code in Google Apps Script. It loops through more than 800 rows to find empty cells of a column. But it takes too long. Is there a way to speed it up?
for(var t = 2; t <= lastrow; t++)
{
if (Geojson_Data.getRange(t,col1).getValue() == "")
{
Geojson_Data.deleteRow(t);
}
}
function delRowsWithNothingInColA() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg=sh.getRange(2,1,sh.getLastRow()-1,1);
var vA=rg.getValues();
var d=0;
for(var i=0;i<vA.length;i++) {
if(vA[i][0]=='') {
sh.deleteRow(i+2-d++)
}
}
}
var values = Geojson_Data.getDataRange().getValues();
var col1Arr = col1 - 1;
var t = values.length;
while(t--)
if(values[t][col1Arr] === '')
Geojson_Data.deleteRow(t + 1);
If you need a really tool then look at the snippet The script removes rows in batches without changing rows queue and without destroying formulas.
/**
* Runs the snippet.
* Removes rows by condition 'A:A=""'.
* #ignore
*/
function run() {
var sheet = SpreadsheetApp.getActiveSheet();
deleteRowsByConditional_(sheet, function(row) {
return row[0] === '';
});
}
function deleteRowsByConditional_(sheet, condition, action) {
var values = sheet.getDataRange().getValues();
values.unshift([]);
values.reverse().forEach(
function() {
var i = this.l - arguments[1];
if (this.condition.apply(null, [arguments[0], i, arguments[2]])) {
this.isContinue++;
} else if (this.isContinue) {
if (action) action(arguments[2], i, this.isContinue);
this.sheet.deleteRows(i, this.isContinue);
this.isContinue = 0;
}
},
{ sheet: sheet, condition: condition, isContinue: 0, l: values.length }
);
}
Related
I am trying to get data from Gmail body using GAS. To be specific, I get an email with a table content; I am trying to copy the table from gmail and write it to google sheet for my further analysis. Below is a sample email I get:
The output I am expecting in Google sheets:
UPDATE: I was able to make some modifications to the code I had by referring to Insert table from gmail to google spreadsheet by google script
Here's how the email body and output looks like now.
Email:
GSheet Output:
The issue occurs with merged cells in table. The code does not generate output as how it appears in the gmail body. Is there any workaround for this?
Final code:
var SEARCH_QUERY = "SearchKey";
function getEmailss_(q) {
var emails = [];
var threads = GmailApp.search(q);
if (threads.length == 0) {
console.log("No threads found that match the search query: " + q);
}
for (var i in threads) {
var msgs = threads[i].getMessages();
for (var j in msgs) {
var arrStr = msgs[j].getBody()
.replace(/<\/tr>/gm, '[/tr]')
.replace(/<\/td>/gm, '[/td]')
.replace(/<.*?>/g, '\n')
.replace(/^\s*\n/gm, '')
.replace(/^\s*/gm, '')
.replace(/\s*\n/gm, '\n')
.split("[/tr]");
if (arrStr.length == 1) {
console.log("No data found in thread: " + threads[i].getFirstMessageSubject());
}
var line = [];
for (var i = 0; i < arrStr.length - 1; i++) {
line = arrStr[i].split("[/td]");
line.length -= 1;
emails.push(line);
}
}
}
if (emails.length == 0) {
console.log("No emails found that match the search query: " + q);
}
return convert2ArrayToRectangular_(emails);
}
function convert2ArrayToRectangular_(array2d)
{
// get max width
var res = [];
var w = 0;
for (var i = 0; i < array2d.length; i++)
{
if (array2d[i].length > w) {w = array2d[i].length;}
}
var row = [];
for (var i = 0; i < array2d.length; i++)
{
row = array2d[i];
if(array2d[i].length < w)
{
for (var ii = array2d[i].length; ii < w; ii++)
{
row.push('');
}
}
res.push(row);
}
return res;
}
function appendData_(sheet, array2d) {
var h = array2d.length;
var l = array2d[0].length;
sheet.getRange(1, 1, h, l).setValues(array2d);
}
function saveEmailsss() {
var array2d = getEmailss_(SEARCH_QUERY);
if (array2d) {
appendData_(SpreadsheetApp.getActive().getSheetByName('Sheet1'), convert2ArrayToRectangular_(array2d));
}
markArchivedAsRead();
}
function markArchivedAsRead() {
var threads = GmailApp.search('label:inbox is:unread to:me subject:importnumberlist');
GmailApp.markThreadsRead(threads);
};
As another approach, how about using Sheets API? When Sheets API is used, the HTML table can be parsed by including the merged cells. The sample script is as follows.
Sample script:
This script uses Sheets API. Please enable Sheets API at Advanced Google services.
var SEARCH_QUERY = "SearchKey";
function getEmailss_(q, sheetName) {
var emails = [];
var threads = GmailApp.search(q);
if (threads.length == 0) {
console.log("No threads found that match the search query: " + q);
}
var tables = [];
for (var i in threads) {
var msgs = threads[i].getMessages();
for (var j in msgs) {
var arrStr = msgs[j].getBody();
var table = arrStr.match(/<table[\s\S\w]+?<\/table>/);
if (table) {
tables.push(table[0]);
}
}
}
if (emails.length == 0) {
console.log("No emails found that match the search query: " + q);
}
if (tables.length == 0) {
console.log("No tables.");
return
};
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(sheetName);
var requests = [{ pasteData: { html: true, data: tables.join(""), coordinate: { sheetId: sheet.getSheetId() } } }];
Sheets.Spreadsheets.batchUpdate({ requests }, ss.getId());
}
function saveEmailsss() {
var sheetName = "Sheet1"; // Please set your sheet name.
getEmailss_(SEARCH_QUERY, sheetName);
markArchivedAsRead();
}
When this script is run, the HTML table included in the email message is retrieved and put to the active Spreadsheet.
References:
Method: spreadsheets.batchUpdate
PasteDataRequest
The following code will run very slow in my Google Sheets because my getRange is too large. Is there a way to only loop through the columns that are merged? I only want the for loop to get the number of columns in "yourRange" that are merged.
function getUpfrontCosts() {
var sheet = SpreadsheetApp.getActive().getSheetByName('LPB_COST');
var cl , count=0;
var yourRange = sheet.getRange("H13:UV13");
for (var i = 1; i < yourRange.getNumColumns()+1; i++)
{
cl=yourRange.getCell(1, i);
if (cl.isPartOfMerge()){
if (cl.offset(15, 0).getBackground() == "#ff8300" && cl.getMergedRanges()[0].getCell(1, 1).getValue()=='Upfront Costs') {
count = count + cl.offset(15, 0).getValue();
}
else {
}
} else {
}
}
return count;
};
The second code is how I am trying to turn a string to a range. I am getting "Cell reference out of range" error
How can I change cl to not be a string and be a range?
function getUpfrontCosts()
{
var sheet = SpreadsheetApp.getActive().getSheetByName('LPB_COST');
var destSheet = SpreadsheetApp.getActive().getSheetByName('Top Level PN');
var cl , count=0;
var yourRange = sheet.getRange("I13:UZ13");
var mergedRanges = yourRange.getMergedRanges();
for (var i = 0; i < mergedRanges; i++){
}
var newRange = sheet.getRange(mergedRanges[i].getA1Notation());
Logger.log(newRange.getA1Notation());
for (var i = 0; i < newRange.getNumColumns()+1; i++){
cl=newRange.getCell(1, i);
Logger.log(newRange.getA1Notation());
if (cl.offset(15, 0).getBackground() == "#ff8300" && cl.getValue()=='Upfront Costs') {
count = count + cl.offset(15, 0).getValue();
}
else {
}
}
return count;
};
This is the line with the error
cl=newRange.getCell(1, i);
if you want to use merge cell range, you can do this:
function UntitledMacro1()
{
var sheet = SpreadsheetApp.getActive().getSheetByName('Sheet1');
var cl , count=0 ;
var yourRange = sheet.getRange("I13:UZ13");
var bb = yourRange.getMergedRanges();
for (a=bb[0].getColumn();a<bb[0].getLastColumn()+1;a++)
{
//Your actual columns from I13, for first merge range, here your cl,
//but if your range is ("13:13"), you don't need
//-sheet.getRange("I13").getColumn()+1
cl=yourRange.getCell(1, a-sheet.getRange("I13").getColumn()+1);
//For your offset 15
Logger.log(cl.offset(15, 0).getValue());
//on so on
}
};
I am trying to calculate some values using my own function (showing dummy function in this question), and I'd like the function to trigger when the user edits a cell in a given range, however even after writing the trigger the only time when the value is calculated again is when I go to the script editor and click Save.
My function:
function calculate(column) {
var sheet = SpreadsheetApp.getActiveSheet();
var column = sheet.getActiveCell().getColumn();
var values2d = sheet.getRange(1, 1, 15).getValues();
var values = [].concat.apply([], values2d);
var testRange2d = sheet.getRange(1, column, 15).getValues();
var testRange = [].concat.apply([], testRange2d);
var sum = 0;
for (var i = 0; i < 15; i++) {
if (testRange[i] == true) sum += values[i]
}
return sum;
}
function onEdit(e) {
var activeCell = SpreadsheetApp.getActiveSheet().getActiveCell();
if (activeCell.getBackground() === "#e3f5a2") activeCell.setBackground("#ffffff");
else activeCell.setBackground("#e3f5a2");
calculate();
}
And the sheet:
The cell besides total is where I call my function. And the value will only be updated either manually or as described earlier, but even when I trigger an event, I have also tried doing other actions in the trigger like changing a random cells value and that worked however.
Might i suggest the following. This will only occur if a checkbox in column 2 is changed.
function onEdit(event) {
try {
var sheet = event.source.getSheetByName("Sheet1")
if( event.range.getSheet().getName() === "Sheet1" ) {
if( event.range.getColumn() === 2 ) {
var values = sheet.getRange(1,1,15,2).getValues();
var sum = 0;
for( var i=0; i<values.length; i++ ) {
if( values[i][1] ) sum += values[i][0];
}
sheet.getRange(16,2,1,1).setValue(sum);
if( event.range.getBackground() === "#e3f5a2" )
event.range.setBackground("#ffffff");
else
event.range.setBackground("#e3f5a2");
}
}
}
catch(err) {
SpreadsheetApp.getUi().alert(err);
}
}
I have the code as below that currently deletes the entire row if 'delete' is in the 4th cell of that row. Is it posible to just delete the 3 cells to the left as well as the cell I have written 'delete' in, rather than the whole row (I have other content in Cell B16 for example I don't want to delete?
I haven't been able to find a .deleteCell function. Is there another way to do this?
function deleteRows() {
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getDataRange();
var numRows = rows.getNumRows();
var values = rows.getValues();
var rowsDeleted = 0;
for (var i = 0; i <= numRows - 1; i++) {
var row = values[i];
if (row[4] == 'delete') { // This searches all cells in columns A (change to row[1] for columns B and so on) and deletes row if cell is empty or has value 'delete'.
sheet.deleteRow((parseInt(i)+1) - rowsDeleted);
rowsDeleted++;
}
}
};
Delete with Shift Up or Shift Left
This will delete a cell and shift the data up or left depending upon the shift parameter.
function delCellAndShift(row,column,shift)
{
if(row && column)
{
var shift=(typeof(shift)!='undefined')?shift:'left';
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg=sh.getDataRange()
var vA=rg.getValues();
if(shift=='left')
{
var tA=vA[row-1];
tA.splice(column-1,1);
tA.push('');
vA[row-1]=tA;
rg.setValues(vA);
}
if(shift=='up')
{
var vB=Object.keys(vA[0]).map(function (c) { return vA.map(function (r) { return r[c]; }); });
var tA=vB[column-1];
tA.splice(row-1,1);
tA.push('');
vB[column-1]=tA;
vA=Object.keys(vB[0]).map(function (c) { return vB.map(function (r) { return r[c]; }); });
rg.setValues(vA);
}
}
else
{
var s='Invalid Inputs for delCellAndShift.<br /><input type="button" value="Close" onClick="google.script.host.close();" />';
var ui=HtmlService.createHtmlOutput(s);
SpreadsheetApp.getUi().showModalDialog(us, 'Invalid Inputs')
}
}
I tested it using the following functions to setup my data and run the main function.
function testDelCell()
{
delCellAndShift(3,7,'up');
}
function testsetupfordelcell()
{
var rs=10;
var cs=10;
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rg=sh.getRange(1,1,rs,cs);
var vA=rg.getValues();
for(var i=0;i<rs;i++)
{
for(var j=0;j<cs;j++)
{
vA[i][j]=Utilities.formatString('%s,%s',i,j)
}
}
rg.setValues(vA);
}
This is a sample of it shifting left:
This is a sample of it shifting up:
So the problem is:
How do I move all the cells beneath a given cell one row up (overwriting the given cell value)?
The code to do this would be:
function deleteCell(sheet, cellx, celly){
var wholeSheet = SpreadsheetApp.openById(sheet).getRange("A1:F").getValues();
for(var i = celly; i<wholeSheet.length; i++){
wholeSheet[cellx][i] = wholeSheet[cellx][i+1];
}
SpreadsheetApp.openById(sheet).setValues(wholeSheet);
}
I would like to use a formula inside a custom function, like this for example:
function myFunction(range, value) {
var countNumber = COUNTIF(range; value); // COUNTIF is a formula that can be used in the spreadsheet
if (countNumber > 0) {
return "RESULT";
} else {
return "OTHER RESULT";
}
}
And then:
=MYFUNCTION(A1:A5,"VALUETOTEST")
I would like to simplify a huge formula:
Something like:
=IF(SUM(COUNTIFS(G182:G186;"ERROR";H182:H186;"62");COUNTIFS(G182:G186;"ERROR";H182:H186;"ALL"))>0;"ERRO";IF(SUM(COUNTIFS(G182:G186;"RETEST";H182:H186;"62");COUNTIFS(G182:G186;"RETEST";H182:H186;"TODOS"))>0;"RETEST";IF(COUNTIF(G182:G186;"UNIMPLEMENTED")>0;"UNIMPLEMENTED";"SOLVED")))
You have three ways of performing these actions.
Add the Sheet Formulas to the sheet itself in the ranges that you need. Then read the data from the result cells (wherever you set it to write to) using your GAS Function. You can then perform further processing using the results.
Use your GAS function to write Sheet Formulas into your sheet. Then use more GAS to read that result and process the data. The method for this can be found here: https://developers.google.com/apps-script/reference/spreadsheet/range#setFormula(String)
You can create a Custom Sheet Formula using GAS that you then use in your sheet. GAS can then read that result and process the information. This will require some research into JS as a whole to know how to recreate, combine, and perform the operations that you need the data in the sheet to perform.
You can find a guide to make Custom Formulas here: https://developers.google.com/apps-script/guides/sheets/functions
And a guide to JS here: http://www.w3schools.com/js/default.asp
W3 Schools has a quite comprehensive guide to JS. GAS uses all native JS methods as it is a JS coding environment. Check the GAS Reference for more on GAS-specific methods that may perform what you need.
If what you need is to check conditions and/or iterate through rows, try something like this:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var range = sheet.getRange(startRow, startColumn, numRows, numColumns);
var values = range.getValues(); //This is a 2D array; iterate appropriately
for (i = 0; i < values.length; i++) {
if (values[i] == conditionToCheck) {
//perform code..OR
//continue; <- This works to skip the row if the condition is met
} else {
//perform alternate code if condition is not met
}
}
}
As I mentioned, .getValues() creates a 2D array. If you need to iterate through columns and rows, you will need 2 for() loops like so:
for (i = 0; i < values.length; i++) { //iterates through the rows
for(j = 0; j < values[i].length; j++) { //iterates through the columns in that current row
It is important to mention how GAS handles 2D arrays. values[i][j] denotes how much i rows there are and j columns. You can visualize like so:
values = [[A1, B1, C1],[A2, B2, C2],[A3, B3, C3]]
This is an array of arrays where the outer array is an array of the rows, while the insides are an array of cell values by column in that row.
Custom functions in google apps script do not have access to spreadsheet function. You may try using this =IF(COUNTIF(A1:A5,"VALUETOTEST")>0,"RESULT","OTHER RESULT")
If there is a huge formula for result, try creating functions for the result
function result1() {
return "RESULT";
}
function result2() {
return "OTHER RESULT";
}
Then use this =IF(COUNTIF(A1:A5,"VALUETOTEST")>0,RESULT1(),RESULT2())
Try this - copy the below function in apps script, and use this as Formula =myFunction("G182:G186","H182:H186") remeber to ensclose the range with ' " ' because you will be passing the range as string, and note both the ranges must be of equal length.
function myFunction(aRange, bRange) {
var cond_1 = "ERROR";
var cond_2 = "62";
var cond_3 = "ALL";
var cond_4 = "RETEST";
var cond_5 = "TODOS";
var cond_6 = "UNIMPLEMENTED";
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var aRange = sheet.getRange(aRange);
var aValues = aRange.getValues();
var bRange = sheet.getRange(bRange);
var bValues = bRange.getValues();
var count = 0;
var tmplength = 0;
if (aValues.length != bValues.length) {
return "Range length does not Match";
}
for (i = 0; i < aValues.length; i++) {
if (aValues[i] == cond_1 && bValues[i] == cond_2) {
count += 1;
}
if (aValues[i] == cond_1 && bValues[i] == cond_3) {
count += 1;
}
if (count > 0) {
return "ERROR";
} else {
count = 0;
if (aValues[i] == cond_4 && bValues[i] == cond_2) {
count += 1;
}
if (aValues[i] == cond_4 && bValues[i] == cond_5) {
count += 1;
}
if (count > 0) {
return "RETEST";
} else {
count = 0;
if (aValues[i] == cond_6) {
count += 1;
}
if (count > 0) {
return "UNIMPLEMENTED";
} else {
return "SOLVED";
}
}
}
}
}
This is how I solved my problem. I thank to people who helped me to reach this result!
// Like COUNTIFS
var countConditionals = function(cells, condition1, condition2) {
var count = 0;
for (i = 0; i < cells.length; i++) {
if (cells[i][0] == condition1 && cells[i][1] == condition2) {
count++;
}
}
return count;
}
// Like COUNTIF
var countConditional = function(cells, condition) {
var count = 0;
for (i = 0; i < cells.length; i++) {
if (cells[i][0] == condition) {
count++;
}
}
return count;
}
//Whole Formula
function verificaStatus(cells, db) {
const ERROR = "ERROR";
const ALL = "ALL";
const RETEST = "RETEST";
const NOTYET = "UNIMPLEMENTADED";
const SOLVED = "SOLVED";
var countErrors = countConditionals(cells, ERROR, db);
var countErrorsAll = countConditionals(cells, ERROR, ALL);
var sumErrors = countErrors + countErrorsAll;
if (sumErrors > 0) {
return ERROR;
} else {
var retest = countConditionals(cells, RETEST, db);
var retestAll = countConditionals(cells, RETEST, db);
var sumRetest = retest + retestAll;
if (sumRetest > 0) {
return RETEST;
} else {
var countNonCreated = countConditional(cells, NOTYET);
if (countNonCreated > 0) {
return NOTYET;
}
}
}
return SOLVED;
}