Google Sheets custom function conditional formatting - javascript

I need to make a cell text turn red if the value of the cell is equal to the value of the cell two rows above it. I have these two functions in my script editor:
/**
* Compares cell value to another cell relative in position to it.
* Returns true if equal values.
*
* #param {Number} colDiff Relative positioning column difference.
* #param {Number} rowDiff Relative positioning row difference.
*/
function equalsRelativeCellValue(rowDiff, colDiff) {
var thisCell = SpreadsheetApp.getActiveRange();
var relativeCellValue = getRelativeCellValue(rowDiff, colDiff);
if (thisCell.getValue() === relativeCellValue)
return true;
else
return false;
}
/**
* Returns value of cell according to relative position
*
* #param {Number} colDiff Relative positioning column difference.
* #param {Number} rowDiff Relative positioning row difference.
*/
function getRelativeCellValue(rowDiff, colDiff) {
var range = SpreadsheetApp.getActiveRange();
var col = range.getColumn();
var row = range.getRow();
var range2 = SpreadsheetApp.getActiveSheet().getRange(row + rowDiff,col + colDiff);
return range2.getValue();
}
The second function, getRelativeCellValue(rowDiff, colDiff) works just fine, I put 8 in a cell, and two cells below it I entered getRelativeCellValue(-2, 0), and the cell evaluated to 8.
But the first function, getRelativeCellValue(rowDiff, colDiff) won't work as my custom function in conditional formatting for some reason:
Custom formula is: =equalsRelativeCellValue(-2,0)
The difficult part is referring to the value of the cell being referenced in the conditional formatting. But my function looks right to me, it returns true if the cell values are equal, and false if they are not. Im hoping I'm just using the "Custom formula is" feature of Conditional Formatting improperly, but the documentation is pretty sparse.

Just realizer what you want to do, just use conditional formatting, select the range, and apply for the first cell, and it will apply for all correctly:
Eg.
In conditional formating dialog, select Custom Formula, paste the custom formula =J10=J8, and select the range J10:J100.
OldAnswer:
You created a circular reference.
If you input =equalsRelativeCellValue(-2,0) in the the Cell, how can it's value be anything, if it is waiting for the function to resolve?
You can overcome this in a column besides the values, or pass the value directly in the function.
You can also use this to make the between cell have a true/false state:
function equalsRelativeCellValue(rowDiff, colDiff) {
var below = getRelativeCellValue(1, 0);
var relativeCellValue = getRelativeCellValue(2, 0);
if (below === relativeCellValue)
return true;
else
return false;
}

Related

Is there a JavaScript function to link ranges (top range 0----100 bottom range 55%----24%) known top input, want bottom value output

The following image will help explain what I am trying to achieve...
The top line (A) is a given calculated JavaScript value, lets call this the input.
The bottom line (B) is the output, so whatever input to (A) is given (will always be within the range) if a line (like the green one shown) were to be drawn I need the value of the output.
I have tried to search for phrases like "linked range", "parallel linked values" and similar but I think half of my problem is not knowing what this kind of calulation is called.
Usually I would be able to show what I have tried but on this one I really dont have a clue where to start.
Any help greatly appreciated.
So get the percentage in A
percentage = A[value] / ( A[max] - A[min] )
Use that to figure out the value in second
result = B[max] - percentage * (B[max] - B[min])
so basic JavaScript
var aMin = 0;
var aMax = 500;
var bMin = 24;
var bMax = 55;
var aValue = 100;
var percentage = aValue / ( aMax - aMin );
var result = bMax - percentage * (bMax - bMin);
console.log(result + "%");

Formula's permission error inside an Addon - getActiveUser

The custom formula I created isn't working due to a permission issue, but after following the guidelines I still can't understand what the problem is.
I wrote a custom formula "SUMIFALL" that I published to other group members on the same domain as an addon.
For some reason, when we try to use it (me included) in a spreadsheet that doesn't contain the actual script (but does included through the addon), it generates the following error:
Error
You do not have permission to call getActiveUser (line 37).
Following these guidelines about access permission of custom formulas in google sheets: https://developers.google.com/apps-script/guides/sheets/functions#advanced
and specifically regarding sheets:
Read only (can use most get*() methods, but not set*()).
Cannot open other spreadsheets (SpreadsheetApp.openById() or SpreadsheetApp.openByUrl()).
I tried to make sure to use only Get functions, but it still generates the error. The code:
/**
* For each cell in {searchCol} that contains the cell content, it adds
* the correspond value from the same row in {sumCol}
* {criterion} is optional if want to add a condition the the {sumCol} value also need to fullfill.
* #param {searchCol} input The column to check and iterate over.
* #param {sumCol} input The correspond column from which we add to sum.
* #param {criterion} input An optional field - adding a constraint on {sumCol} values.
* #return The sum of all cells that met the requirements.
* #customfunction
*/
function SUMIFALL(searchCol, sumCol, criterion){
var checkCriteria = criterion != null ? encapsulateCriterion(criterion) : function(val) { return true; };
var arrayOfSum = searchCol.map(function(cell){
var sum = 0;
for(var i=0; i<searchCol.length; i++){
var traffic = Number(sumCol[i]);
var phrase = searchCol[i].toString();
var found = phrase.search(new RegExp("\\b("+cell+")\\b","gi")) != -1;
sum += checkCriteria(traffic) && found ? traffic : 0;
}
return sum;
});
return arrayOfSum;
}
/**
* For each cell in {searchCol} that contains the word {cell} content
* adds the correspond value from the same row in {sumCol}
* {criterion} is optional if want to add a condition the the {sumCol} value also need to fullfill.
* #param {cell} A cell that contains the word to be searched.
* #param {searchCol} input The column to check and iterate over.
* #param {sumCol} input The correspond column from which we add to sum.
* #param {criterion} input An optional field - adding a constraint on {sumCol} values.
* #return The sum of all cells that met the requirements.
* #customfunction
*/
function SUMIFREGEX(cell, searchCol, sumCol, criterion){
var checkCriteria = criterion != null ? encapsulateCriterion(criterion) : function(val) { return true; };
var sum = 0;
for(var i=0; i<searchCol.length; i++){
var traffic = Number(sumCol[i]);
var phrase = searchCol[i].toString();
var found = phrase.search(new RegExp("\\b("+cell+")\\b","gi")) != -1;
sum += checkCriteria(traffic) && found ? traffic : 0;
}
return sum;
}
function encapsulateCriterion(criterion){
var criteriaSplit = criterion.search(/[0-9]/gi);
var criteria = criterion.substring(0,criteriaSplit);
var number = Number(criterion.substring(criteriaSplit));
return function(val){
return criteria == '=' ? val == number :
criteria == '<' ? val < number :
criteria == '>' ? val > number :
criteria == '<=' ? val <= number :
criteria == '>=' ? val >= number :
criteria == '<>' ? val != number : true;
}
}
They also mention on their page:
If your custom function throws the error message You do not have permission to call X service., the service requires user authorization and thus cannot be used in a custom function.
Does anybody know what might be the problem?
The only manipulation on Sheet content is on the supplied range, which is very similar to the example they provided on their page.
Thanks in advance
Apparently (as I found out with the help of #TheMaster), the global variables inside the project's files affects the formulas permission even if the specific formula doesn't contain operations that aren't allowed.
On a different script file in the same addon, on line 37 I call the "getActiveUser()" method.
Removing it fixed the problem.

How to use correctly setFormula in App Script

I have a problem to use correctly setFormula in app script, i tried to use setFormula in a indeterminated range cells but I do not know how to specify that the range of rows be increased and it is not just a specific range. The script that I try to make is a condition in which if in a range of cells there is information, then put the formula in a cell.
function formulas() {
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet 1");
var rows = activeSheet.getMaxRows();
for(var i=7; i <= rows; i++){
var workingCell = activeSheet.getRange(i, 3).getValue();
if(workingCell != ""){
activeSheet.getRange(i, 4).setFormula("=$B$5"); //this is fine
activeSheet.getRange(i, 5).setFormula("=((100/H7)*I7)/100"); //but this not
}
}
}
how can I do it so if it's row 8 is (" = ((100 / H8) * I8) / 100 ") and so on.
EDIT
The problem is that I try to apply it to many cells and that the rows that I add will increase according to the row in which I am placing the formula ... If a formula is added in row D9 and D10 , then The range is H9, I9 and H10, I10
The simplest "fix" , as pointed out in a comment, is to concatenate your i loop variable into the formula, like this:
function formulas() {
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet 1");
var rows = activeSheet.getLastRow(); //maxRows consider blank rows, you don't need those
for(var i=7; i <= rows; i++){
var workingCell = activeSheet.getRange(i, 3).getValue();
if(workingCell != ""){
activeSheet.getRange(i, 4).setFormula("=$B$5");
activeSheet.getRange(i, 5).setFormula("=((100/H" +i+ ")*I" +i+ ")/100");
}
}
}
Anyway, this function executes too many gets and sets against the spreadsheet, and this will perform poorly as your sheet grows. You should try to minimize all your sets and gets by issuing them in bulk, that is, against a bigger range rather than cell-by-cell.
Your use-case has a problem with this approach because you have some blank spots in your range (when workingCell is blank). If setting a "blank" formula for those values is not an issue for you, then you can speed your script greatly by using this:
function formulas() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet 1"); //not necessarily active
var workingCells = sheet.getSheetValues(7, 3, -1, 1); //-1 == lastRow
var r1c1formulas = [];
for (var i=0; i < workingCells.length; i++){
if (workingCells[i][0] != "") {
r1c1formulas.push(['=R5C2', '=((100/R[0]C[3])*R[0]C[4])/100']);
} else
r1c1formulas.push(['=""','=""']);
}
sheet.getRange(7, 4, workingCells.length, 2).setFormulasR1C1(r1c1formulas);
}
The 2nd "trick" is to use the formulas in R1C1 notation instead of regular A1 style. Check the setFormulaR1C1 documentation here.
The R1C1 notation may seem daunting at first but is rather simple, I'd say it is simpler then the 'A1' one. I'll try to summarize it here. R is the row number, and C column, and in front of the letter you have the row and column numbers (instead of letter). So =$B$5 is written as =R5C2.
The last thing different is the relative reference. In the A1 notation you just don't place the '$' signs. Which is not really intuitive and not all that flexible when you're trying to set a bunch of formulas at once (exactly your use-case). Because on the A1 the relative formula is a different formula, the references are different =B1 is not the same as =C1 (which could be "the same" if set on two cells in the same row and consecutive columns).
Anyway, on the R1C1 notation the relative reference is counted as number of rows and columns from the cell that is the reference.
So, when you set the formula =H7*I7 to cell E7, you count that H is 3 columns ahead of E and I 4. And it is all on the same row, so zero row difference. Lastly, to write a relative reference you wrap the number in []. Therefore =H7 * I7 on E7 becomes =R[0]C[3] * R[0]C[4].

Google Script Errror: "Incorrect range width" when using setValues

Try as I might I CANNOT decipher the problem that I'm having writing new rows to a sheet. I've done this several times and I've debugged this thoroughly using Logger.log, but I just can't solve it. Here's a summary of what I'm doing, a code snippet, and a log:
What I'm doing:
Adding rows to a sheet (below existing rows)
73 new rows are stored stored in array: Grade Rows
When attempt to write the new rows to the sheet, get this error:
Incorrect range width, was 1 should be 26
Here’s the code including some Logger.logs:
var BeginningRow = LastSGRowSheet + 1;
var EndingRow = BeginningRow + SGPushKtr -1;
Logger.log("BeginningRow =>" + BeginningRow + "<=, SGPushKtr =>" + SGPushKtr + "<=, Ending Row =>" + EndingRow + "<=");
var GradesRangeString = 'A' + BeginningRow + ':' + LastStudentGradesColumnLetter + EndingRow;
Logger.log("GradesRangeString =>" + GradesRangeString + "<=");
StudentGradeSheet.getRange(GradesRangeString).setValues(GradeRows);
The error occurs in that last line of code.
Here’s the log:
17-12-31 11:51:15:763 EST] BeginningRow =>364<=, SGPushKtr =>73<=, Ending Row =>436<=
[17-12-31 11:51:15:764 EST] GradesRangeString =>A364:Z436<=
Let's say that your data array is dA then the number of rows in that array is dA.length and assuming its a rectangular array then the number of columns is vA[0].length. So your output command has to be some thing like this.
sheet.getRange(firstRow,firstColumn,dA.length,dA[0].length).setValues(dA);
If you'd like to learn a little more about this problem check this out.
You could also append each row to the current sheet one row at a time in loop.
It's hard to know why GradeRows doesn't match your range without seeing all of your code.
Using Cooper's getRange arguments will likely reveal your problem, and will prevent you from having to update your row and column variables when you make changes to your code. Another issue that gets me sometimes is the fact that the setValues array needs to be exactly the same dimensions as the range. If one row has a different length, it will fail. If the logic I use to create row arrays can result in different lengths, I use the function below to make sure my arrays are symmetric before writing them to a sheet. It is also helpful for debugging.
/**
* Takes a 2D array with element arrays with differing lengths
* and adds empty string elements as necessary to return
* a 2D array with all element arrays of equal length.
* #param {array} ar
* #return {array}
*/
function symmetric2DArray(ar){
var maxLength;
var symetric = true;
if (!Array.isArray(ar)) return [['not an array']];
ar.forEach( function(row){
if (!Array.isArray(row)) return [['not a 2D array']];
if (maxLength && maxLength !== row.length) {
symetric = false;
maxLength = (maxLength > row.length) ? maxLength : row.length;
} else { maxLength = row.length }
});
if (!symetric) {
ar.map(function(row){
while (row.length < maxLength){
row.push('');
}
return row;
});
}
return ar
}
How about using appendRow()? That way you don't need to do lots of calculations about the range. You can loop through your data and add it row by row. Something like this:
myDataArr = [[1,2],[3,4],[5,6]]
myDataArr.forEach(function(arrayItem){
sheet.appendRow([arrayItem[0],arrayItem[1]])
})
// This will output to the sheet in three rows.
// [1][2]
// [3][4]
// [5][6]

Getting currently selected text

I'm try to get the currently selected text in an input using window.getSelection() but I'm always getting an empty string:
expect(browser.executeScript("return window.getSelection().toString();")).toEqual("test");
Results into:
Expected '' to equal 'test'.
The complete reproducible test using angularjs.org as a target site:
describe("My test", function () {
beforeEach(function () {
browser.get("https://angularjs.org/");
});
it("should select text in an input", function () {
var query = element(by.css("input.search-query"));
query.sendKeys("test");
query.sendKeys(protractor.Key.chord(protractor.Key.COMMAND, "a"));
expect(browser.executeScript("return window.getSelection().toString();")).toEqual("test");
});
});
Note that I actually see the entered text being selected with COMMAND + "a".
What am I doing wrong?
Using protractor 2.5.1, firefox 41.
getSelection does not work for text selected in input elements, but for selections made on elements across the page.
You could use selectionStart and selectionEnd like this:
return document.activeElement.value.substring(
document.activeElement.selectionStart,
document.activeElement.selectionEnd)
You should probably create a function for this instead of this one-liner. And maybe you want to then also test whether document.activeElement is indeed the right type of element, etc. And when you are at it, you might even make it compatible for pre-IE9 browsers... (difficult though)
Simple Function
This will work also on input or textarea controls that do not have focus:
function getInputSelection(el) {
if (el.selectionStart !== undefined) {
return el.value.substring(el.selectionStart, el.selectionEnd);
}
}
// Example call:
console.log(getInputSelection(document.activeElement));
Extensive jQuery Plug-in
This provides for more cross-browser compatibility (pre-IE9), and supports not only getting, but also setting the selection range and text, in the form of a jQuery plug-in. It deals with the fact that CRLF character sequences count as one character position in a pragmatic way (replace in-place by LF only):
/**
* jQuery plug-in for getting/setting the selection range and text
* within input/textarea element(s). When the selection is set,
* the element will receive focus. When getting the selection,
* some browsers require the element to have focus (IE8 and below).
* It is up to the caller to set the focus first, if so needed.
* #this {jQuery} Input/textarea element(s).
* #param {object} opt_bounds When provided, it sets the range as follows:
* #param {number} opt_bounds.start Optional start of the range. If not
* provided, the start point of the range is not altered.
* #param {number} opt_bounds.end Optional end of the range. If not
* provided, the end point of the range is not altered. If null, the end
* of the text value is assumed.
* #param {number} opt_bounds.text Optional text to put in the range. If
* not provided, no change will be made to the range's text.
* #return {jQuery|object|undefined} When setting: the same as #this to
* allow chaining, when getting, an object {start, end, text, length}
* representing the selection in the first element if that info
* is available, undefined otherwise.
*/
$.fn.selection = function (opt_bounds) {
var bounds, inputRange, input, docRange, value;
function removeCR(s) {
// CRLF counts as one unit in text box, so replace with 1 char
// for correct offsetting
return s.replace(/\r\n/g, '\n');
}
if (opt_bounds === undefined) {
// Get
if (!this.length) {
return;
}
bounds = {};
input = this[0];
if (input.setSelectionRange) {
// Modern browsers
bounds.start = input.selectionStart;
bounds.end = input.selectionEnd;
} else {
// Check browser support
if (!document.selection || !document.selection.createRange) {
return;
}
// IE8 or older
docRange = document.selection.createRange();
// Selection must be confined to input only
if (!docRange || docRange.parentElement() !== input) { return; }
// Create another range that can only extend within the
// input boundaries.
inputRange = input.createTextRange();
inputRange.moveToBookmark(docRange.getBookmark());
// Measure how many characters we can go back within the input:
bounds.start =
-inputRange.moveStart('character', -input.value.length);
bounds.end = -inputRange.moveEnd('character', -input.value.length);
}
// Add properties:
bounds.length = bounds.end - bounds.start;
bounds.text = removeCR(input.value).
substr(bounds.start, bounds.length);
return bounds;
}
// Set
if (opt_bounds.text !== undefined) {
opt_bounds.text = removeCR(opt_bounds.text);
}
return this.each(function () {
bounds = $.extend($(this).selection(), opt_bounds);
bounds.end = bounds.end === null ? this.value.length : bounds.end;
if (opt_bounds.text !== undefined) {
value = removeCR(this.value);
this.value = value.substr(0, bounds.start) + bounds.text +
value.substr(bounds.end);
bounds.end = bounds.start + bounds.text.length;
}
if (this.setSelectionRange) {
// Modern browsers
// Call .focus() to align with IE8 behaviour.
// You can leave that out if you don't care about that.
this.focus();
this.setSelectionRange(bounds.start, bounds.end);
} else if (this.createTextRange) {
// IE8 and before
inputRange = this.createTextRange();
inputRange.collapse(true);
inputRange.moveEnd('character', bounds.end);
inputRange.moveStart('character', bounds.start);
// .select() will also focus the element:
inputRange.select();
}
});
};
Example use:
// Get
console.log($('textarea').selection().text);
// Set text
$('textarea').selection({text: "Hello!"});
// Set starting point of selection
$('textarea').selection({start: 1});

Categories