Make custom function refresh - google-apps-script

So I have this one code that takes a range and a reference cell, and returns the number of cells with the same background color as the ref. This works great, but I want it to be usable when the color of some of the cells changes. I put it on an every minute time based trigger, and I tried to use SpreadsheetApp.flush() to uncache the current number. This did not have the desired affect.
I also tried to make a second function that used the flush() and then returned the first function. This also did not work. The only way I know to make it refresh is to take the "=" from the beginning of the cell with the function in it, and then replace it. The code that works is below.
function countBGColor(range, ref) {
var sheet = SpreadsheetApp.getActiveSheet();
var color = sheet.getRange(ref).getBackground();
var range = sheet.getRange(range);
var rangeVal = range.getValues();
var count = 0;
var allColors = range.getBackgrounds();
for (var i = 0; i < allColors.length; i++) {
for (var j = 0; j < allColors[0].length; j++) {
if (allColors[i][j] == color) count += 1;
};
};
return count;
}

I couldn't find direct way to do this. But you could make script that clears formula and then reenters it into cell:
function Refresh() {
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange('D1');
range.clear();
SpreadsheetApp.flush();
range.setFormula('=countBGColor("A1:A10","E1")');
}
Next link your image to a script's function: Insert > image > in top right menu of image, Assign Script
Sample file:

Related

Google Script - Execute only if cell contains a certain string of text

Completely new to using the AppsScript in Google but I came across a script that would allow GoogleSheets to count cells if coloured, and it automatically updates thanks to that last bit of code that sets a random value that essentially triggers a recalculation:
// Unsed third argument
function countColoredCells(countRange,colorRef,unUsed) {
var activeRg = SpreadsheetApp.getActiveRange();
var activeSht = SpreadsheetApp.getActiveSheet();
var activeformula = activeRg.getFormula();
// The regex matches the parentheses as an array, gets the arguments as a string,
// then splits the arguments on the comma into another array
var arrayOfArguments = activeformula.match(/\((.*)\)/).pop().trim().split(',');
// Get the first argument which is the range
var countRangeAddress = arrayOfArguments[0];
// Get the second argument, which is the reference color
var colorRefAddress = arrayOfArguments[1];
var backGrounds = activeSht.getRange(countRangeAddress).getBackgrounds();
var BackGround = activeSht.getRange(colorRefAddress).getBackground();
var countCells = 0;
for (var i = 0; i < backGrounds.length; i++)
for (var k = 0; k < backGrounds[i].length; k++)
if ( backGrounds[i][k] == BackGround )
countCells = countCells + 1;
return countCells;
};
// If Cell A1 has ColouredCells, Writes a random number to B1
function onEdit(e) {
if SpreadsheetApp.getActiveSheet().getRange('A1')="ColouredCells" {
SpreadsheetApp.getActiveSheet().getRange('B1').setValue(Math.random());}
}
Specifically I'm looking to edit the last part of the code so that this script only runs on sheets which I'd like it to (instead of randomly changing my B1 cell on all sheets I touch). As per the comments I'd like it to only run if cell A1 has the string "ColouredCells". Sorry newbie to all this so apologies if this is a really simple ask! Appreciate any help I can get on this one, I've tried googling quite a few different things but can't seem to to find the solution on this one!
This is the part I specifically need help with.
// If Cell A1 has ColouredCells, Writes a random number to B1
function onEdit(e) {
if SpreadsheetApp.getActiveSheet().getRange('A1')="ColouredCells" {
SpreadsheetApp.getActiveSheet().getRange('B1').setValue(Math.random());}
}
Thank you!
To run the code for specific sheets only you have to first get the name of the sheet and with an if-statement run the code as you wish
function onEdit(e){
// code
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
if ("Sheet 1" === sheet.getName() || "Sheet 2" === sheet.getName()) {
// Run your code
}
}

Get notes from a cell and insert into another cell

I have a range of cells in Google Sheets, some of them have notes attached.
If there's a note attached to a cell, I need to put the note in a separate cell, and put the location of that note in another cell.
I found this script elsewhere:
function getNote(cell)
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var range = ss.getRange(cell)
return range.getNote();
}
but when I try to use it I get an error "Exception: Range not found (line 12)."
But this script only gets me halfway there as it only gets the note and puts it in a cell. I also need to know what cell the note came from.
Any help is greatly appreciated.
In the script above the function getNote() expects the paramter cell
If you just run the script without calling getNote() from another function / an environment where it gets a values for cell assigned, the script will fail wiht the error you obtained.
Indeed, this script doe snot not meet your needs. What you probably want is to screen all your cells for the one that have notes.
What you need to decide is into which cells you want to put the note and the cell notation.
Below is a sample that you need to adapt for your needs:
function getNotes() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
//if you have only one sheet in the spreadsheet, otherwise use ss.getSheetByName(name);
var sheet = ss.getActiveSheet();
var range = sheet.getDataRange();
var results = range.getNotes();
for (var i = 0; i < results.length; i++) {
for (var j = 0; j < results[0].length; j++) {
//if a not empty note was found:
if(results[i][j]){
var note = results[i][j];
var cell = range.getCell(i+1, j+1);
var notation = cell.getA1Notation();
//adjust the offset as function of the column / row where you want to output the results
cell.offset(0, 1).setValue(note);
cell.offset(0, 2).setValue(notation);
}
}
}
}
Important references:
getNotes()
getA1Notation()
getDataRange()
getCell(row, column)
offset(rowOffset, columnOffset)

What trigger to use to update cells when inputted data is copied and pasted into spreadsheet? (Google Sheets/ apps script)

So i have an OnEdit function that when the user chooses a color from the list, the cells background color becomes that selected value.
This works fine, but I am trying to get the function to update the cells background color immediately when data is copied and pasted into the spreadsheet as well as when a user edits a cell. Currently when you copy and paste data into the speadsheet, the code i wrote to change the cells background colour does not trigger as nothing has been "edited". Im trying to get the code to work for OnEdit and when data is copied and pasted into the sheet. Any ideas would be greatly appreciated! Thanks!
This is what I have so far..
// Validates the colours using the above map and changes the cell background to the input colour..
function onEdit(e) {
try {
var range = e.range;
var sheet = range.getSheet();
var tabColumnData = getColumnData(sheet);
if(!tabColumnData.hasOwnProperty('Color')) return;
var col = range.getColumn();
if (col == tabColumnData['Color'] + 1) {
var color = e.value;
if(validateCellValue(color) || COLOR_TO_HEX_MAP.hasOwnProperty(color)) {
range.setBackground(color);
} else {
range.clear();
SpreadsheetApp.getUi().alert('Please select a color from the list or a hex color code.');
}
}
if (col == tabColumnData['Name'] + 1) {
setDataValidation(createColorList());
}
} catch(e) {
Logger.log(e);
}
}
// Sets the data validation when the spreadsheet is opened.
function onOpen(e){
setDataValidation(createColorList());
}
After reading your question, I assume the following:
You want to detect when a group of cells are edited.
When the modification is detected, you want to change the colour of each cell to match its content.
If my assumptions are correct, you can use the following example to fulfill your requests:
CODE
function onEdit(e) {
var firstRow = e.range.getLastRow() - e.range.getHeight() + 1;
var firstColumn = e.range.getLastColumn() - e.range.getWidth() + 1;
var values = e.range.getValues();
for (var i = 0; i < e.range.getHeight(); i++) {
for (var j = 0; j < e.range.getWidth(); j++) {
try {
e.source.getActiveSheet().getRange(firstRow + i, firstColumn + j)
.setBackground(values[i][j]);
} catch (e) {
// You might want some error handling or logging here
}
}
}
}
BEHAVIOUR
The code will run each time that a cell or group of cells are edited. When the edition is detected, the code will iterate each cell and try to change its background colour to the content of the cell.
OBSERVATIONS
This code works for every sheet of the spreadsheet. To make it exclusive to a sheet, you should use getSheet methods [as getSheetByName()].
Due to onEdit trigger limitations, this code won't run if the modification is made by a script (in opposition to a user edit).
The background colours are limited until the ones defined on CSS Colour 3. You can check the full list here.
ALLUSIONS
onEdit(event object) simple trigger
Event object
getLastColumn() / getLastRow()
getHeight() / getWidth()
setBackground(color)
Please, don't hesitate to write me back any additional doubts or request me further clarifications.
UPDATE
The previous code will iterate only over edited cells, not over all the sheet. This behaviour will occur on all the spreadsheet. If you want to reduce your effects over a specific range (like the categorycolor column) you need a conditional statement to check if the modified cells are in the desired range.
The first step to solve this situation is to create a named range as described here. In this example I used the name ColourRange, but you can change the code to set up your own named range. Take this as one of the possible solutions:
function onEdit(e) {
// Colour range
var colourRange = SpreadsheetApp.getActiveSheet().getRange('ColourRange');
var firstColourRow = colourRange.getRow();
var lastColourRow = colourRange.getLastRow();
var firstColourColumn = colourRange.getColumn();
var lastColourColumn = colourRange.getLastColumn();
// Modified range
var firstRow = e.range.getLastRow() - e.range.getHeight() + 1;
var firstColumn = e.range.getLastColumn() - e.range.getWidth() + 1;
var values = e.range.getValues();
for (var i = 0; i < e.range.getHeight(); i++) {
for (var j = 0; j < e.range.getWidth(); j++) {
var modifiedCell = e.source.getActiveSheet().getRange(firstRow + i,
firstColumn + j);
if (modifiedCell.getRow() >= firstColourRow && modifiedCell
.getLastRow() <= lastColourRow && modifiedCell.getColumn() >=
firstColourColumn && modifiedCell.getLastColumn() <= lastColourColumn) {
try {
e.source.getActiveSheet().getRange(firstRow + i, firstColumn + j)
.setBackground(values[i][j]);
} catch (e) {
// You might want some error handling or logging here
}
}
}
}
}

Counting cells with particular background color in Google Sheets

I am trying to produce a visual rota for my restaurant using Google Sheets. I need to count the background color of a row in order to get a figure for the number of half hour periods worked that day.
Here is a link to the google sheet:
https://docs.google.com/spreadsheets/d/19IEDGZypi3nVt55-OayvPo__pbV0sTuRQ3wCJZ1Mhck/edit?usp=sharing
And the script that I am using:
function countBG(range, colorref) {
var sheet = SpreadsheetApp.getActiveSheet();
var color = sheet.getRange(colorref).getBackground();
var range = sheet.getRange(range);
var rangeVal = range.getValues();
var count = 0;
var allColors = range.getBackgrounds();
for (var i = 0; i < allColors.length; i++) {
for (var j = 0; j < allColors[0].length; j++) {
if (allColors[i][j] == color) count += 1;
};
};
return count;
}
I find that the script works the first time it's run, but after that it gives an error:
Range not found (line 4, file "Code")
Would appreciate any help in getting this working, I'm new at Google Sheets and scripting so possibly missing something obvious.
Thanks,
DB.
If you wanted to run it as a custom function and just use a single row as the input, this will work.
You'd pass the row variables like so =countBG("D6:AH6")
function countBG(input) {
var sheet = SpreadsheetApp.getActiveSheet();
var colors = sheet.getRange(input).getBackgrounds();
var count = 0;
Logger.log(colors);
for (var i = 0; i < colors.length; i++) {
for (var j = 0; j < colors[0].length; j++) {
if (colors[i][j] != "#ffffff") count += 1;
};
};
Logger.log(count);
return count;
}
Example in the sheet here in column AI
For some reason SpreadsheetApp.getActiveSheet().getRange(...) gives me only a matrix of values, not an actual range.
My solution (notice how the range is a String here):
function countColoredCellsInRange(countRange, expectedBackground) {
var range = SpreadsheetApp.getActiveSpreadsheet().getRange(countRange);
var backgrounds = range.getBackgrounds();
var coloredCellsAmount = 0;
for (var i = 0; i < backgrounds.length; ++i) {
for (var j = 0; j < backgrounds[i].length; ++j) {
var currentBackground = backgrounds[i][j];
if (currentBackground == expectedBackground) {
++coloredCellsAmount;
}
}
}
return coloredCellsAmount;
};
Example usage:
=countColoredCellsInRange("Sheet!A2:A" ; "#00ff00")
Also, here is example of it at work (calculating percentages in "Progress graph" sheet).
Here is a function I put together for something similar. Rather than a hard-coded value, or needing to know the color beforehand; the first input is the cell that you want to grab the color from.
/**
* Counts the number of cells with the given color within the range.
*
* #param {"B32"} target_color_cell Cell reference to extract the color to count.
* #param {"A1:F22"} range_count_color Range to cycle through and count the color.
* #returns integer The count of the number of cells within range_count_color
* which match the color of target_color_cell.
* #customfunction
*/
function count_cells_by_color( target_color_cell, range_count_color ) {
if (typeof target_color_cell !== "string")
throw new Error( "target_color_cell reference must be enclosed in quotes." );
if (typeof range_count_color !== "string")
throw new Error( "range_count_color reference must be enclosed in quotes." );
var count=0;
var target_color = SpreadsheetApp.getActiveSheet().getRange( target_color_cell ).getBackground();
var range_colors = SpreadsheetApp.getActiveSheet().getRange( range_count_color ).getBackgrounds();
var t=""
for(var row=0; row<range_colors.length;row++){
for(var column=0;column<range_colors[row].length;column++){
let cell_color = range_colors[row][column];
if(cell_color == target_color){
count = count+1;
}
}
}
return count;
}
How to use this:
Create or Open up your Google Sheet.
At the top of the page navigate to Extensions -> App Scripts
Delete the sample that is there and paste in the code from the above block.
Click the "Save" button.
Within any cell of your spreadsheet where you now want to do this calculation enter the "=" button and then specify the function name "count_cells_by_color". For example the following will take the background color from cell A1, and count the number of times it appears in cells A1 through G15.
=count_cells_by_color("A1", "A1:G15")

preventing changes to a cell in google spreadsheet

I want to prevent changes to column K in google spreadsheet. Whatever value is there, I do not want it changed. I do not like the protection feature as it makes what I consider an ugly display.
My code. Unfortunately, it does absolutely nothing. The intent was to take whatever the current value is in the cell, save it, and then write it back on exit of the cell instead of saving whatever changes might have been made to the cell. The cell will either be blank to start, or will already have been modified to contain a date & time. Whatever the current contents blank or not, it should retain the same value after leaving the cell.
function onEdit() {
var s = SpreadsheetApp.getActiveSheet();
var r = s.getActiveCell();
var columnNum=r.getColumn()
// if column is K then prevent any changes
if (columnNum == 11) {
var dateCell = s.getRange(r.getRow(), 11);
var v=dateCell.getValue();
dateCell.setValue(v);
}
};
This might help you. A workaround to your problem I came up with till Google have a proper script API to protect ranges. This one is using the validation function and it works.
function setRangeProtection(rangeToProtect) {
var firstRow = rangeToProtect.getRow();
var noOfRows = rangeToProtect.getHeight();
var firstColumn = rangeToProtect.getColumn();
var noOfColumns = rangeToProtect.getWidth();
var rangeToProtectValues = rangeToProtect.getValues();
var rangeToProtectFormulas = rangeToProtect.getFormulas();
var cellRow = firstRow;
for (var i=0 ; i<noOfRows ; ++i) {
var cellColumn = firstColumn;
for (var j=0 ; j<noOfColumns ; ++j) {
var cell = sheet.getRange(cellRow, cellColumn);
if (rangeToProtectFormulas[i][j] == "") {
var rangeToProtectContent = rangeToProtectValues[i][j];
} else {
var rangeToProtectContent = rangeToProtectFormulas[i][j];
}
var rules = SpreadsheetApp.newDataValidation()
.requireTextEqualTo(rangeToProtectContent)
.setAllowInvalid(false)
.setHelpText('Don\'t mess with the DJ!')
.build();
cell.setDataValidation(rules);
++cellColumn;
}
++cellRow;
}
}
The only small issue with this is that with cells that contain formulas a small red triangle appears and a message is displayed when you hover over it. Couldn't get rid of that one. If you find a solution for that let me know. Removing the help text doesn't help as it returns to a default mode.