Row is out of bounds even though it exists [duplicate] - google-apps-script

This question already has an answer here:
Why do we use SpreadsheetApp.flush();?
(1 answer)
Closed 1 year ago.
I'm struggling to understand an issue I'm having with row indexes being out of bounds.
Using Apps Script I'm copying a range from one sheet to another. The sheet that is being copied to originally contains one empty row, and then I am copying two rows of data to it - this works fine.
I then am trying to copy the row heights from the source sheet to the destination sheet and this is where I run into the issue. The first row works fine but then when it gets to the 2nd row it gives me an out of bounds error. If I change the destination sheet so that it starts off with 2 empty rows then I do not get this error.
I also do not get the error if I first run the copy function and then separately run the change row height function after.
To me it seems like Apps Script is storing the destination sheet before the additional row is added and then when I try to change the height of the 2nd row it thinks it doesn't exist? Does anyone know a way around this.
What's also odd is if I run getLastRow() on the destination sheet right before changing the row heights it returns 2, so somewhere it knows there are 2 rows.
Here is my function that copies a named range and then the row heights:
const ss = SpreadsheetApp.getActiveSpreadsheet();
const namedRanges = [['namedRange', 24, 2]] //Range name, number of columns, number of rows
function copyRange(sheetName, rangeName, destRow, destCol) {
const sourceRange = ss.getRangeByName(rangeName);
const sourceSheet = sourceRange.getSheet();
const destSheet = ss.getSheetByName(sheetName);
for (let i = 0; i < namedRanges.length; i++) {
if (namedRanges[i][0] === rangeName) {
var rangeIndex = i;
break;
}
}
const destRange = destSheet.getRange(destRow, destCol, namedRanges[rangeIndex][2] - 1, namedRanges[rangeIndex][1] - 1);
sourceRange.copyTo(destRange)
for (let i = 0; i < namedRanges[rangeIndex][2]; i++) {
let row = sourceRange.getRow() + i;
let height = sourceSheet.getRowHeight(row);
destSheet.setRowHeight(destRow + i, height);
}
}

Fix was to add SpreadsheetApp.flush() between copying the range and changing the row heights as per TheMaster's comment above.

If you are just copying from a named range to a named range in which the latter only need be a single cell. Then this would suffice.
function myfunk() {
const nrl = [{srg = 'srg1',drg: 'drg1'},{srg = 'srg2',drg: 'drg2'}];
nrl.forEach(obj => {
obj.srg.copyTo(obj.drg)
});
}
This can only occur in the same spreadsheet and the sheet names are included in the definition of the named ranges.

Related

Unable to complete data move within apps script

Ive been messing with this google apps script for far too long and need some help.
I have a table on a sheet called options that starts on col A line 31 and is 3 col wide.
Col a is all checkboxes. I was able to write a script that checks to see which checkboxes are checked.
For each checked box it copies that rows data in b:c into an array.
Then opens an existing tab called Worksheet and is supposed to paste them in the first empty cell it finds in column b.
function createNamedRanges() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Worksheet");
var range = sheet.getRange("B2:C");
var namedRange = ss.setNamedRange("outputRange", range);}
function processSelectedRows() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Prompt Options");
var data = sheet.getDataRange().getValues();
var checkedRows = [];
for (var i = 30; i < data.length; i++) {
var row = data[i];
var checkbox = sheet.getRange(i + 1, 1).getValue() == true;
if (checkbox){
checkedRows.push([row[1], row[2]]);
} }
var worksheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Worksheet");
var pasteRange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName("outputRange");
pasteRange.offset(worksheet.getLastRow(), 0).setValues(checkedRows);
}
The first row on the worksheet tab are headers. The first array to copy over is 11 rows. When I ran the script. I got an error that sat there was only 1 row in the range and I had 11 rows of data. Ok, I figured I neeeded to name a range. This table will be a different size every time. So I named this range outoutRange and no matter what size i make it I get error messages.
This is my latest error message and it is hitting the very last line of code
Exception: The number of rows in the data does not match the number of rows in the range. The data has 11 but the range has 1007.
You assistance is appreciated
Modification points:
If your Worksheet is the default grid like 1000 rows and 26 columns, I think that pasteRange is all rows like 1000. I thought that this might be the reason for your current issue.
In order to retrieve the last row of the columns "B" and "C" of "Worksheet" sheet, how about the following modification?
From:
pasteRange.offset(worksheet.getLastRow(), 0).setValues(checkedRows);
To:
var lastRow = pasteRange.getLastRow() - pasteRange.getDisplayValues().reverse().findIndex(([b, c]) => b && c);
worksheet.getRange(lastRow + 1, 2, checkedRows.length, checkedRows[0].length).setValues(checkedRows);
By this modification, the values of checkedRows is put to the next row of the last row of columns "B" and "C" of "Worksheet" sheet.

Range Length in Google Apps Script

I want to run a script that copies a sheet data to a master sheet (append all my sheets).
The first part of copying and pasting is working but I want to add a column which tells me the name of the origin sheet. I wrote a loop for it but nothing is happening when I executing the script (only the copy and paste). This is my whole code:
function appendSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getActiveSheet();
var reportLastRow = sh.getLastRow()
var reportLastColumn = sh.getLastColumn()
var reportData = sh.getSheetValues(3,1,reportLastRow,reportLastColumn);
var recordsSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("2020 Data");
var lastRow = recordsSheet.getLastRow();
//var recordLastRow = sh.getLastRow();
var recordLastColumn = recordsSheet.getLastColumn();
var reportSheetName = sh.getSheetName();
recordsSheet.getRange(lastRow + 1,1,reportLastRow,reportLastColumn).setValues(reportData);
var arrayLength = (lastRow - reportData.length);
for (var i = 0 ; i <= arrayLength ; i ++) {
var taskDateCell = recordsSheet.getRange(arrayLength - i, recordLastColumn);
taskDateCell.setValues(reportSheetName);
}
}
Your code has several problems you should fix
Be aware of the fact that the method getRange() expects the syntax firstRow, firstColumn, numRows, numColumns - not firstRow, firstColumn, lastRow, lastColumn. You need to adjust your range and your function getSheetValues() (I assume it is you custom funciton based on the method getRange() accordingly.
If you assign a value to one cell at a time taskDateCell, you should use setValue() instead of setValues()
It seems like your definition of arrayLength might not be right. Test it by logging it.
Your main problem:
You define:
for (var i = arrayLength ; i < arrayLength ; i ++)
In other words:
Set i to arrayLength and iterate while i is smaller than arrayLength.
This condition is never fullfilled, and thus the number of iterations will be zero.
As a genral advice: Implement in your code many logs - to visualize important values, such a range notations and length of arrays, or counter variables in a loop - this will help you to find bugs faster.
Explanation:
First of all I optimized your code. You have unnecessary lines of code which make your code difficult to be understood but also slow. The optimizations involve reducing the number of lines to the minimum, defining constant variables, making the variable names more descriptive and finally getting rid of the for loop by replacing it with a more efficient approach.
Another correction would be at this line: sh.getSheetValues(3,1,reportLastRow,reportLastColumn); Here you are starting from the third row but you are getting two rows extra; reportLastRow is the number of rows you want to get but since you are starting from the third row you need to deduct 2.
To answer your question, one way to solve your problem is to set the name of the report to the last column of the records sheet where you entered the data from the report sheet. Since you want to add the same value (sheet name) to every row, you can select a 2D range but use setValue.
I am not a big fan of getActiveSheet in const report_sh = ss.getActiveSheet(); since this line is assuming that you have selected the desired sheet (report sheet) in the UI. Please be careful with that, otherwise change that line to something like that:
const report_sh = ss.getSheetByName('report sheet');
and of course adjust 'report sheet' to the name of the sheet you want to append.
Solution:
function appendSheet() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const records_sh = ss.getSheetByName("2020 Data");
const report_sh = ss.getActiveSheet();
const reportData = report_sh.getSheetValues(3,1,report_sh.getLastRow()-2,report_sh.getLastColumn());
const records_lr = records_sh.getLastRow();
records_sh.getRange(records_lr+1,1,reportData.length,reportData[0].length).setValues(reportData);
SpreadsheetApp.flush();
records_sh.getRange(records_lr+1,records_sh.getLastColumn()+1,reportData.length,1).setValue(report_sh.getSheetName());
}

Copy a range and Paste it to a Sheet that is using a Filter [duplicate]

I want to know, how to use setValue, if there are filtered rows, so that only the shown rows (C1 + one row down to last row of C) get a value.
x(){
var sheet = SpreadsheetApp.getActiveSheet();
var lastRow = sheet.getLastRow();
sheet.getRange(C2, lastRow).setValue('x');
}
Update
It works, but very slowly. I have tested the following code and it works fast. It must start in the second shown row. The following solution works both with and without filter. What is not yet running is the second row (C2). The copied value is always inserted there. In addition I would like to do without an auxiliary cell for copying if possible. Is it possible to copy setValue for the copypaste function (getValue)?
function x() {
var spreadsheet = SpreadsheetApp.getActive();
var lastRow = spreadsheet.getLastRow();
spreadsheet.getRange('C2:'+'C'+lastRow).activate();
spreadsheet.getRange('C1').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_NORMAL, false);
};
The goal is to put an x in the currently visible (not the hidden or non-visible filtered) cells of column C. For this I just need to know how to specify the second visible cell as getRange value (with offset for example), because the rest (end cell: lastRow) is working (correct selection and input, only C2, everytime, if i'm using this script, there is in C2 a x):
spreadsheet.getRange('C2:'+'C'+lastRow).activate();
The first row is fixed. How to use the first visibile not fixed row (second row) for getRange? If the last row is hidden and the script is used, no x is set there, probably because of 'C'+lastRow. This works. Only C2 is affected.
Here is the solution
var s = SpreadsheetApp.getActive().getActiveSheet();
function x() {
var lastRow = s.getLastRow();
for(var row = 2; s.isRowHiddenByFilter(row); ++row);
var range = s.getRange('C'+row+':C'+lastRow);
s.getRange('F1').copyTo(range, SpreadsheetApp.CopyPasteType.PASTE_NORMAL, false);
}
Understanding
You want to put the value using setValue to the showing rows of the filtered column (column "C").
The filter is the basic filter.
Modification points:
In this case, you can retrieve the row numbers of the shown and hidden rows by the filter using isRowHiddenByFilter.
When isRowHiddenByFilter is true, the row is hiding.
When isRowHiddenByFilter is false, the row is showing.
The range list is created from the retrieved row numbers and is used for setValue.
Modified script:
var sheet = SpreadsheetApp.getActiveSheet();
var lastRow = sheet.getLastRow();
var ranges = [];
for (var i = 0; i < lastRow; i++) {
if (!sheet.isRowHiddenByFilter(i + 1)) {
ranges.push("C" + (i + 1));
}
}
sheet.getRangeList(ranges).setValue('x');
When you run the script, the value of x is put to the showing rows of the column "C".
If if (!sheet.isRowHiddenByFilter(i + 1)) { is modified to if (sheet.isRowHiddenByFilter(i + 1)) {, the value of x is put to the hidden rows.
References:
isRowHiddenByFilter(rowPosition)
getRangeList(a1Notations)

How to edit an imported Google sheet using ImportRange?

I used =QUERY(IMPORTRANGE..) to import data from Sheet 1 to Sheet 2 for Column A, B & C. I have to take note in Column D of Sheet 2 for each entry imported. However, for any new one added, the note (for the previous ones) in Column D stays in the same cells. For example, the formula is in A2, so the new data will be added to A2,B2 & C2. The note is in D2. When a new one is imported, the previous one moved to A3, B3 & C3. However, the note is still in D2.
Is there any way to make those notes to move to the next row automatically when a new entry is added?
Here are the files the data has to be imported to and from: https://drive.google.com/drive/folders/1wbOfW9PbSfJbTBv_CwXOTiyyN_LBTiFq?usp=sharing
If my understanding is correct, you want to accomplish the following:
Import data from one spreadsheet to another using IMPORTRANGE.
Add notes manually to a column in your destination spreadsheet.
When a new row is imported to the destination spreadsheet and make previously imported data, the notes should move too.
To achieve that, you would need to keep track of which note belongs to which row of imported data. Both sets of data should be somehow attached. Considering that you have a timestamp in column A, and that this timestamp is probably unique for each row, this timestamp could be used to attach both (if that's not possible, I'd propose adding another column that will be used to identify each row without ambiguity, via some kind of id).
At this point, I would consider using Google Apps Script to accomplish your needs. With this tool, you could develop the functionality that =QUERY(IMPORTRANGE(...)) is providing right now, and you could use other Apps Script tools to reach the desired outcome. Two tools could be specially necessary to accomplish this:
onEdit triggers, to keep track of when the different spreadsheets are edited and make the appropriate changes if that's the case (basically, copying data from one spreadsheet to another).
Properties Service, to store the information about which note is attached to which row of data.
You could do something on the following lines:
Install two edit triggers, (1) one that will fire a function when the source spreadsheet is edited, and (2) another one that will fire when the destination spreadsheet is edited (a simple trigger cannot be used because you have to reference files to which your spreadsheet might not be bound). You can do this manually or programmatically.
Create a function that, for each note that is added to the destination sheet (in this code sample, that's in column D, please change according to your preferences), stores a key-value pair where the key is the value in column A (which should uniquely identify a row of data) and value is the note. This will be used later for the script to know where each note belongs to:
function storeNotes(e) {
var scriptProperties = PropertiesService.getScriptProperties();
var cell = e.range;
var sheet = cell.getSheet();
var rowIndex = cell.getRow();
var column = cell.getColumn();
var noteColumn = 4; // The column where notes are written, change accordingly
// Check whether correct sheet, column and row is edited:
if (column == noteColumn && rowIndex > 1 && sheet.getName() == "Destination") {
var row = sheet.getRange(rowIndex, 1, 1, sheet.getLastColumn()).getValues()[0];
scriptProperties.setProperty(row[0], row[noteColumn - 1]); // Store property to script properties
}
}
Create a function that, every time the source spreadsheet is edited, will delete all content in the destination spreadsheet and copy the data from the source. Then, it will look at the script properties that were store and, using this information, it will write the notes to the appropriate rows (because I see you only want to copy/paste some of the columns, in this sample some of the columns - the ones whose index is in columnsToDelete - are not copied/pasted, you can change this easily to your preferences):
function copyData(e) {
var range = e.range;
var origin = range.getSheet();
var row = range.getRow();
if (origin.getName() == "Origin" && row > 1) { // Check if edited sheet is called "Origin" and edited row is not a header.
var dest = SpreadsheetApp.openById("your-destination-spreadsheet-id").getSheetByName("Destination");
var firstRow = 2;
var firstCol = 1;
var numRows = origin.getLastRow() - 1;
var numCols = origin.getLastColumn();
var values = origin.getRange(firstRow, firstCol, numRows, numCols).getValues();
// Removing some of the columns to get copied/pasted (in this case B and D):
var columnsToDelete = [1, 3];
values = values.map(function(row) {
for (var i = row.length; i > 0; i--) {
for (var j = 0; j < columnsToDelete.length; j++) {
if (i == columnsToDelete[j]) {
row.splice(i, 1);
}
}
}
return row;
})
// Copying content from source to destination:
var firstRowDest = 2;
var firstColDest = 1;
var numRowsDest = values.length;
var numColsDest = values[0].length;
var noteColumn = 4;
var currentValues = dest.getDataRange().getValues();
if (currentValues.length > 1) dest.deleteRows(2, dest.getLastRow() - 1);
var importedRange = dest.getRange(firstRowDest, firstColDest, numRowsDest, numColsDest);
importedRange.setValues(values);
// Writing notes stored in Properties in the appropriate rows:
var properties = PropertiesService.getScriptProperties().getProperties();
for (var i = 0; i < values.length; i++) {
for (var key in properties) {
if (key == values[i][0]) {
dest.getRange(i + 2, noteColumn).setValue(properties[key])
}
}
}
}
}
Notes:
All these functions should be in the same script if you want all both functions to use Properties.
In this sample, the sheet with source data is called Origin and the sheet where it is copied is called Destination (from what I understood, they are in different spreadsheets).
In this simplified example, columns A, B, E from source sheet get copied to columns A, B, C of the destination sheet, and notes are added to column D. Please change this to fit your case by modifying the corresponding indexes.
I hope this is of any help.
Thank you everyone for helping me, especially Lamblichus & user11982798. I recently noticed that importrange will import data to the destination in the same order as that of the source file. Before I sorted the data based on the timestamp in descending order so the new entry was always on the first row. If I changed it to ascending order, the new one is added to the last row, so the note/comment order will not be affected.
Is it possible to update the note/comment in the destination file back to the source one?
If the note is string please try to put in D2 like this:
=ARRAYFORMULA(if(row(A2:A) = max(arrayformula(if(ISBLANK(A2:A),0,row(A2:A)))),"Your Note", ""))
This will automatically place your note to last row of data

Script to copy from one sheet to another, needs edit

I have this script which is working well, but i need to edit it to
a) only return new rows since last run
b) only return certain cells instead of whole row
any guidance would be greatly appreciated
function Copy() {
var sourceSheet = SpreadsheetApp.openById('1WAtRDYhfVXcBKQoUxfTJORXwAqYvVG2Khl4GuJEYSIs')
.getSheetByName('Jobs Log');
var range = sourceSheet.getRange(1, 1, sourceSheet.getLastRow(), sourceSheet.getLastColumn());
var arr = [];
var rangeval = range.getValues()
.forEach(function (r, i, v) {
if (r[1] == 'Amber') arr.push(v[i]);
});
var destinationSheet = SpreadsheetApp.openById('137xdyV8LEh6GAhAwSx4GmRGusnjsHQ0VGlWbsDLXf2c')
.getSheetByName('Sheet1');
destinationSheet.getRange(destinationSheet.getLastRow() + 1, 1, arr.length, arr[0].length)
.setValues(arr);
}
In order to only check new data added after last runtime we have to store .getLastRow() value in properties and retrieve it every runtime. We would also have to work under a few assumptions:
In the input data new values are only appended at the bottom and never inserted between other data
Data is never deleted from the input sheet (if you ignore this, then you must also have an update script for the last row that runs after deleting data)
The sheet is not sorted after new data is added but before this script is run.
So you would need something along the lines of
var sourceSheet = SpreadsheetApp.openById('1WAtRDYhfVXcBKQoUxfTJORXwAqYvVG2Khl4GuJEYSIs')
.getSheetByName('Jobs Log');
var lastRow = sourceSheet.getLastRow();
// note that you need to hav the script property initialized and stored
// or adjust the if to also check if prevLastRow gets a value
var prevLastRow = PropertiesService.getScriptProperties().getProperty('lastRow')
if (lastRow <= prevLastRow) {
return; // we simply stop the execution right here if we don't have more data
}
// then we simply start the range from the previous last row
// and take the amount of rows added afterwards
var range = sourceSheet.getRange(prevLastRow,
1,
lastRow - prevLastRow,
sourceSheet.getLastColumn()
);
As for the second question, inside the forEach you need to simply push an array into arr that will contain only the columns you want. So for example
if (r[1] == 'Amber') arr.push(v[i]);
changes into
if (r[1] == 'Amber') arr.push([v[i][0], v[i][3], v[i][2]]);
which will output A D C columns (in that order) for each row.
Finally, the last thing you need to run before the script ends is
PropertiesService.getScriptProperties().setProperty('lastRow', lastRow)
which will let us know where we stopped the next time we run the script. Again, keep in mind that this works only if new data will always be in new rows. Otherwise, you need to do a different method and retrieve 2 arrays of data. 1 for the entire input sheet and 1 for the output sheet. Then you would have to perform 2 if checks. First one to see if your criteria are met and a second one to see if it already exists in the output data.