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.
Related
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]
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});
could someone help me to order my table of documents in format (number/year) which are stored in one column? First i need to order them by year and then some small order by number (means ordering from newest documents to oldest and reverse order from oldest to newest)
Input data:
1/15
3/15
4/12
41/12
30/12
30/110
21/02
128/02
Ordered data:
30/110
3/15
1/15
41/12
30/12
4/12
128/02
21/02
First of all I was thinking to make double (year.number) and compare them but it will badly order eg. data like 4/12, 30/12 because (12.30 < 12.4)
I have created fiddle http://jsfiddle.net/N3ckS/64/
Thx you
Instead of parsing the individual values into a single value to compare (e.g a double like you proposed), you can compare both values step by step just as you described at the start:
Compare years: If they are different, they already give the absolute order of the two elements; If they are the same, then the order is given by the document number alone.
You also missed to parse the value of years and document-numbers to numbers, resulting in comparing them as strings.
function compare_x_y (x, y) {
var xx = x.split('/');
var x_year = parseInt(xx[1], 10);
var x_number = parseInt(xx[0], 10);
var yy = y.split('/');
var y_year = parseInt(yy[1], 10);
var y_number = parseInt(yy[0], 10);
var year_equals = x_year == y_year;
return year_equals ? x_number < y_number : x_year < y_year;
}
jQuery.fn.dataTableExt.oSort["document-desc"] = function (x, y) {
return compare_x_y (x, y) ? 0 : 1;
};
jQuery.fn.dataTableExt.oSort["document-asc"] = function (x, y) {
return compare_x_y (x, y) ? 1 : 0;
};
Here is the updated fiddle
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;
}
I want to implement a saving system similar to Imgur where if a user presses a button a unique 5 character value is returned. Here is what I have so far:
The database backend uses auto-incrementing ID's starting at 5308416. I use a modified Radix function (see below) to convert these numerical ID's into characters. I use a reverse function to lookup character ID's back to numerical database ID's.
function genID (value)
{
var alphabet = "23456789BCDFGHJKLMNPRSTVWXYZbcdfghjkmnpqrstvwxyz";
var result = "";
var length = alphabet.length;
while (value > 0)
{
result = alphabet[value % length] + result;
value = Math.floor (value / length);
}
return result;
}
The problem is that these generated ID's are very much predictable. My question is, how can I make the generated ID's seem random but still unique (so I can look them up in the database as numbers). I was thinking of using some encryption algorithm but not sure where to start. Any help or suggestions would be appreciated (maybe there is a better way of doing this also).
Do you have to be able to go both ways (i.e. convert an integer to it's hash and back again)? If you can store the hash and lookup the content that way, then it's relatively easy to create a function that produces a hard-to-guess, but complete hash space. You use primes to generate a sequence that only repeats once all possible permutations are exhausted.
The following PHP example is from my own code, adapted from this site:
function hash($len = 6) {
$base = 36;
$gp = array(1,23,809,28837,1038073,37370257 /*,1345328833*/);
$maxlen = count($gp);
$len = $len > ($maxlen-1) ? ($maxlen-1) : $len;
while($len < $maxlen && pow($base,$len) < $this->ID) $len++;
if($len >= $maxlen) throw new Exception($this->ID." out of range (max ".pow($base,$maxlen-1).")");
$ceil = pow($base,$len);
$prime = $gp[$len];
$dechash = ($this->ID * $prime) % $ceil;
$hash = base_convert($dechash, 10, $base);
return str_pad($hash, $len, "0", STR_PAD_LEFT);
}
It would be easy enough to implement that in JavaScript, but ideally you wouldn't need too - you'd have an insert trigger on your table that populated a hash field with the result of that algorithm (adapted for SQL, of course).
A non-predictable, but unique ID can be made by combining your server-side auto-incrementing number with either a current date/time nugget or with a random number. The server-side auto-incrementing number guarantees uniqueness and the date/time nugget or random number removes the predictability.
For a unique ID in string form that takes the server-side unique number as input and where you add the date/time nugget on the client you can do this:
function genID(serverNum) {
return(serverNum + "" + (new Date).getTime());
}
Or using a random number:
function genID(serverNum) {
return(serverNum + "" + Math.floor(Math.random() * 100000));
}
But, it might be best to add the date/time element on the server and just store that whole unique ID in the database there.