copyValuesToRange returns nothing if startColumn is integer - google-apps-script

Writing a script where a cell with a formula is overwritten with value shown in the same cell. The following code WORKS (however formatted for readability):
var TargetHistorySheetRow = 95; //some arbitrary integer
var HistorySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("History");
HistorySheet.getRange(TargetHistorySheetRow, HistorySheet.getRange("1:1").getValues()[0].indexOf("Allocated")+1)
.copyValuesToRange(
HistorySheet,
HistorySheet.getRange("1:1").getValues()[0].indexOf("Allocated")+1,
HistorySheet.getRange("1:1").getValues()[0].indexOf("Allocated")+1,
TargetHistorySheetRow,
TargetHistorySheetRow
);
There is a repeating piece of code throughout (HistorySheet.getRange("1:1").getValues()[0].indexOf("Allocated")+1;) for finding a specific column number dynamically. For readibility, I moved this snippet to a variable and used the variable throughout instead. The following update DOES NOT WORK and changes the target cell to a blank
var TargetHistorySheetRow = 95; //some arbitrary integer
var HistorySheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("History");
var HistorySheetAllocatedColumn = HistorySheet.getRange("1:1").getValues()[0].indexOf("Allocated")+1;
HistorySheet.getRange(TargetHistorySheetRow, HistorySheetAllocatedColumn)
.copyValuesToRange(
HistorySheet,
HistorySheetAllocatedColumn,
HistorySheetAllocatedColumn,
TargetHistorySheetRow,
TargetHistorySheetRow
);
However, if I change only the startColumn parameter of copyValuesToRange back, it WORKS again:
HistorySheet.getRange(TargetHistorySheetRow, HistorySheetAllocatedColumn)
.copyValuesToRange(
HistorySheet,
HistorySheet.getRange("1:1").getValues()[0].indexOf("Allocated")+1,
HistorySheetAllocatedColumn,
TargetHistorySheetRow,
TargetHistorySheetRow
);
Changing that parameter to a static value (eg: 7), doesn't work either. What's going on here?

This appears to be a bug!
The behaviour you describe here is very interesting. When you call Range.copyValuesToRange(sheet, column, columnEnd, row, rowEnd), according to the documentation, the column, columnEnd, row and rowEnd parameters should all be Integers.
Now after some testing, you can see that the following line returns number:
typeof HistorySheet.getRange("1:1").getValues()[0].indexOf("ColumnHeaderC")
As does:
typeof HistorySheet.getRange("1:1").getValues()[0].indexOf("ColumnHeaderC") + 1
However when you log HistorySheetTargetColumn and HistorySheet.getRange("1:1").getValues()[0].indexOf("ColumnHeaderC") + 1 separately, you get the following result:
var HistorySheetTargetColumn = (HistorySheet.getRange("1:1")
.getValues()[0]
.indexOf("ColumnHeaderC") + 1);
Logger.log('variable = ' + HistorySheetTargetColumn); // => variable = 3
Logger.log('method call = ' + HistorySheet.getRange("1:1")
.getValues()[0]
.indexOf("ColumnHeaderC") + 1);
// => method call = 21
You can see that the 21 concatenation happens because HistorySheet.getRange("1:1").getValues()[0].indexOf("ColumnHeaderC") is being evaluated as a string before the + operator, meaning that after the + 1 the value becomes 21 and not 3, as it should be if it was being evaluated as an integer.
It therefore appears that when you use HistorySheetTargetColumn in .copyValuesToRange() is is re-evaluating HistorySheet.getRange("1:1").getValues()[0].indexOf("ColumnHeaderC") + 1 each time, getting 2 from the indexOf() return as a string, and then concatenating the 1 at the end. This is why your cell is evaluating to be blank - because Cell A21 is blank.
Workaround:
You can fix this in the mean time by adding parenthesis around HistorySheet.getRange("1:1").getValues()[0].indexOf("ColumnHeaderC") + 1 on assignment:
var HistorySheetTargetColumn = (HistorySheet.getRange("1:1")
.getValues()[0]
.indexOf("ColumnHeaderC") + 1);
Reporting the Issue:
I have taken the liberty of reporting this on Google's Issue Tracker for you, detailing the behaviour:
Range.copyValuesToRange(sheet, column, columnEnd, row, rowEnd) reading string instead of integer
You can hit the ☆ next to the issue number in the top left on the page which lets Google know more people are encountering this and so it is more likely to be seen to faster.
References:
Google Apps Script - Range.copyValuesToRange(sheet, column, columnEnd, row, rowEnd)
w3schools - JavaScript Array indexOf() Method
MDN web docs - Arithmetic operators
Google's Issue Tracker
Range.copyValuesToRange(sheet, column, columnEnd, row, rowEnd) reading string instead of integer

Related

Using Row() within a formula with appendRow

So I'm pulling some data from GMail and adding a new row to a sheet that has a specific format. Name, Address, etc etc
On Column "P" I want to replicate the below:
=IF(NOT(ISBLANK($J3985)),"Replied", IF((TODAY()>=$O3985),"Late", "OK"))
However, I want to replace 3985 with Row(), for the row number that I'm appending, while I'm appending it. I've tried playing with: ADDRESS(row(),10) but this returns a string value that I can't seem to re-insert into a formula in a manner that works.
What I'm passing through in appendRow now:
var replied = "";
var later = x // a Date that's today + 6 weeks
var checkResult = `=IF(NOT(ISBLANK(` + replied + `)), "Replied", IF((TODAY()>=` + later + `), "Late", "OK"))`;
I want it so that I can populate the "responded" cell at a later point in the sheet and for this to still work. Would be keen to hear your suggestions for the same.
If you use appendRow:
=IF(NOT(ISBLANK(INDIRECT("RC[-6]",FALSE))),"Replied", IF((TODAY()>=INDIRECT("RC[-1]",FALSE)),"Late", "OK"))
If you use setFormulaR1C1:
Method A
Putting the row number directly with template literal
Method B
You could use setFormulaR1C1(formula)
'=IF(NOT(ISBLANK(RC[-6])), "Replied", IF((TODAY()>=RC[-1]), "Late", "OK"))';

Copy filtered data to another sheet but creates a blank row if it met a condition

For example, I have this data,
I want to filter the data which is "USA" and copy it to another sheet but I have to creates a blank row if it met a condition. For example like this
Is it possible?
I have also tried
IF(AND(F2=FALSE, NOT(ISBLANK(F2))), "", INDEX(QUERY('Sheet 1'!A2:E, "Select A where A contains '"&"USA"&"'"),COUNTIF($F$2:F2,TRUE),1))
But it didn't work as I expected
Here is a formula version:
=ArrayFormula(
array_constrain(
sortn(
filter(
{if(A:A="USA",A:A,),if(A:A="USA",B:B,),A:A="USA",
if(A:A="USA",row(A:A),iferror(vlookup(row(A:A),if(A:A="USA",row(A:A),),1,true),0)+1)},
A:A<>""),
1000,2,4,1),
1000,3)
)
The reason for the long formula is mainly finding a way to get just one row to replace one or more rows that don't start with USA. The basis of the formula is to do a lookup for non-USA rows to get the row number of the most recent USA row. All of the non-USA rows in the same block then have the same row number and can be discarded (apart from the first) using Sortn.
I have added an extra non-USA row at the beginning to check that this edge case works and falls through to the Iferror clause.
Since you provided Google Apps Script as tag, I assume you are open to script answers? If so, then I'll provide this code below. Is this what you are looking for?
function copyUSA() {
var sheetSrc = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
var sheetDst = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet2");
var rowSrc = sheetSrc.getLastRow();
var values = sheetSrc.getRange("A2:A" + rowSrc).getValues().flat();
values.forEach(function (value, index){
var rowDst = sheetDst.getLastRow() + 1;
if(value == "USA"){
// If USA, copy then replace third column with TRUE
var row = index + 2;
sheetSrc.getRange(row + ":" + row).copyTo(sheetDst.getRange(rowDst + ":" + rowDst));
sheetDst.getRange("C" + rowDst).setValue("TRUE");
}
else{
// If not, set third column to FALSE
sheetDst.getRange("C" + rowDst).setValue("FALSE");
}
});
}
Sheet1:
Sheet2:
My assumptions were based from your formula. These are:
I assumed that you add blank when it is not USA. Thus having 2 blanks on the output
If USA, 3rd column is TRUE, else, FALSE with blank data
Based on your formula, it seems your data starts at A2, thus I adjust the code too.
You are also getting the 4th and 5th column on your formula but since you didn't show it in your post, I can't assume any values for it.
If the answer isn't the one you are looking for, I apologize.

Is there a way to merge multiple range into a single range for sheets appscript?

I was trying to merge 2 ranges(WITH values) into 1 range(empty)
I was trying to do it like this
function regressionMaker(){
var app = SpreadsheetApp;
var activeSheet = app.getActiveSpreadsheet().getActiveSheet();
var name = activeSheet.getRange ("E2:E").getValues();
var title = activeSheet.getRange ("F2:F").getValues();
var x = " - ";
activeSheet.getRange("T2:T").setValues(name + x + title);
}
All I get is an error saying "Cannot find method setValues(string)"
I was expecting of having a result of
Column 1 Column 2 Column3
Apple Banana Apple - Banana
Updates
var name = activeSheet.getRange ("E2:E").getValues();
var title = activeSheet.getRange ("F2:F").getValues();
var merged = name.map(
function(nameValues,rowIndex){
var titleValues = title[rowIndex];
return [nameValues[0] + ' - ' + titleValues[0]];
}
);
activeSheet.getRange("V2:V").setValues(merged);
Tho nothing happened :(
Problem 1.1
You are trying to pass a String to the setValues() method which can only be passed a two-dimensional Array (that is, Array of Arrays). Error message states just that: "there is no method setValues() such that it accepts String as an argument".
Problem 1.2
getValues() method yields an Array of Arrays as well, so your names and title variables actually contain [['Apple']] and [['Banana']] respectively (assuming your ranges include values only).
Problem 1.3
JavaScript is dynamically typed, so it performs type coercion for you and it often leads to issues like you have. In the case of +, if any argument is of type String, JS will try to coerce the other one to a String (e.g. '11' + 2 = '112').
Solution
The abovementioned problems should lead you to an algorithm: "map each row of data in column 1 to a column 2 such that the result is a string concatenation of both values separated by a hyphen with two whitespaces".
Application
Since you do a one-column merge, all you need is to get the first value from column 1, the first value from column 2, concatenate them and wrap into an Array representing new cell.
var merged = names.map(function(nameValues,rowIndex){
var titleValues = titles[rowIndex];
return [nameValues[0] + ' - ' + titleValues[0]];
});

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.

Setting a specific cell on the active sheet

I am attempting to set the value of a single cell on the active spread sheet. I know that I can uses .setValue to record a value to a single cell range. I want to get the range of a single cell than use .setValue to give it a specific value. I am using the following
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
var testing_range = sheet.getRange("range_BOL_number");
logger.log("Rows: " + testing_range.getHeight() + " Columns: " + testing_range.getWidth());
var cell = testing_range.getCell(1, 0);
cell.setValue('999');
The problem is that when I try and run it, I get an error that says that the getCell call is out side of the range. The log entry tells me that testing_range is 2 rows by 1 column. Not sure what I am doing wrong as I copied the getCell code from the documentation.
its because getCell parameters are not zero based but one-based. you can see that from the example in the official docs. it uses getCell(1,1) to get the first cell from the example range:
https://developers.google.com/apps-script/reference/spreadsheet/range
personally i think it should be zero based as the parameters are not really "row" and "column" but a delta.