Sum cells if they are not bold - javascript

I'm confused with my Google Apps script which is purposed to calculate the sum of the cells only if these cells are bold.
Here is the source:
function SumIfNotBold(range, startcol, startrow){
// convert from int to ALPHANUMERIC
// - thanks to Daniel at http://stackoverflow.com/a/3145054/2828136
var start_col_id = String.fromCharCode(64 + startcol);
var end_col_id = String.fromCharCode(64 + startcol + range[0].length -1);
var endrow = startrow + range.length - 1
// build the range string, then get the font weights
var range_string = start_col_id + startrow + ":" + end_col_id + endrow
var ss = SpreadsheetApp.getActiveSpreadsheet();
var getWeights = ss.getRange(range_string).getFontWeights();
var x = 0;
var value;
for(var i = 0; i < range.length; i++) {
for(var j = 0; j < range[0].length; j++) {
if(getWeights[i][j].toString() != "bold") {
value = range[i][j];
if (!isNaN(value)){
x += value;
}
}
}
}
return x;
Here is the formula:
=(SumIfNotBold(K2:K100,COLUMN(K2), ROW(K2)))*1
I have three major concerns:
When I set up a trigger to launch this script on any edits I accidentally receive an email from Google Apps stating that
TypeError: Cannot read property "length" from undefined. (line 7, file
"SumIfNotBold")
Thus, how can I fix it? Are there any ways to ignore these automatically delivered notifications?
The formula doesn't calculate the sum of cells if they are on the other list. For example, if I put the formula on B list but the cells are located on A list then this script doesn't work properly in terms of deriving wrong calculations.
When the cell values are updated the formula derivation is not. In this case I'm refreshing the formula itself (i.e., changing "K2:K50" to "K3:K50" and once back) to get an updated derivation.
Please, help me with fixing the issues with this script. Or, if it would be better to use a new one to calculate the sum in non-bold cells then I'll be happy to accept your new solution.

Here is a version of this script that addresses some of the issues you raised. It is invoked simply as =sumifnotbold(A3:C8) or =sumifnotbold(Sheet2!A3:C8) if using another sheet.
As any custom function, it is automatically recalculated if an entry in the range to which it refers is edited.
It is not automatically recalculated if you change the font from bold to normal or back. In this case you can quickly refresh the function by delete-undo on any nonempty cell in the range which it sums. (That is, delete some number, and then undo the deletion.)
Most of the function gets a reference to the passed range by parsing the formula in the active cell. Caveat: this is based on the assumption that the function is used on its own, =sumifnotbold(B2:C4). It will not work within another function like =max(A1, sumifnotbold(B2:C4).
function sumifnotbold(reference) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
var formula = SpreadsheetApp.getActiveRange().getFormula();
var args = formula.match(/=\w+\((.*)\)/i)[1].split('!');
try {
if (args.length == 1) {
var range = sheet.getRange(args[0]);
}
else {
sheet = ss.getSheetByName(args[0].replace(/'/g, ''));
range = sheet.getRange(args[1]);
}
}
catch(e) {
throw new Error(args.join('!') + ' is not a valid range');
}
// everything above is range extraction from the formula
// actual computation begins now
var weights = range.getFontWeights();
var numbers = range.getValues();
var x = 0;
for (var i = 0; i < numbers.length; i++) {
for (var j = 0; j < numbers[0].length; j++) {
if (weights[i][j] != "bold" && typeof numbers[i][j] == 'number') {
x += numbers[i][j];
}
}
}
return x;
}

Related

Google Script working as 2 separate scripts but not inside the same function

Basically I have a script that is in 4 blocks:
1. Copies within a range each row provided it meets a criteria
2. Removes all empty rows
3. Sets all numbers as percentage
4. Applies conditional cell formatting to one of the columns
The 4th part is the one that is causing me issues. The script runs without any error message AND block 4 works perfectly fine if it's in another script alone with the same variables defined but as soon as it is inside the same function as the others it simply doesn't run without any error message of any kind.
Tried changing the name of the variables to single use ones to ensure it wasn't because one of the "var" was modified above it, removing the "else if" to keep only an "if" in the loop, moving it around to other parts of the script but if the block 1 is in the script then block 4 won't apply (will apply if it is only with 2 & 3.
2 & 3 which follow the same structure work well with 1.
Does any one have any clue what's wrong with my script ? :)
Each block is commented with what it does
function copy() {
//Set variables & criterion to choose which rows to copy
var s = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/1bEiLWsbFszcsz0tlQudMBgTk5uviyv_wDx7fFa8txFM/edit');
var ssSource = s.getSheetByName('Variations');
var ssDest = s.getSheetByName('Email');
var lastRowSource = ssSource.getLastRow();
var lastRowDest = ssDest.getLastRow();
var lastColSource = ssSource.getLastColumn()
var criteria = 0;
var titles = ssSource.getRange(1,1,1, lastColSource).getValues()
//Copies the range
ssDest.getRange(1,1,1, lastColSource).setValues(titles)
for (var i = 2; i < lastRowSource; i++ ) {
var test = ssSource.getRange(i ,1);
Logger.log(test.getValue()+ ' <? ' + criteria);
if (ssSource.getRange(i ,6).getValue() > criteria) {
ssSource.getRange(i ,1,1,ssSource.getLastColumn()).copyTo(ssDest.getRange(i ,1,1,ssSource.getLastColumn()), {contentsOnly:true}); // copy/paste content only
}
}
//Removes empty rows
var data = ssDest.getDataRange().getValues();
var targetData = new Array();
for(n=0;n<data.length;++n){
if(data[n].join().replace(/,/g,'')!=''){ targetData.push(data[n])};
Logger.log(data[n].join().replace(/,/g,''))
}
ssDest.getDataRange().clear();
ssDest.getRange(1,1,targetData.length,targetData[0].length).setValues(targetData);
//Formats numbers as percentages
var rangePercent = ssDest.getRange(1,1,ssDest.getLastRow(),ssDest.getLastColumn());
var rowsPercent = rangePercent.getNumRows();
var colsPercent = rangePercent.getNumColumns();
for(var rowPercent = 1; rowPercent <= rowsPercent; rowPercent++) {
for(var colPercent = 1; colPercent <= colsPercent; colPercent++) {
var cellPercent = rangePercent.getCell(rowPercent, colPercent);
var valuePercent = cellPercent.getValue();
if(typeof(valuePercent) == 'number') {
cellPercent.setNumberFormat("##.#%");
}
}
}
//Adds conditional background colours
for (var z = 2; z < lastRowDest+1;z++) {
var avgCpc = 4;
var rangeColour = ssDest.getRange(z,avgCpc);
var dataColour = rangeColour.getValue()
if (dataColour < 0) {
ssDest.getRange(z,avgCpc).setBackground('#d9ead3')
}
else if (dataColour > 0) {
ssDest.getRange(z,avgCpc).setBackground('#f4cccc')
}
}
//Centers Values
}
The problem you're having is your code has performance issues because you're calling too many times methods such as getRange() and getValue() inside various loops, therefore Apps Script can't keep up with all those calls. Please check Best Practices.
Having said that, I modified your code in order to make it more efficient. Besides your copy function, I added two more functions to make the code more readable.
function copy
As before this function sets the variables, but now it calls two other functions, which are setPositiveCostValues and formatCells
function copy() {
//Set variables & criterion to choose which rows to copy
var ss = SpreadsheetApp.openByUrl('your-url');
var ssSource = ss.getSheetByName('Variations');
var ssDest = ss.getSheetByName('Email');
// set the title
var titles = ssSource.getRange(1,1,1, ssSource.getLastColumn()).getValues();
ssDest.getRange(1,1,1, ssSource.getLastColumn()).setValues(titles);
// get the positive values you want from the cost col
var positiveValues = setPositiveCostValues(ssSource, ssDest, ssSource.getLastRow());
// fomrat the cells you want as percentage and set the color
formatCells(ssDest, positiveValues);
}
function setPositiveCostValues
This will take the values where the cost is positive and it will get rip off of the cells with empty values and "n/a" values.
function setPositiveCostValues(ssSource,ssDest, lastRowSource){
var postiveCost = ssSource.getRange(2, 1, lastRowSource, 6).getValues();
// this loop will clean the empty elements and the ones that only have n/a
for (var i = postiveCost.length - 1; i >= 0; i--) {
if (postiveCost[i][0]) {
postiveCost.splice(i + 1, postiveCost.length - (i + 1));
postiveCost = postiveCost.filter(function(el){ return el != 'n/a'})
break;
}
}
return postiveCost;
}
function formatCells
This will format the cells in the cost col as a percentage and will set the right color in your avgCpc col.
function formatCells(ssDest, postiveCost){
var avgCpc = 4, cost = 6, row = 2, criteria = 0;
// iterate over the array and depending on the criteria format the cells
postiveCost.forEach(function(el){
if(el[cost - 1] > criteria){
var ssDestRange = ssDest.getRange(row, 1, 1, cost);
ssDestRange.setValues([el]);
ssDestRange.getCell(1, cost).setNumberFormat("##.#%");
// set the color depending on the avgCpc value condition
if(el[avgCpc - 1] < criteria) ssDest.getRange(row, avgCpc).setBackground('#d9ead3');
else ssDest.getRange(row, avgCpc).setBackground('#f4cccc');
row++;
}
});
}
Code
Your whole code now it will look like this:
function copy() {
//Set variables & criterion to choose which rows to copy
var ss = SpreadsheetApp.openByUrl('your-url');
var ssSource = ss.getSheetByName('Variations');
var ssDest = ss.getSheetByName('Email');
// set the title
var titles = ssSource.getRange(1,1,1, ssSource.getLastColumn()).getValues();
ssDest.getRange(1,1,1, ssSource.getLastColumn()).setValues(titles);
// get the positive values you want from the cost col
var positiveValues = setPositiveCostValues(ssSource, ssDest, ssSource.getLastRow());
// fomrat the cells you want as percentage and set the color
formatCells(ssDest, positiveValues);
}
function setPositiveCostValues(ssSource,ssDest, lastRowSource){
var postiveCost = ssSource.getRange(2, 1, lastRowSource, 6).getValues();
// this loop will clean the empty elements and the ones that only have n/a
for (var i = postiveCost.length - 1; i >= 0; i--) {
if (postiveCost[i][0]) {
postiveCost.splice(i + 1, postiveCost.length - (i + 1));
postiveCost = postiveCost.filter(function(el){ return el != 'n/a'})
break;
}
}
return postiveCost;
}
function formatCells(ssDest, postiveCost){
var avgCpc = 4, cost = 6, row = 2, criteria = 0;
// iterate over the array and depending on the criteria format the cells
postiveCost.forEach(function(el){
if(el[cost - 1] > criteria){
var ssDestRange = ssDest.getRange(row, 1, 1, cost);
ssDestRange.setValues([el]);
ssDestRange.getCell(1, cost).setNumberFormat("##.#%");
// set the color depending on the avgCpc value condition
if(el[avgCpc - 1] < criteria) ssDest.getRange(row, avgCpc).setBackground('#d9ead3');
else ssDest.getRange(row, avgCpc).setBackground('#f4cccc');
row++;
}
});
}

Google Script stops running because there are no passed arguments

I have a function which I have been using in google sheets for ages and it worked just fine. After I created a new sheet document and added the function, it stopped working. I have been trying to figure out what has google changed in their API but without any success. This is my function:
/**
* Sums a cell across all sheets
*
* #param {string} cell The cell (i.e. "B2")
* #return number
* #customfunction
*/
function sumAllSheets(cell)
{
var sum = 0;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var activeSheet = ss.getActiveSheet().getName();
var sheets = ss.getSheets();
for (var i=1; i < sheets.length; i++) {
var sheet = sheets[i];
var sheetName = sheet.getName();
var sheetRange = sheet.getRange(cell);
sum += parseInt(sheetRange.getValue());
}
return sum;
}
What is basically does is it goes through all of the sheets and sums the the value from a single cell (i.e. cell B2 in all sheets). In previous versions I was able to pass the argument cell as a string by calling the function like so =sumAllSheets("B2"). This worked just fine. However with the new update the cell argument is undefined. Any ideas what's wrong or has been changed? I couldn't find anything in the google documentation.
Are you excluding the first sheet from the sum? If not, "i" should be set to 0 in the first statement of your loop. Also try this variation of your code
function sumAllSheets(cell) {
var sum = 0;
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
for (var i = 0; i < sheets.length; i++) {
var v = sheets[i].getRange(cell)
.getValue();
sum += parseInt(v ? v : 0);
}
return sum;
}
Here's an alternative version
function sumCellOnAllSheets(cell) {
return SpreadsheetApp.getActive()
.getSheets()
.map(function (sh) {
return Number(sh.getRange(cell)
.getValue())
})
.reduce(function (a, b) {
return a + b;
})
}

Iterating over cells in Google Spreadsheet Custom Function

I am trying to build a custom function in Google Spreadsheet, that would basically do this:
- say custom function called func() is placed in cell D2 and invoked as =func(B2)
- provided a particular cell reference (say B2) as a starting point it would iterate over all fields that follow B2 down the column (so B3, B4, B5) while the value of those fields equals to a particular symbol (say pipe |).
- For each iteration where this condition succeeds (i.e B3 == '|') it could add up/aggregate values from the cell it was placed, down the column. So if cells B3, B4,B5 contain | and then B6 doesn't it would return value of D3+D4+D5.
So for example if in this spreadsheet:
In the cells B10 the function should produce value of 8 (1+3+4) and in the cell B15 the function should produce value of 11 (5+6).
I've came up with something like this:
function sumByPipe(startRange) {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(startRange)
var sum = 0;
for (var row_num = 1; row_num < 128; row_num ++) {
var cell = range.getCell(row_num, 1);
var cellValue = cell.getValue();
if (cellValue == '|') {
sum += 1;
}
}
return sum;
}
and got stuck in 2 places really:
Function seems to work in the debugger, but when I invoke it from the spreadsheet it fails on the getRange() function call saying no such range exist. If I replace it with static call of say getRange('A2') that part works but then it fails on the getCell() saying index out of range.
How do I actually get the value of the next cell down the column from where the function itself is placed?
Really quite lost on these two and would appreciate any advice. Thank you!
This works. I tested it:
function sumByPipe(startRange) {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(startRange)
var sum = 0;
var startColumn = range.getColumn();
var startRow = range.getRow();
for (var row_num = startRow; row_num < startRow+128; row_num++) {
var cellWithPipe = sheet.getRange(row_num, startColumn-1).getValue();
var cellValue = sheet.getRange(row_num, startColumn).getValue();
if (cellWithPipe === '|') {
sum += cellValue;
} else {
//If pipe is no longer present, stop and return sum
return sum;
}
}
}

Pull data from one Google spreadsheet to another using Google script

I have 2 spreadsheet in my Drive. I want to pull data from a cell in 1 spreadsheet and copy it in another.
The spreadsheet "TestUsage" will sometimes have data in column A, but none is column B.
I would like so that when I open the spreadsheet, it would populate that empty cell in sheet "TestUsage" from sheet "TestParts".
Here is my code:
var ssTestUsage = SpreadsheetApp.getActiveSpreadsheet();
var sTestUsage = ssTestUsage.getActiveSheet();
var lastRowTestUsage = sTestUsage.getLastRow();
var rangeTestUsage = sTestUsage.getSheetValues(1, 1, lastRowTestUsage, 4);
var TESTPARTS_ID = "1NjaFo0Y_MR2uvwit1WuNeRfc7JCOyukaKZhuraWNmKo";
var ssTestParts = SpreadsheetApp.openById(TESTPARTS_ID);
var sTestParts = ssTestParts.getSheets()[0];
var lastRowTestParts = sTestParts.getLastRow();
var rangeTestParts = sTestParts.getSheetValues(1, 1, lastRowTestParts, 3);
function onOpen() {
for (i = 2; i < lastRowTestUsage; i++) {
if (rangeTestUsage[i][0] !== "" && rangeTestUsage[i][1] == "") {
for (j = 1; j < lastRowTestParts; j++) {
if (rangeTestUsage[i][0] == rangeTestParts[j][0]) {
Logger.log(rangeTestUsage[i][1]);
Logger.log(rangeTestParts[j][1]);
rangeTestUsage[i][1] = rangeTestParts[j][1];
break;
}
}
}
}
}
The problem with this is this doesn't do anything:
rangeTestUsage[i][1] = rangeTestParts[j][1];
I know there must be a method that can get data from one range to another.
Please let me know if I am totally incorrect or I am on the right path.
the statement
"this doesn't do anything:"
rangeTestUsage[i][1] = rangeTestParts[j][2];
is not really true... and not really false neither..., actually it does assign the value to rangeTestUsagei but you dont see it because it is not reflected in the spreadsheet.
Both values are taken from the Sheet using getValues so at that time they are both array elements.
What is missing is just writing back the array to the sheet using the symetrical statement setValues()
Give it a try and don't hesitate to come back if something goes wrong.
EDIT :
I didn't notice at first that you were using getSheetValues instead of getValues (simply because I never use this one)... the only difference is that getValues is a method of the range class while yours belongs to the sheet class; the syntax is similar in a way, just use
Sheet.getRange(row,col,width,height).getValues()
it takes one word more but has the advantage to have a direct equivalent to set values
Sheet.getRange(row,col,width,height).setValues()
Serge insas has a good explanation of why your code doesn't work and hints at the solution below.
I recommend you use an array to store the updated values of column B that you want then set the entire column at the end.
Modifying your code...
var ssTestUsage = SpreadsheetApp.getActiveSpreadsheet();
var sTestUsage = ssTestUsage.getActiveSheet();
var lastRowTestUsage = sTestUsage.getLastRow();
var rangeTestUsage = sTestUsage.getSheetValues(1, 1, lastRowTestUsage, 2);
var TESTPARTS_ID = "1NjaFo0Y_MR2uvwit1WuNeRfc7JCOyukaKZhuraWNmKo";
var ssTestParts = SpreadsheetApp.openById(TESTPARTS_ID);
var sTestParts = ssTestParts.getSheets()[0];
var lastRowTestParts = sTestParts.getLastRow();
var rangeTestParts = sTestParts.getSheetValues(1, 1, lastRowTestParts, 2);
var colB = [];
function onOpen() {
for (i = 2; i < lastRowTestUsage; i++) {
if (rangeTestUsage[i][0] !== "" && rangeTestUsage[i][1] == "") {
var matched = false;
for (j = 1; j < lastRowTestParts; j++) {
if (rangeTestUsage[i][0] == rangeTestParts[j][0]) {
//Logger.log(rangeTestUsage[i][1]);
//Logger.log(rangeTestParts[j][1]);
colB.push([rangeTestParts[j][1]]); // push the value we want into colB array
matched = true;
break;
}
}
if(!matched) // this is in case you don't have a match
colB.push([""]); // incase we don't have a matching part
} else {
colB.push([rangeTestUsage[i][0]]); // we already have a value we want so just add that to colB array
}
}
sTestUsage.getRange(2,2,lastRowTestUsage).setValues(colB); // update entire column b with values in colB array
}

Writing an array in Google Spreadsheets

I'm trying to learn javascript, so I decided to code a script in Google Apss Script to list all emails with attachment. Until now, I have this code:
function listaAnexos() {
// var doc = DocumentApp.create('Relatório do Gmail V2');
var plan = SpreadsheetApp.create('Relatorio Gmail');
var conversas = GmailApp.search('has:attachment', 0, 10)
var tamfinal = 0;
if (conversas.length > 0) {
var tam = 0
var emails = GmailApp.getMessagesForThreads(conversas);
var cont = 0;
for (var i = 0 ; i < emails.length; i++) {
for (var j = 0; j < emails[i].length; j++) {
var anexos = emails[i][j].getAttachments();
for (var k = 0; k < anexos.length; k++) {
var tam = tam + anexos[k].getSize();
}
}
var msginicial = conversas[i].getMessages()[0];
if (tam > 0) {
val = [i, msginicial.getSubject(), tam];
planRange = plan.getRange('A1:C1');
planRange.setValue(val);
// doc.getBody().appendParagraph('A conversa "' + msginicial.getSubject() + '" possui ' + tam + 'bytes em anexos.');
}
var tamfinal = tamfinal + tam;
var tam = 0;
}
}
}
listaAnexos();
It works, but with 2 problems:
1) It writes the three val values at A1, B1 and C1. But I want to write i in A1, msginicial.getSubject() in B1 and tam in C1.
2) How can I change the range interactively? Write the first email in A1:C1, the second in A2:C2 ...
I know that are 2 very basic questions, but didn't found on google :(
Problem 1: Make sure you use the right method for the range. You've used Range.setValue() which accepts a value as input, and modifies the content of the range using that one value. You should have used Range.setValues(), which expects an array and modifies a range of the same dimensions as the array. (The array must be a two-dimensional array, even if you're only touching one row.)
val = [[i, msginicial.getSubject(), tam]];
planRange = plan.getRange('A1:C1');
planRange.setValues(val);
Problem 2: (I assume you mean 'programmatically' or 'automatically', not 'interactively'.) You can either use row and column numbers in a loop say, with getRange(row, column, numRows, numColumns), or build the range string using javascript string methods.

Categories