JXA 'Can't convert types' - javascript-automation

JavaScript for Apple Numbers - I'm trying to use the row.address of a selected range to step through a for{} loop, but the values returned aren't compatible integers (if that makes sense). A spreadsheet is open, and several rows are selected when the script runs.
var firstRow = cells[0].row.address;
var lastRow = cells[cells.length-1].row.address;
// these values seem valid, and can be inserted into the spreadsheet
cells[0].value = firstRow;
cells[1].value = lastRow;
for (i=firstRow; i<lastRow; i++) {
// this generates an error that terminates the script:
// The action "Run JavaScript" encountered
// an error: "Error: Error: Can't convert types."
cells[2].value = i; // this line never runs
}
If I use math on firstRow or lastRow, I get the same error (e.g., var j = lastRow+1), so it's like the 'integer' value isn't really an integer value. How do I reference these values as integers, or at least so as to not get the error?
Thanks.

I think you have to run the getter on address. It's all in the poor documentation: firstRow = cells[0].row.address(); should do the trick.
address is an "ObjectSpecifier", and address() is syntactic sugar to get the value of whatever this ObjectSpecifier points to.
You could try to get the properties of objects in JXA with app.properties(object), but that doesn't always work.

Related

Implementing vlookup and match in function

I'm trying to create a function in Sheets that combines a "Vlookup" and "Match" combination that I use frequently.
I want to use my function, "Rates" to accept 1 argument and return a combination of Vlookup and Match, that always uses the same values.
Vlookup(argument, DEFINED RANGE (always stays the same defined range), match(A1 (always cell A1), DIFFERENT DEFINED RANGE, 0), FALSE)
I have tried creating a script, but have no experience coding, and I receive an error that "vlookup is not defined"
function ratesearch(service) {
return vlookup(service, Rates, Match($A$1,RatesIndex,0),FALSE);
}
Actual results: #ERROR!
ReferenceError: "vlookup" is not defined. (line 2).
function findRate() {
var accountName = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(1,1).getValue(); //determine the account name to use in the horizontal search
var rateTab = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Rates'); //hold the name of the rate tab for further dissection
var rateNumColumns =rateTab.getLastColumn(); //count the number of columns on the rate tab so we can later create an array
var rateNumRows = rateTab.getLastRow(); //count the number of rows on the rate tab so we can create an array
var rateSheet = rateTab.getRange(1,1,rateNumRows,rateNumColumns).getValues(); //create an array based on the number of rows & columns on the rate tab
var currentRow = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getActiveCell().getRow(); //gets the current row so we can get the name of the rate to search
var rateToSearch = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getRange(currentRow,1).getValue(); //gets the name of the rate to search on the rates tab
for(rr=0;rr<rateSheet.length;++rr){
if (rateSheet[rr][0]==rateToSearch){break} ;// if we find the name of the
}
for(cc=0;cc<rateNumColumns;++cc){
if (rateSheet[0][cc]==accountName){break};
}
var rate = rateSheet[rr][cc] ; //the value of the rate as specified by rate name and account name
return rate;
}
Optimization points for Alex's answer:
Never forget to declare variables with var, const or let (rr and cc). If you omit the keyword, the variables will be global and cause you a lot of trouble (as they will not reset after the loop finishes). The best way is to use block-scoped let.
Following #1, do not rely on out-of-scope variables (rateSheet[rr][cc]).
You do not need to call SpreadsheetApp.getActiveSpreadsheet() multiple times - that's what variables are for. Call once, then reuse.
getRange(1,1,<last row>, <last col>) is equivalent to a single getDataRange call.
use find or findIndex method to avoid verbose loops.
With the points applied, you get a clean and optimized function to use:
const findRate = () => {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const accountName = ss.getActiveSheet().getRange(1, 1).getValue();
const rateTab = ss.getSheetByName("Rates");
const rates = rateTab.getDataRange().getValues();
const currentRow = ss.getActiveSheet().getActiveCell().getRow();
var rateToSearch = ss.getActiveSheet().getRange(currentRow, 1).getValue();
const rr = rates.findIndex((rate) => rate === rateToSearch);
const [firstRates] = rates;
const cc = firstRates.findIndex((rate) => rate === accountName);
return rates[rr][cc];
};
Note that the "vlookup" is not defined error indicates there is no vlookup variable / function declaration in scope. Which obviously is the case as there is no built-in Google Apps Script vlookup function.
You can't access random ranges from a custom function so you would have to provide the data to the function, some of the other solutions here that use get active spreadsheet won't work as a custom function which I am guessing is what the OP is looking for, here is an example of a script that does that but word of warning before you go down this road, custom functions are much slower than the built in functions so doing this will be much slower than vlookup and match, if you only have a few functions like this in the sheet you will be fine, but if you build large tables with dozens of rows that use custom functions it will slow down you spreadsheet substantially.
// Combines VLOOKUP and MATCH into a single function
// equivalent to VLOOKUP(rowValue, tableData, MATCH(columnName, tableHeader))
// but in this version tableData includes tableHeader
function findInTable(tableData, columnName, rowValue) {
if (rowValue === "") {
return "";
}
if (tableData.length == 0) {
return "Empty Table";
}
const header = tableData[0];
const index = header.indexOf(columnName);
if (index == -1) {
return `Can't find columnName: ${columnName}`;
}
const row = tableData.find(row => row[0] == rowValue);
if (row === undefined) {
return `Can't find row for rowValue: ${rowValue}`;
}
return row[index];
}
Another optimization I suggest you do is use named ranges, it allows you to transform something like:
=VLOOKUP(C5, 'Other Sheet'!A2:G782, MATCH("Column Name", 'Other Sheet'!A1:G1))
into a more readable and easier to look at:
=VLOOKUP(C5, someTableData, MATCH("Column Name", someTableHeader))
for the custom function form this will look like:
=findInTable(A1:G782, "Column Name", C5)
Note that I shorted the argument list by merging the data and header, this makes some assumptions about the table structure, e.g. that there is a one header line and that the lookup value is in the first column but it makes it even shorter and easier to read.
But as mention before this comes at the cost of being slower.
I ended up giving up on using this for my needs due to how slow it is and how much faster VLOOKUP and MATCH are since they are built in functions.
vlookup is not something you can use in a function in a script, it is a spreadsheet formula.
Google Scripts use JavaScript, so you'll need to write your code in JS then output it to a relevant cell.
If you could share your sheet/script we could help figure it out with you.

How to print variables in google script

function checkWho(n,b)
{
// n and be are comparing two different cells to check if the name is in the registry
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var glr = sheet.getLastColumn();
var glr2 = sheet.getLastRow();
for(var i = 9; i <= glr; i++)
{
for(var z = 10; z<= glr2; z++)
{
if( n == b)
{
var courts = sheet.getRange(3,i).getValue();
var times = sheet.getRange(z,10).getValue();
return(b+ " "+"has booked"+" "+ courts+" "+"at"+times);
}
}
}
}
I am having issues printing out the values contained in var courts and var times. My code consists of two for loops iterating through columns and rows and eventually spitting out the users name, what court they've booked and at what time. As of now the name gets printed, but the courts and the times don't.
it currently prints: "(name) has booked at"
When I want it to print:" (name) has booked court 1 at 4:30"
Any help on the situation?
What is happening is that the the nested for statements are overwriting the result. It's very likely that the court and time are "" (empty strings) because the iteration is done from a start column/row and repeated for the next columns/rows. It's very common that the last column/rows are empty.
Side notes:
The script include a comment mentioning that custom function arguments are cells but custom function can't use cells as argument in the same sense of getCurrentCell(). Custom functions arguments types could be String, Number, Date or Array.
It doesn't make sense to compare the arguments inside the nested for statements as they doesn't change on each iteration.
Including a return inside the nested for statement will stop the iterations. As the arguments are not iteration dependent, only the first iteration is made for the case considered in the question.
If you return a string and your matter is to print those variables, then replace your return statement like this.(same as java script ES6 )
return(`${b} has booked ${courts} at ${times}`);
App script is grooming scripting language. My suggestion is working properly now.

Google Sheets Custom Formula Sometimes Works Sometimes Doesn't

I have a spreadsheet in which I developed a custom function called RawVal:
function RawVal(BlockName) {
try {
var rawVal = 1;
var thiSheet = SpreadsheetApp.getActiveSheet();
var BlockRow = thiSheet.getRange("C:C").getValues().map(function(row) {return row[0];}).indexOf(BlockName);
if (BlockRow > -1) {
var baseVal = thiSheet.getRange("B" + (BlockRow+1)).getValue();
var ingVal = thiSheet.getRange("D" + (BlockRow+1)).getValue();
rawVal = Math.max(baseVal, ingVal);
Logger.log(BlockName+": base="+baseVal+"; ing="+ingVal+"; max="+rawVal);
}
return rawVal;
}
catch (e) {
Logger.log('RawVal yielded an error for " + Blockname + ": ' + e);
}
}
While the function is long, the intent is to replace a moderately sized function from having to be typed in on each row such as:
=if(sumif(C:C,"Emerald Block",D:D)=0,sumif(C:C,"Emerald Block",B:B),sumif(C:C,"Emerald Block",D:D))
The problem is sometimes it works and sometimes it just doesn't. And it doesn't seem to be related to the content. A cell that worked previously may display #NUM and have the error "Result was not a number". But if I delete it and retype it (but oddly not paste the formula), most of the time it will calculate correctly. Note: it is NOT stuck at "Loading", it is actually throwing an error.
Debug logs haven't been useful - and the inconsistency is driving me crazy. What have I done wrong?
EDIT: I replaced the instances of console.log with Logger.log - the cells calculated correctly for 6 hours and now have the #NUM error again.
It seems that your custom function is used in many places (on each row of the sheet). This and the fact that they stop working after a while points to excessive computational time that Google eventually refuse to provide. Try to follow their optimization suggestion and replace multiple custom functions with one function that processes an array and returns an array. Here is how it could work:
function RawVal(array) {
var thiSheet = SpreadsheetApp.getActiveSheet();
var valuesC = thiSheet.getRange("C:C").getValues().map(function(row) {return row[0];});
var valuesBD = thiSheet.getRange("B:D").getValues();
var output = array.map(function(row) {
var rawVal = 1;
var blockRow = valuesC.indexOf(row[0]);
if (blockRow > -1) {
var baseVal = valuesBD[blockRow][0];
var ingVal = valuesBD[blockRow][2];
rawVal = Math.max(baseVal, ingVal);
}
return [rawVal];
}
return output;
}
You'd use this function as =RawVal(E2:E100), for example. The argument is passed as a double array of values, and the output is a double array too.
Also, when using ranges like "C:C", consider whether the sheet has a lot of empty rows under the data: it's not unusual to see a sheet with 20000 empty rows that pointlessly get searched over by functions like that.
Alternative: use built-in functions
It seems that your custom function is mostly a re-implementation of existing =vlookup. For example,
=arrayformula(iferror(vlookup(H:H, {C:C, B:B}, 2, false), 1))
looks up all entries in H in column C, and returns the corresponding values in column B; one formula does this for all rows (and it returns 1 when there is no match). You could have another such for column D, and then another arrayformula to take elementwise maximum of those two columns (see Take element-wise maximum of two columns with an array formula for the latter). The intermediate columns can be hidden from the view.

Cannot convert NaN to (class)

While writing a Google Apps Script, I ran across this error:
"Cannot convert NaN to (class). (line 19, file "Code")
The line in question:
pos[pos.length] =
ss.getSheets()[sheetNumber + 13].getRange(teamRow + i, teamCol + 1).getValue();
I searched stackoverflow, but the best I could find was that "getRange" wasn't being passed integers. However, I derived the teamRow and teamCol from integers, and i is an index for a for loop, also an integer.
This error only appears when I call my function through another function.
Final question: what does the error mean, and what can I do to fix it?
NaN means Not a Number. It is basically used in floating point arithmetic to significate that a value cannot be expressed using the convention. You can look for IEEE 754 to get more details.
Your error message informs you that, in your script, you are using a NaN value in an expression that forbids it.
To track it, you could make your life simplier in using local variables to store intermediate values. Then you'll be able to inspect them either using the debugger or by logging them, as suggested in a comment.
For instance :
var allSheets = ss.getSheets();
var targetSheetIndex = sheetNumber + 13;
var targetSheet = allSheets[targetSheetIndex];
var targetRow = teamRow + i;
var targetCol = teamCol + 1;
var targetCell = targetSheet.getRange(targetRow, targetCol);
var targetValue = targetCell.getValue();
Using this technique you can identify where your code may fail and prevent it from failing by guarding it with tests (existence tests, boundary tests, …). After that, you can reassemble your code, either using your original single line or a balance between a single line and the above splitting example.

How to use the getRange() method with an array value as one of the parameters

I don't have much experience using Javascript but I'm developing a simple code to filter some information relevant to a professor I'm helping. I am searching the row number of a certain amount of data using a for and then I'm using an array to store all the rows that contain those words. Since I'm using Appscript, I only need to relocate a certain amount of data from the row I'm returning to a final row I've already know. My code is as follows:
if(cell === "Average")
{
index++;
initialcoords[index] = n; // n is the iteration variable in the for
}
I've tested the contents of the array and they are just fine, so I'm storing correctly the rows. The problem is that I'm using a different method to paste the data in a different sheet in Google Spreadhsheets. My code to do so is the following:
function pasteInfo()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var source = ss.getSheetByName("Sheet 1");
var destination = ss.getSheetByName("Sheet 2");
var range = source.getRange(initialcoords[1], 1, 8, 3);
range.copyValuesToRange(destination, 4, 6, 4, 6);
}
My probelm is the getRange() since it prints an error like this:
can't find method getRange((class),number,number,number).
I believe that even if n is declared as an integer, the values that I'm returning are of a different type incompatible with the getRange() method. Could anyone help me to confirm this and to help me convert it to integer? I would really appreciate your help.
You first need to define the Sheet you want to get the data from since a Spreadsheet can have multiple Sheets.
You need to ensure you have appropriate default values defined before using the parameters, otherwise the interpreter will start making guess.
Provide defaults if parameters are empty:
function fillLine(row, column, length, bgcolor)
{
row = row || 0;
column = column || 0;
length = length || 1;
bgcolor = bgcolor || "red";
var sheet = SpreadsheetApp.getActiveSheet();
sheet.getRange(1+row, 1+column, 1, length).setBackground(bgcolor)
}
You may also try the solution offered by community: Can't get Google Scripts working