Cannot find method getRange(number,(class),number) - google-apps-script

I have read all answers about this error, but none of them work for me. So maybe someone has another idea.
I'm trying to pass parameter to function getRange but when I'm trying to invoke this function it shows me error
Cannot find method getRange(number,(class),number)
Here is my code:
function conditionalCheck(color,rangeCondition, rangeSum, criteria){
var condition = SpreadsheetApp.getActiveSheet().getRange(2,rangeCondition,15).getValues();
var val = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,rangeSum,15).getValues();
var bg = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(2,rangeSum,15).getBackgrounds();
var sum = 0;
for(var i=0;i<val.length;i++){
if(condition[i][0] == criteria && bg[i][0] != color){
sum += val[i][0];
}
}
return sum;
}
And I pass a custom function like:
conditionalCheck("#ffff00",1,3,A3)
This is how the sheet looks like:
I understand that JS trying to guess the type of parameters, that is why it thinks that ex. "rangeCondition" is a class, but I don't know how to give him a Number type.
Funny thing is, that this function works when I open the spreadsheet, but when I'm trying to invoke this function while I'm working it shows me this error. So to update sheet I have to reopen the whole spreadsheet.

I was able to reproduce the error by executing the function directly from the editor.
This behavior makes sense, considering custom function needs to receive a valid parameter. At runtime, your 'rangeSum' parameter is set to 'undefined', which invalidates the getRange() method as it requires a number.
It's actually quite strange that you got your 'for' loop working. In most cases, using the the '+' operator on array values obtained from the sheet will concatenate them into a string, so you'll get '5413' instead of 13 (5 + 4 + 1 + 3)
function calculateSum(column) {
column = column||1; //set the default column value to 1
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange(1, column, sheet.getLastRow());
var values = range.getValues();
var sum = 0;
for (var i=0; i < values.length; i++) {
sum += parseInt(values[i]);
}
return sum;
}
Finally, this approach is not very efficient in terms of performance. Once you get the instance of the object, you should use that instance instead of attacking the server with multiple calls like you do in the first 3 lines of your code. For example
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var range = sheet.getRange(1, 1, sheet.getLastRow(), 1);
If you plan on using this custom function in multiple places within the sheet, consider replacing it with the script that processes the entire range - this way you will only perform a single read and write operation to update all values. Otherwise, you may end up filling up your service call quotas very quickly
UPDATE
Instead of creating custom function, create the function that takes the entire range and processes all data in one call. You can test the 'calculateSum()' function above with predefined column number, e.g.
var column = 1 // the column where you get the values from
Upon getting the 'sum' value, write it into the target range
var targetRange = sheet.getRange("B1"); // the cell that you write the sum to
targetRange.setValue(sum);
Finally, you can make that function execute after you open or edit the sheet by appending the following code
function onOpen() {
var ui = SpreadsheetApp.getUi();
var menu = ui.createMenu('Menu')
.addItem('Calculate sum', 'calculateSum')
.addToUi();
calculateSum();
}
function onEdit() {
calculateSum();
}
Calling it from the custom menu would be simple - you just need to create the handler and pass function name as parameter.

I found a solution for a problem with reloading custom function after changing data.
The solution is described here:
Refresh data retrieved by a custom function in google spreadsheet
It showed that we cannot refresh custom function if we don't change inputted parameters because it is cached. So if we change inputted data, a function will reload. We can force a function if we put there timestamp

Related

How do I write data in every last column?

I need to add a new column everyday and the data insertion will be done individually for the specific row comparing with the input... please tell me if it is correct or not.
will this piece of code work for adding new columns ever day and initialize all the entries by 0?
function trig(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1);
var trigger = builder.create();
}
function addcol(){
var cname = builder.atDate( day, month, year)
var column = eventRange.getLastColumn();
sheet.insertColumnAfter(column).setName(cname);
sheet.getRange("E1").setValue(new Date()).setNumberFormat('d/M/yyyy');
var col = [];
for(var n=0 ; n<s.getMaxRows();n++){
col.getLastColumn().push(['0']);
}
ss.getRange('N:N').setValues(col);
}
// now for the insertion part
here the sr will be compared to SRN from the sheet (E) and if it matches it will replace 0 with 1 in the last column added everyday. plese tell me will this work?
function doPost(e){
var action = e.parameter.action;
if(action == 'scanner'){
return scanner(e);
}
}
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for(i=1; i<=R; i++)
{
if (srn == sheet.getDataRange([i][2]))
{
sheet.getDataRange([i],[C]).push[(1)];
sheet.append([i],[C]);
return ContentService.createTextOutput("Success").setMimeType(ContentService.MimeType.TEXT);
break;
}
}
}
Time-based trigger:
There are no event objects associated with time-based triggers, so variables like eventRange cannot work. It seems like you want to use variables in addcol that are defined in trig (e.g. builder). That is not possible. Also, if you want your function to run once a day, there is no need for lines like this: builder.atDate(day, month, year)). The trigger will be created by running this function once:
function createTrigger(){
var builder = ScriptApp.newTrigger("addcol").timeBased().everyDays(1).create();
}
Adding column with 0's:
There are many problems with the function addcol:
Several uninitialized variables are being used (s, builder, eventRange).
Unexisting methods are being used: e.g.: setNumberFormat is a method of the Range class, not of the Date object. You should use Utilities.formatDate(date, timeZone, format) to format dates. Also, you are using setName when inserting a new column, but that changes the sheet name. Is that what you want to do? And also, cname is assigned a trigger builder as value, which I seriously doubt is your purpose. The same way, an array col does not have a method getLastColumn().
You could use this addcol function instead (change your sheet name, currently set to Your sheet name, and the timeZone in formatDate, currently set to GMT:
function addcol() {
var sheet = SpreadsheetApp.getActive().getSheetByName("Your sheet name"); // Change accordingly
var lastCol = sheet.getLastColumn();
var lastRow = sheet.getLastRow();
if (sheet.getMaxColumns() === lastCol) sheet.insertColumnAfter(lastCol);
var newCol = sheet.getRange(1, lastCol + 1, lastRow, 1);
var values = [];
values.push([Utilities.formatDate(new Date(), "GMT", "d/M/yyyy")]); // Change accordingly
for (var i = 1; i < sheet.getLastRow(); i++) {
values.push([0]);
}
newCol.setValues(values);
}
Replacing 0's with 1's:
Assuming that you are getting the function scanner to run correctly and that the parameter e.parameter.sr is getting populated correctly, you can do the following:
function scanner(e){
var srn = e.parameter.sr;
var C = sheet.getLastColumn();
var R = sheet.getLastRow();
for (i=1; i<=R; i++) {
if (srn == sheet.getRange(i, 2).getValue()) {
sheet.getRange(i, C).setValue(1);
}
}
}
Here you were also using unexisting methods or providing incorrect parameters:
The method getDataRange doesn't allow any argument, you should be using getRange(row, column), and provide the row and column indexes separated by commas, not as if trying to access a 2D array.
break terminates the current loop, so only use it if you only want to update 1 cell. The same goes for return which finishes current function execution.
Reference:
Spreadsheet Service
Installable Triggers
Short
No
Long
There are several problems with the script:
getDataRange() expects no arguments passed (docs only say it is the same as using getRange(yourSheet.getLastRow(), yourSheet.getLastColumn()), not that you should do it). Certainly it does not expect instances of Array (bracket [] notation wraps C and i, which are of type Number into one). Moreover, it returns a Range, which at the time of writing does not have push() method.
getLastColumn() returns an instance of Number, and thus does not have a push() method as well. You are on the right track, though, since col is an Array, and you need to push() into it.
If you want the script to add a zero-filled column, don't get constant ranges: in current state, getRange('N:N') guarantees that each time you will re-initialize column N. Btw, same goes for getRange("E1").
You still haven't addressed issues listed in comments to your previous question.
Also, in your scanner function there is a syntax error: push[(1)] should be push([1]).
Also, the sheet variable is either undeclared or is declared globally, which is bad.
Notes
If you don't expect number of students to change dynamically, you can switch from getMaxRows() to getLastRow() to only zero-fill cells that are in range of cureent student info grid.
This question is a direct continuation of a currently closed one (please, always disclose that for reference at least).
How about skipping init to zero step at all? If cell is empty, getValue() / getValues() will return its value as an empty string, which is a falsy value, just as 0 is. If you want to count attendance at the end of period, a simple conditional will suffice to sum up.
The default MIME type for TextOutput instance obtained by createTextOutput() is plain text, so setting it to ContentService.MimeType.TEXT is an overkill in your case.
Reference
getDataRange() docs
getLastColumn() docs
getValue() docs
getValues() docs
Range docs
createTextOutput() docs
Falsy values explanation on MDN

How can I avoid a VLOOKUP Error that occurs when I delete a name?

The pictures used are only from an example sheet! My basic problem is that I have a list called Assignment in which names appear (dropdown list). For Location (in the assignment sheet) I use the following formula: =IF(C2<>"",VLOOKUP(C2,'Input Data'!C$3:D$7,2,FALSE),"")
These names are assigned certain values, they are in the same line. The names are defined in a worksheet called Input Data!
If I now delete a name like Green, John from the Input Data worksheet, then I get the following error in another worksheet (Evaluation). (More than 40 people have access to this worksheet and randomly delete names)In this evaluation worksheet the values are evaluated by the following formula:
=ARRAY_CONSTRAIN(ARRAYFORMULA(SUM(IF((IF($B$2="dontcare",1,REGEXMATCH(Assignment!$E$3:$E$577,$B$2 &"*")))*(IF($B$3="dontcare",1,(Assignment!$E$3:$E$577=$B$3)))*(IF($B$4="dontcare",1,(Assignment!$D$3:$D$577=$B$4)))*(IF($B$5="dontcare",1,(Assignment!$F$3:$F$577=$B$5)))*(IF($B$6="dontcare",1,(Assignment!$B$3:$B$577=$B$6))),(Assignment!S$3:S$577)))), 1, 1)
The following error appears in the evaluation sheet:
Error:
During the evaluation of VLOOKUP the value "Green, John" was not found.
How can I avoid this error? Is it possible to avoid this error with a macro that deletes Names from assignment sheet that are not in the Input data sheet? Do you have any ideas for a code?Maybe a Formula or perhaps a Macro?
example sheet with explanation: https://docs.google.com/spreadsheets/d/1OU_95Lhf6p0ju2TLlz8xmTegHpzTYu4DW0_X57mObBc/edit#gid=1763280488
If what you want to do is make sure that rows are deleted in a sheet when there are incorrect values you could try something like this in Apps Script:
function onEdit(e) {
var spreadsheet = e.source;
var assignment = spreadsheet.getSheetByName("Assignment");
var assignmentRange = assignment.getDataRange();
var assignmentNames = assignment.getRange(3, 2, assignmentRange.getNumRows());
var inputData = spreadsheet.getSheetByName("Input Data");
var inputDataRange = inputData.getDataRange();
var i = 1;
while(assignmentNames.getNumRows() > i){
var currentCell = assignmentNames.getCell(i, 1);
var txtFinder = inputDataRange.createTextFinder(currentCell.getValue());
txtFinder.matchEntireCell(true);
if(!txtFinder.findNext()){
assignment.deleteRow(currentCell.getRow())
}else{
// We are only steping when no elements have been deleted
// Otherwise we would skip rows due to shifting in row deletion
i++;
}
}
}
Explanation
onEdit is a special function name in Apps Script that would execute every time it's parent sheet is modified.
After that we retrieve the spreadsheet from the event object
var spreadsheet = e.source;
Now we get the relevant range in the Assignment sheet. Look at the usage of getDataRange documentation to avoid retrieving unnecessary cell values. And from that range we actually get the specific column we are interested on.
var assignment = spreadsheet.getSheetByName("Assignment");
var assignmentRange = assignment.getDataRange();
var assignmentNames = assignment.getRange(3, 2, assignmentRange.getNumRows());
Now we do the same for the other sheet(Input Data):
var inputData = spreadsheet.getSheetByName("Input Data");
var inputDataRange = inputData.getDataRange();
Note: Here I'm not getting a specified column because I assume that the full name will not repeat in any other column. But if you want you could get the specified range as I have done at Assignment.
After that we want to look for specific values in the Assignment range that don't exist in the Input Data sheet, you should try the TextFinder.
For every name in Assignment you should create a TextFinder. I have also forced to make a whole cell match.
var i = 1;
while(assignmentNames.getNumRows() > i){
var currentCell = assignmentNames.getCell(i, 1);
var txtFinder = inputDataRange.createTextFinder(currentCell.getValue());
txtFinder.matchEntireCell(true);
If txtFinder finds a value the findNext() will evaluate to true. In the other hand when the txtFinder does not find a value it will be null and evaluated to false.
if(!txtFinder.findNext()){
assignment.deleteRow(currentCell.getRow())
}else{
// We are only stepping forward when no elements have been deleted
// Otherwise we would skip rows due to shifting in row deletion
i++;
}
}
}

Google sparesheet formula/function to get random values without recalculating(randbetween)

I am trying to achive the following. In google sparesheet I have one sheet with values "AllValues", in another sheet "Randomvalues" I would like to get random values from sheet "AllValues".
I have tried two options, first I tried randbetween formula:
=INDEX(AllValues!A4:A103,RANDBETWEEN(1,COUNTA(AllValues!A4:A103)),1)
It is working, but it refresh/recalculate new values all the time column is changed. Googeled a lot and seems that there is not much to do to freeze already calculated results.
Next I tried function:
function random() {
var sss = SpreadsheetApp.getActiveSpreadsheet();
var ss = sss.getSheetByName('Values'); //the sheet that has the data
var range = ss.getRange(1,1,ss.getLastRow(), 4); //the range you need: 4 columns on all row which are available
var data = range.getValues();
for(var i = 0; i < data.length; i++)
{
var j = Math.floor(Math.random()*(data[i].length)); //method of randomization
var element = data[i][j]; // The element which is randomizely choose
ss.getRange(i+1, 6).setValue(element);
}
}
But this function is not working for me, google sparesheet gives error on line 11, that setVaue is not allowed.
Line 11: ss.getRange(i+1, 6).setValue(element);
Googled this one too, there are lot of suggestion, but I am not very familiar with functions, I did not managed to get it working.
Hope that someone can help me out.
Using a formula assumes repeated calculations usually. You cannot prevent them and only can try to return old values instead. This task is not trivial, since any formula cannot refer to the same cell where the result is to be returned (a circular reference occurs). Do not use formulas for single time calculation.
On the other hand, using a script function makes it possible to generate required data directly and only once or on demand. I think, the function below will help you to understand all the neccesary steps for sample source and target ranges.
function random() {
var source = "AllValues!A4:A103",
target = "RandomValues!F2:F22";
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sourceValues = ss.getRange(source).getValues(),
targetRange = ss.getRange(target),
targetValues = [];
while (targetValues.length < targetRange.getHeight()) {
var randomIndex = Math.floor(Math.random() * sourceValues.length);
targetValues.push(sourceValues[randomIndex]);
}
targetRange.setValues(targetValues);
}
You can run it manually or choose a proper trigger.
There are multiple ways of achieving this goal.
Custom Menu
As mentioned by #Tanaike, you can avoid the recalculation and the formula dependency by using a Custom Menu:
// #OnlyCurrentDoc
// Create a function that binds the "simple trigger" for the open event:
function onOpen(e) {
// Add a menu to the UI with the function we want to be able to invoke.
const ui = SpreadsheetApp.getUi();
ui.createMenu("Randomizer")
.addItem("Sample from 'AllValues' sheet", "sampleAllValues")
.addToUi();
}
You then need a function definition matching this name sampleAllValues, and when the user selects the associated menu option, it will be invoked with the permissions of the clicking user (the user will be prompted first to provide consent for access per the script's OAuth scopes).
function sampleAllValues() {
const wb = SpreadsheetApp.getActive();
const destination = wb.getSheetByName("RandomValues");
const source = wb.getSheetByName("AllValues");
if (!source || !destination)
throw new Error("Missing required sheets 'RandomValues' and 'AllValues'");
// Create a flat array of all non-empty values in all rows and columns of the source sheet.
const data = source.getDataRange().getValues().reduce(function (compiled, row) {
var vals = row.filter(function (val) { return val !== ""; });
if (vals.length)
Array.prototype.push.apply(compiled, vals);
return compiled;
}, []);
// Sample the smaller of 50 elements or 10% of the data, without replacement.
const sample = [];
var sampleSize = Math.min(50, Math.floor(data.length * .1));
while (sampleSize-- > 0)
{
var choice = Math.floor(Math.random() * data.length);
Array.prototype.push.apply(sample, data.splice(choice, 1));
}
// If we have any samples collected, write them to the destination sheet.
if (sample.length)
{
destination.getDataRange().clearContent();
// Write a 2D column array.
destination.getRange(1, 1, sample.length, 1)
.setValues(sample.map(function (element) { return [ element ]; }));
// Write a 2D row array
// destination.getRange(1, 1, 1, sample.length)
// .setValues( [sample] );
}
}
Custom Function
If you still wanted to use a custom function from the RandomValues sheet, e.g.
RandomValues!A1: =sampleAllValues(50, AllValues!A1:A)
then you would need to return sample instead of write to a specific sheet. Note that custom functions are treated deterministically--they are computed at the time of entry and then only recalculated when the values of their arguments change. Custom functions run with very limited scope, so be sure to review their restrictions.
The above usage hints that you might find it useful to allow passing in the number of desired samples, and the values to sample from:
function sampleAllValues(sampleSize, value2Darray) {
const data = value2Darray.reduce(function (compiled, row) {
/* as above */
}, []);
/* sample as above */
return sample; // Must be 2D row or 2D column array, or a single primitive e.g. `1`
}
No matter which route you take, be sure to review your script's error logging by viewing your script's Stackdriver logs. (View -> Stackdriver Logging)
References:
Sheet#getRange
Custom functions
Custom menus
Array#reduce
Array#map
Array#splice
.push.apply

Google Apps Script - How do I create a trigger for a function with a parameter? [duplicate]

This question already has answers here:
Refresh data retrieved by a custom function in Google Sheet
(21 answers)
Closed 5 years ago.
I am working on a tracker for my cryptocurrencies. In the following function I fetch data from an API and put in a cell. For instance, when I want to get the value of Bitcoin in USD I put the formula =getCryptodata("bitcoin", "price_usd") in a cell and it returns the current price.
function getCryptoData(coin, api) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var url = 'https://api.coinmarketcap.com/v1/ticker/' + coin + '/';
var response = UrlFetchApp.fetch(url, {'muteHttpExceptions': true});
var json = response.getContentText();
var data = JSON.parse(json);
return parseFloat(data[0][api]);
}
I want the price to be updated every minute, so I've set a trigger for the function. All of a sudden there seems to be a problem now. Every time the function is triggered this error shows up (in my mailbox).
TypeError: Cannot read property "(class)#35c554a5" from undefined.
When I run the script from the script editor I get the same problem. Then I came up with the idea to check whether the parameters in the function are defined or not, and if not, to give them a generic value. Google seemed to have a problem with the undefined parameters (although they are defined in the sheet). So I added these lines before the lines I already had in my function.
if (typeof coin == 'undefined') {
var coin = "bitcoin";
}
if (typeof api == 'undefined') {
var api = "price_usd";
}
else {
Now the error is gone but the values are not updated when the function is triggered.
What am I missing?
So there are a few things going on here:
You are trying to load an active spreadsheet but when you trigger a script, there is no active spreadsheet. (Why are you loading this spreadsheet? You don't seem to be doing anything with it)
Your script returns a value but if your script is triggered from a schedule (so not from another function) the returned value will be returned to nothing. You need to put this value somewhere useful.
A solution to this issue could be to directly put the value from the API in the sheet. To do this the script requires a small change.
Example:
function getCryptoData(coin, api, target) {
var ss = SpreadsheetApp.openById("YOUR-ID-HERE"); // The script has no active spreadsheet so make sure to acquire the spreadsheet by providing the ID
var sheet = ss.getSheets()[0];
var url = 'https://api.coinmarketcap.com/v1/ticker/' + coin + '/';
var response = UrlFetchApp.fetch(url, {'muteHttpExceptions': true});
var json = response.getContentText();
var data = JSON.parse(json);
SpreadsheetApp.getRange(target).setValue(parseFloat(data[0][api])); // Write the returned value directly to the range (e.g. A1) that was specified as an argument
}
So with such a function, you may create another function that you would trigger every one in a while to update your values:
function updateCryptoData() {
getCryptodata("bitcoin", "price_usd", "B2");
getCryptodata("bitcoin", "price_usd", "C2");
}
You don't need to put a formula in your sheet anymore if you choose to go with a solution like this one.
Note: this solution is not optimal from a performance point of view. However, I've tried to make as little code changes to your own code to make it work. Ideally you'd only call your API once per coin type.
When using fetch you should be aware there are daily limits to how much data can come back from the call. Because you are calling for data every minute, it is well within the realms of possibility you will/have exceeded the daily limit.
This will not need to reload the spreadsheet. First you have to create the trigger for specific coin and pricing.
function teste()
{
trigger_("bitcoin","price_usd","B2");
trigger_("litecoin","price_usd","B3");
trigger_("bitcoin","price_usd","B4");
}
Store the data corresponding to the trigger, with reference to the trigger_id.
function trigger_(coin,api,target)
{
var new_trigger = ScriptApp.newTrigger(getCryptoData).timeBased().everyMinutes(1).create();
var trigger_id = new_trigger.getUniqueId();
PropertiesService.getUserProperties().setProperty(trigger_id, coin+","+api+","+target);
}
When the trigger gets fired, it gets the corresponding data(coin,price,target) with which the api is called and the target cell gets updated.
function getCryptoData(event)
{
var trig = ScriptApp.getProjectTriggers();
for(var i =0; i<trig.length; i++)
{
if(trig[i].getUniqueId()== event.triggerUid )
{
var cryptoData = PropertiesService.getUserProperties().getProperty(event.triggerUid);
cryptoData = cryptoData.split(",");
var coin = cryptoData[0];
var api = cryptoData[1];
var target = cryptoData[2];
var ss = SpreadsheetApp.openById("YOUR_SPEADSHEET_ID").getSheets(); // change accordingly
var sheet = ss[0]; // change accordingly
var url = 'https://api.coinmarketcap.com/v1/ticker/' + coin + '/';
var response = UrlFetchApp.fetch(url, {'muteHttpExceptions': true});
var json = response.getContentText();
var data = JSON.parse(json);
Logger.log(parseFloat(data[0][api]));
ss[0].getRange(target).setValue(parseFloat(data[0][api]));
}
}
}
NOTE : Daily limit to run trigger for normal user is 90min/day
app script quota

Using Built-in function inside Custom Function

In google sheets, I'd like my custom function to use one of the built-in functions. Specifically, this function would take a string as a parameter, comb through another sheet to locate that value, and then return a link to that cell's address.
function href(findMe) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var hrefSheet = ss.getSheetByName("otherSheet");
hrefSheet.activate();
var rows = hrefSheet.getDataRange();
var numRows = rows.getNumRows();
var finds = rows.getValues()[1][0];
var hrefCellRow;
for(var i=0; i<numRows; i++){
if(finds[i][0] == findMe){
hrefCellRow = i+1;
break;
}
}
return address(hrefCellRow, 1); //address() is a function that is built in to sheets
}
So if I have a value "XYZ" in the "otherSheet" sheet, when I type
=href("XYZ")
I'd like it to try to find the value XYZ and return an address to the active cell. Extra kudos if it returns an actual link that when clicked, goes to that cell.
I didn't think of this before. I could just use the address function outside of the script and have the custom function embedded in the built-in function (instead of the reverse, which I tried to do). it's not as pretty, but it would be simply
=address(href(findMe),1)
Still haven't found a way to link to another cell, without perhaps creating a function that would reset the active cell?