I'm trying to write a simple script to clear the values from a specific Google Sheets range [H29:H] if the background color of the cell = #ffff00. While the script doesn't return any errors, it isn't making any changes to cells that meet those criteria (or anywhere else in the sheet). Any guidance as to where I am going wrong?
function resetCells() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Test Sheet');
var rangeData = sheet.getDataRange();
var lastRow = rangeData.getLastRow();
var searchRange = sheet.getRange('H29:H');
var rangeColors = searchRange.getBackgrounds();
for ( i = 29 ; i < lastRow - 1; i++){
if(rangeColors[i][8] === '#ffff00'){
sheet.getRange(i,8).clearContent();
};
};
}
When I saw your script, I thought that the index of rangeColors might not be corresponding to i in the for a loop. I thought that this might be the reason for your issue. In this case, how about the following modification?
Modified script:
From:
for ( i = 29 ; i < lastRow - 1; i++){
if(rangeColors[i][8] === '#ffff00'){
sheet.getRange(i,8).clearContent();
};
};
To:
for (i = 0; i < rangeColors.length; i++) {
if (rangeColors[i][0] === '#ffff00') {
sheet.getRange(i + 29, 8).clearContent();
}
}
or, in your situation, when sheet.getRange(i + 29, 8).clearContent() is used at outside of the loop, the process cost might be able to be reduced a little. In this case, please modify as follows.
var rangeList = rangeColors.reduce((ar, [h], i) => {
if (h === '#ffff00') ar.push("H" + (i + 29));
return ar;
}, []);
sheet.getRangeList(rangeList).clearContent();
In this modification, when the background color of the cell of column "H" is #ffff00, the cell content is clear.
Reference:
Class RangeList
What you did wrong
In rangeColors[i][8] you were assuming rangeColors related to the cells in the spreadsheet. But they actually relate to the searchRange you made. So rangeColors[i][8]should be rangeColors[i][0]. Where i starts at 0.
To make it "extra fun" for you rangeColors is an array which is zero-based indexed. And when you are working with a range you can retreive the first cell with range.getCell(1, 1), not range.getCell(0, 0).
Because I got a little carried away I wrote this solution ( may be an interesting extra to read )
I don't think mapping an array of arrays (rangeColors) to a range is a great way to do this; It's hard to keep track on what your code is doing.
What I did was:
Create a range to check
Generate column and row numbers for all the cells in the range. Note that these are relative to the range, not the stylesheet. So range.getCell(1, 1) is the top left cell in the range. This is the (main) mistake you made in your code.
Do whatever you want with every individual cell in the range.
code:
resetCells( '#ffff00' );
function resetCells( color ) {
const app= SpreadsheetApp.getActiveSpreadsheet();
const sheet = app.getSheetByName('Sheet1');
const range = sheet.getRange('F4:F50');
for( let row = 1; row < range.getNumRows() + 1; row++ ) {
for( let column = 1; column < range.getNumColumns() + 1; column++ ) {
const cell = range.getCell( row, column );
if( cell.getBackground() === color )
cell.clearContent();
}
}
}
Disclaimer: I never used this google-app-script before. So I may have done something wrong performance-wise; iterating a large amount of cells takes a little long;
Related
There are two ways that i am able to add an auto increment column. By auto-increment, i mean that if column B has a value, column A will be incremented by a numeric value that increments based on the previous rows value.
The first way of doing this is simple, which is to paste a formula like the one below in my first column:
=IF(ISBLANK(B1),,IF(ISNUMBER(A1),A1,0)+1)
The second way i have done this is via a GA script. What i found however is performance using a GA script is much slower and error prone. For example if i pasted values quickly in the cells b1 to b10 in that order, it will at times reset the count and start at 1 again for some rows. This is because the values for the previous rows have not yet been calculated. I assume that this is because the GA scripts are probably run asynchronously and in parallel. My question is..is there a way to make sure each time a change happens, the execution of this script is queued and executed in order?
OR, is there a way i should write this script to optimize it?
function auto_increment_col() {
ID_COL = 1;
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
//only increment column 1 for sheets in this list
var auto_inc_sheets = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("auto_inc_sheets").getValues();
auto_inc_sheets = auto_inc_sheets.map(function(row) {return row[0];});
var is_auto_inc_sheet = auto_inc_sheets.indexOf(spreadsheet.getSheetName()) != -1;
if (!is_auto_inc_sheet) return;
var worksheet = spreadsheet.getActiveSheet();
var last_row = worksheet.getLastRow();
var last_inc_val = worksheet.getRange(last_row, ID_COL).getValue();
//if auto_inc column is blank and the column next to auto_inc column (col B) is not blank, then assume its a new row and increment col A
var is_new_row = last_inc_val == "" && worksheet.getRange(last_row, ID_COL+1).getValue() != "";
Logger.log("new_row:" + is_new_row + ", last_inc_val:" + last_inc_val );
if (is_new_row) {
var prev_inc_val = worksheet.getRange(last_row-1, ID_COL).getValue();
worksheet.getRange(last_row, ID_COL).setValue(prev_inc_val+1);
}
}
There is my vision of auto increment https://github.com/contributorpw/google-apps-script-snippets/tree/master/snippets/spreadsheet_autoincrement
The main function of this is
/**
*
* #param {GoogleAppsScript.Spreadsheet.Sheet} sheet
*/
function autoincrement_(sheet) {
var data = sheet.getDataRange().getValues();
if (data.length < 2) return;
var indexCol = data[0].indexOf('autoincrement');
if (indexCol < 0) return;
var increment = data.map(function(row) {
return row[indexCol];
});
var lastIncrement = Math.max.apply(
null,
increment.filter(function(e) {
return isNumeric(e);
})
);
lastIncrement = isNumeric(lastIncrement) ? lastIncrement : 0;
var newIncrement = data
.map(function(row) {
if (row[indexCol] !== '') return [row[indexCol]];
if (row.join('').length > 0) return [++lastIncrement];
return [''];
})
.slice(1);
sheet.getRange(2, indexCol + 1, newIncrement.length).setValues(newIncrement);
}
But you have to open the snippet for details because this doesn't work without locks.
First off, I am not a coder at all, just a teacher who's handy at googling things to make life easier. In my attendance book, I bold the times a student comes in tardy (they get a 1 if present and a 0 if absent in order to calculate attendance rate).
I found an awesome script that allows me to count the number of bold items in a range. However, the range is set and I can't specify a new range within google sheets for each student as is necessary.
I tried changing it to "function countColoredCells(countRange)" but it doesn't work as I assume there is something else I have to do within the rest of the script.
I literally have little to no coding knowledge and would really appreciate any help to solve this!
function countboldcells() {
var book = SpreadsheetApp.getActiveSpreadsheet();
var sheet = book.getActiveSheet();
var range_input = sheet.getRange("C3:S3");
var range_output = sheet.getRange("N3");
var cell_styles = range_input.getFontWeights();
var count = 0;
for(var r = 0; r < cell_styles.length; r++) {
for(var c = 0; c < cell_styles[0].length; c++) {
if(cell_styles[r][c] === "bold") {
count = count + 1;
}
}
}
range_output.setValue(count);
}
range_input in the existing script is hard-coded. This is unsatisfactory because it doesn't permit analysis on a student-by-student basis. To fix this, you need to loop through the data for each student, and do 'countbold' for each student.
Let's assume that "C3:S3" is the range for a single student. Let's also assume that the data for other students is contained in each subsequent row, and that there are two header rows.
To do:
Work out the number of rows of student data - refer variable ALast.
Get the data for all students in one go. Why? Because this is more efficient than getting the data one row at a time - refer range_input discussed below.
Loop through each row of the data (i.e. loop by student - using a "for" loop).
Count the bold cells and update the results for each student - using most of your existing code;
Note:
The destination range (range_output) is calculated for each row, using getRange (row,column). This could have been done by saving values to an array, and updating all the values in a single process, but I though it was better to retain the approach the OP had already taken, and not over-complicate matters. If there are a LOT of students AND the code is taking too long to run, then updating the counts by array would be more efficient.
The input range (range_input) is defined using getRange(row, column, numRows, numColumns).
row = 3, the first row of data
column = 3, Column C
numRows = a calculated value (ALast minus two header rows)
numColumns = Columns C to S inclusive = 17 (assigned to a variable).
function so54260768() {
// Setup spreadsheet and target sheet
var book = SpreadsheetApp.getActiveSpreadsheet();
var sheet = book.getActiveSheet();
// get the number of students in Column A
var Avals = book.getRange("A1:A").getValues(); // assuming rows one and two are headers
var Alast = Avals.filter(String).length;
//Logger.log("DEBUG: The last row on A = " + Alast);// DEBUG
// number of columns in the data range
var NumberofColumns = 17;
// get the data for all students
var range_input = sheet.getRange(3, 3, Alast - 2, NumberofColumns); // the first two rows are headers
var cell_styles = range_input.getFontWeights();
// start loop though each row - one row per student
for (z = 0; z < Alast - 2; z++) {
// set the bold counter to zero
var count = 0;
//loop through the cells in this row; count the cells that are bold
for (var i = 0; i < NumberofColumns; i++) {
if (cell_styles[z][i] === "bold") {
count = count + 1;
}
}
//Logger.log("DEBUG: row="+(z+3)+", count="+count);//DEBUG
var range_output = sheet.getRange(z + 3, 14).setValue(count); //. row, column
}
}
I'm a bit of newbie at coding, especially Javascript/Google-script language. I've created the code below, and it works, but now that I've got a working code I'd like to see how I can optimize it. It seems to me that all of the getValue() calls are a major performance hit, and I've never really been good at optimizing loops. Anyone know a better way to accomplish the same as this code?
What it does: Checks each spreadsheet in one of my folders to see if it needs to have the rest of the script run. If true, it opens that sheet and counts the number of rows that have data, using that to limit the amount of rows it checks in the loop. It then looks for any row marked for push and copies that range to another spreadsheet in my drive. It then continues to the next file in the folder and does the same.
Here's my code:
function myVupdate() {
try {
var folder = DriveApp.getFolderById("123abc"),
files = folder.getFiles();
while (files.hasNext()) {
var file = files.next(),
sss = SpreadsheetApp.open(file);
SpreadsheetApp.setActiveSpreadsheet(sss);
//Work orders update
var ss = sss.getSheetByName("Sheet2"),
refresh = ss.getRange("W3").getValue();
if (refresh == 0) {continue};
var avals = ss.getRange("D5:D").getValues(),
count = avals.filter(String).length,
rows = count + 5
var val = ss.getDataRange().getValues();
for (var row=5; row < rows; row++) {
var cell = ss.getDataRange().getCell(row, 23).getValue();
if (cell == 0) {
var cells = [["v" + "WO-" + val[row-1][3] + "_" + val[row-1][2],val[row-1][13],val[row-1][14],val[row-1][15],new Date()]];
var tss = SpreadsheetApp.openById("target_spreadsheet"),
ts = tss.getSheetByName("Sheet5");
ts.insertRowBefore(2);
var last_hmy = ts.getRange(3,1).getValue();
ts.getRange(2,1).setValue(last_hmy+1);
ts.getRange(2,2,cells.length,cells[0].length).setValues(cells);
ts.getRange(2,7).setValue(sss.getName());
ss.getRange(row,17).setValue(last_hmy+1);
ss.getRange(row,18,cells.length,cells[0].length).setValues(cells);
//Turnover update
var ss = sss.getSheetByName("Sheet1"),
avals = ss.getRange("D5:D").getValues(),
count = avals.filter(String).length,
rows = count + 5
var val = ss.getDataRange().getValues();
}
}
for (var row=5; row < rows; row++) {
var cell = ss.getDataRange().getCell(row, 24).getValue();
if (cell == 0) {
var cells = [["v" + val[row-1][3] + "_" + val[row-1][2],val[row-1][12],val[row-1][15],val[row-1][16],new Date()]];
var tss = SpreadsheetApp.openById("target_spreadsheet"),
ts = tss.getSheetByName("Sheet5");
ts.insertRowBefore(2);
var last_hmy = ts.getRange(3,1).getValue();
ts.getRange(2,1).setValue(last_hmy+1);
ts.getRange(2,2,cells.length,cells[0].length).setValues(cells);
ts.getRange(2,7).setValue(sss.getName());
ss.getRange(row,18).setValue(last_hmy+1);
ss.getRange(row,19,cells.length,cells[0].length).setValues(cells);
}
}
}
}
catch(e) {
// Browser.msgBox("An error occured. A log has been sent for review.");
var errorSheet = SpreadsheetApp.openById ("target_sheet").getSheetByName("Error Log"),
source = sss.getName();
lastRow = errorSheet.getLastRow();
var cell = errorSheet.getRange('A1');
cell.offset(lastRow, 0).setValue(e.message);
cell.offset(lastRow, 1).setValue(e.fileName);
cell.offset(lastRow, 2).setValue(e.lineNumber);
cell.offset(lastRow, 3).setValue(source);
cell.offset(lastRow, 4).setValue(new Date());
MailApp.sendEmail("my#email.com", "Error report - " + new Date(),
"\r\nSource: " + source + "\r\n"
+ "\r\nMessage: " + e.message
+ "\r\nFile: " + e.fileName
+ "\r\nLine: " + e.lineNumber
);
}
}
Hello and welcome to Stack Overflow,
first of all, you are correct. The more getValue(), or setValue() calls you do the worse the performance, read more on best practices here. Google recommends you batch these as much as possible. One thing that immediately springs to attention is the following:
var val = ss.getDataRange().getValues();
so now you have all the values on the sheet in a 2D array. That means that in the following bit
var ss = sss.getSheetByName("Sheet2"),
refresh = ss.getRange("W3").getValue();
if (refresh == 0) {continue};
var avals = ss.getRange("D5:D").getValues(),
count = avals.filter(String).length,
rows = count + 5
var val = ss.getDataRange().getValues();
for (var row=5; row < rows; row++) {
var cell = ss.getDataRange().getCell(row, 23).getValue();
every single getValue() or getValues() is no longer necessary. Instead, you know that refresh = val[2][22] because you need the 3rd row and 23rd column, as you already have the entire range that has data from that sheet.
Same with avals as all values in range D5:D are in vals[n][3], where n starts from 4. Remember, the array index starts from 0 (so first row and first column is vals[0][0].
So anywhere you are trying to use getValues() from the ss spreadsheet, you already have that data. What you can also do, is manipulate the array you have, so you always change the values only in that array. Once you are done with it, you use ss.getDataRange().setValues(vals) to push the entire array back to the same range (you can just store the range in a variable like datRange = ss.getDataRange() and then do datRange.setValues(vals).
You will just need to work with a separate data array for any other sheet. I did not go into detail for the rest of the code as the same ideas go throughout. Since you already grab everything with getValues() there is no longer any reason to use getValue() for any cell within that range.
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;
}
I have a script below which calls separately for column G and I of a spreadsheet and returns the last non-zero value in the specific column.
I am wondering if it is faster to call for an array of data from columns G, H and I (is that called an array?) and retrieve the last non-zero value in column G and I? How can I do that?
// get the latest value for Column G
var lastRow = spreadsheet.getSheetByName("Form responses 1").getLastRow();
var columnGvalues = spreadsheet.getSheetByName("Form responses 1").getRange("G" + "1:" + "G" + lastRow).getValues();
for (; columnGvalues[lastRow - 1] == "" && lastRow > 0; lastRow--) {}
var columnGLast = columnGvalues[lastRow - 1];
// get the latest value for Column I
var lastRow = spreadsheet.getSheetByName("Form responses 1").getLastRow();
var columnIvalues = spreadsheet.getSheetByName("Form responses 1").getRange("I" + "1:" + "I" + lastRow).getValues();
for (; columnIvalues[lastRow - 1] == "" && lastRow > 0; lastRow--) {}
var columnILast = columnIvalues[lastRow - 1];
I'm not sure which is faster, it will strongly depend on how much data you have in those columns (and in H too)
There are 3 possibilities:
1 - You have very few data: using a single array from G to I will be faster (but then, it wouldn't matter at all, everything would be fast)
2 - You have moderate data and one of the columns has a lot of empty cells related to the other. Your approach is a good one.
3 - You have huge data. Then each getValues() will take long, it would be better to go cell by cell (provided you do not have one of the two columns with a great number of empty cells)
If you have really huge data, it will be faster to get the last line and do range offsets upwards.
Repeated getRanges and offsets are usually slower than a getValues(), but if you have tons of data, getValues will be slower. (Because get values would always take the entire column, and going cell by cell would take only the necessary data).
But what would be considered huge data and small data?? Only speed tests could tell...
So, for huge data:
function mainFunction()
{
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheetByName("Form responses 1");
var lastRow = sheet.getLastRow();
var columnGLast = getLast(sheet, "G", lastRow);
var columnILast = getLast(sheet, "I", lastRow);
}
function getLast(sheet, column, row)
{
var currCell = sheet.getRange(column + row);
while (currCell.getValue() === "" && row > 1)
{
row--;
currCell = currCell.offset(-1,0);
}
return currCell.getValue();
}
Now, if your columns do not have that huge data, you can pick just one array:
function mainFunction()
{
var spreadsheet = SpreadsheetApp.getActive();
var sheet = spreadsheet.getSheetByName("Form responses 1");
var lastRow = sheet.getLastRow();
var myRange = sheet.getRange("G1:I" + lastRow).getValues();
var columnGLast = getLast(myRange, 0, lastRow - 1);
var columnILast = getLast(myRange, 2, lastRow - 1);
}
function getLast(array, columnIndex, lastRow)
{
var val = array[lastRow][columnIndex];
while (val === "" && lastRow > 0)
{
lastRow--;
val = array[lastRow][columnIndex];
}
return val;
}
So for this you get the values of each column(G and I) separately to two different array using the getRange(row, column, numRows) by adding the last row value to numRows.
Then you can easily find the last non zero value from those arrays.
Hope this helps!