Copy and Pasting a Moving Range in Google Sheets - loops

I'm very much still learning the basics of script writing, but I have a rather specific question that I've not been able to find a solution for - perhaps because I might be phrasing it wrong.
Essentially, In sheet MS, I have a range of data (A2:O23) with formulas that I want to stay fixed (as in, always reference the same range in a different sheet).
My aim is, at the run of a script, that this block of formulas will copy and paste from the next empty row (in the same columns [A:O]) and that the previous block (A2:O23) will copy and paste over itself as values.
Then, each time the script runs, it does the same thing, copying the latest block of 22 rows beneath itself and pasting itself as values. Essentially having the effect of building a long list of data with the latest (read lowest) range being the formulas and all above being values.
For example, the results of the first time it runs should be that, A24:O45now show the same formula and that A2:O23 are showing as values only. The next time, A24:O45 would be frozen as values and A46:O67 are now the formulas.
I'm not sure if this requires the use of loops or there is a different way to achieve the effect, but the former is something I've not used and the latter is still a mystery to me.
I realise that this might not be especially clear, but I'm happy to field any questions and greatly appreciate any efforts made to help.
Thanks

If I understand you correctly, you want to:
Copy the last 22 rows of your sheet (columns A to O) to the next 22 rows (formulas included).
Copy/paste to the original range as values only.
If the above is correct, then you can copy and run the following code to the script bound to your spreadsheet (see inline comments for detail on what the code is doing, step by step):
function copyPasteValues() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getSheetByName("MS"); // Name of your sheet (change if that's necessary)
var lastRow = sheet.getLastRow();
var firstCol = 1; // First column to copy
var numCols = 15; // Number of columns to copy
var numRows = 22; // Number of rows to copy
if (lastRow > numRows) { // Check if the sheet has at least 23 rows with content (otherwise it would give error when copying the range)
var originRange = sheet.getRange(lastRow - numRows + 1, firstCol, numRows, numCols);
var destRange = sheet.getRange(lastRow + 1, firstCol, numRows, numCols);
originRange.copyTo(destRange); // Copying with formulas to next 22 rows
originRange.copyTo(originRange, {contentsOnly:true}); // Copying only values to same range
}
}
The main method to notice here is copyTo, which can be used to copy the data as it is, with formulas included, or values only, by setting the parameter {contentsOnly:true} (copyValuesToRange could be used too).
I hope this is of any help.

function copypastepaste() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getActiveSheet();
var rows=sh.getLastRow();
if(rows>22) {
var rg=sh.getRange(sh.getLastRow()-21,1,22,16);
var vA=rg.getDisplayValues();
var fA=rg.getFormulas();
var nrg=sh.getRange(sh.getLastRow()+1,1,22,16);
nrg.setFormulas(fA);
rg.setValues(vA);
}
}

Related

Problem with copying volatile data values to rows of a google sheet

I am trying to convert an old Excel VBA program to Google Sheets for projection 30 years of financial results. The key issue is the VBA Excel sheet haw a row (a1:ad1) of (30) cells that each contain the volatile function Rand() to generate a random number in each cell through each of 1000 loops:
In each loop these cells 1) reset their random number, then 2) some rows of calculations are made based on the new random numbers, then 3) then a resulting row of 30 new values in copy-pasted (values only) to another part of the sheet so the individual results can be statistically analyzed.
The copy-paste in google sheets is too slow and I hit a 6 minute execution time out, so I am trying to accumulate each loop into a single 2D array, each row being one of the 1000 loop results in 3 above,then write the entire array back to the sheet at once. To get the 30 cells to recalc, each time I read a row, I force a new value to be set in a single cell of the spreadsheet at the beginning of each loop. That seems to work, but trying move the data from the row to the accumulation array and then writing the values back to individual rows in the sheet at once, I always wind up with arrays the have too many dimensions some other problems getting the data back to the sheet as a 1000 x 30 range of static data.
Here's the basic outline of where I am so far after many variations of this :
function test1(){
var sheet=SpreadsheetApp.getActive().getSheetByName('UST_Results'),
range;
var values=[];
var LargeArray= Array(new Array()) //new Array()
for (var i=0;i<3;i++) { // using a small number (3) rows to get it work
values =getRandRow(sheet)[0][i];
LargeArray(LargeArray.length) = JSON.stringify(values)
};
};
function getRandRow(sheet) {
var values_array =[];
let myData =[];
var cell = 0;
// force the Rand() to recalc before getting the next copy of A1:Ad1
for (var i=0;i<1;i++) {
var cell = sheet.getRange(i+5,1);
cell.setValue(i);
SpreadsheetApp.flush();
}
range = sheet.getRange('A1:ad1');
values_array = range.getValues();
myData.push(values_array);
return myData;
}
A picture of the sheet testing with just the rand numbers and not the derived row.
enter image description here

Google Script Overwriting Entire Sheet, Not Specific Range

In the function below, I grab data that is in multiple columns via a form response on my second sheet and place the information on my first sheet organized in rows.
I would like to have the first blank column after the data, currently G on my new sheet editable so that someone can come in and "approve" the contents of each row. Right now, when this script runs, it overwrites the contents of Column G. I thought the number 6 in the line with sh0.getRange(2, 1, aMain.length, 6).setValues(aMain); was telling the script to only put data into 6 columns... looks like that's not the case.
I also thought that I may be able to do a workaround by changing that line to sh0.getRange(2, 2 ... it would let me keep the first column as an editable column... that didn't work either.
Any suggestions to allow me to use this script and keep a column editable?
function SPLIT() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh0 = ss.getSheets()[0], sh1 = ss.getSheets()[1];
// get data from sheet 2
var data = sh1.getDataRange().getValues();
// create array to hold data
var aMain = new Array();
// itterate through data and add to array
// i is the loop, j=3 is the column it starts to loop with, j<9 tells it where to stop.
// in the aMain.push line, use data[i][j] for the rows to search and put in the one column.
for(var i=1, dLen=data.length; i<dLen; i++) {
for(var j=5; j<9; j++) {
aMain.push([data[i][0],data[i][1],data[i][2],data[i][3],data[i][4],data[i][j]]);
}
// add array of data to first sheet
// in the last line, change the last number to equal the number of columns in your final sheet.
// the first number in getrange is the row the data starts on... 1 is column.
sh0.getRange(2, 1, aMain.length, 6).setValues(aMain);
}
}

How do I incorporate an array to speed up hiding rows?

I have a script that I want to use so that my manager can quickly see which items in the Spreadsheet need parts ordered. The script quickly and easily hides columns containing information not pertinent to ordering parts, and then hides all rows (out of thousands) where the value in column S is FALSE (doesn't need parts ordered). The hiding columns part is almost instant, but the hiding rows part is EXTREMELY slow. I understand that in order to speed it up, the data should be loaded into an array, then the loop will run on the array in memory instead of making many calls to the spreadsheet. I have seen similar questions, but the answers don't seem to explain exactly how to do this. One example I read suggested that this is already using an array, which confused me even more. Any help pointing me in the right direction would be appreciated. Here is the script I'm using:
function showPartsNeeded() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getRange('S:S').getValues();
for(var i=1; i< data.length; i++){
if(data[i][0] == false){
sheet.hideRows(i);
}
sheet.hideColumns(2,2);
sheet.hideColumns(5,2);
sheet.hideColumns(8,1);
sheet.hideColumns(10,2);
sheet.hideColumns(13,18);
sheet.hideColumns(47,14);
}
}
I have tried the following to load the rows that have the "Needs Parts" column marked as false into an array, then hide only the rows that are present in the array, but I only get a "Cannot find method getRange()." line 15 error and I don't understand why:
function showPartsNeeded() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getRange().getValues();
sheet.hideColumns(2,2);
sheet.hideColumns(5,2);
sheet.hideColumns(8,1);
sheet.hideColumns(10,2);
sheet.hideColumns(13,18);
sheet.hideColumns(47,14);
var temp = [];
for (var i = 1; i< data.length; i++ ) {
if (data[i][19] == false) {
temp.push.data[i];
}
}
if (temp.length > 0 ) {
sheet.hideRows(temp);
}
}
I am fairly sure the sheet.hideRows(temp); line is wrong but I'm still trying to figure out how to use the data in the array with hideRows().
You are looping through thousands of rows and every time you find one row with the condition you call sheet.hideRows(i). Calls to the sheet functions or ranges are very slow and that's why it is recommended to do all the operations in an array and then just insert the changes as one operation in the case of ranges.
In this case you could probably improve your code by grouping consecutive rows that follow the condition and instead of calling sheet.hideRows(i) on each of them, you could call sheet.hideRows(first_row, last_row).
So if you have 10 consecutive rows that have the condition, instead of making 10 calls, you would just make one. eg. sheet.hideRows(20,30);

Loop through table columns and store max or min values in array variable

I am trying to loop through a table in excel using VBA and store the maximum value of each column in an array variable for later use in the program.
I have taken a number of approaches similar to the code snippet below without success. For reference: mainTable is a Public ListObject variable defined earlier in the script and mainCols is a Pubic Long variable, also previously defined, which stores the width of mainTable:
Dim maxVals() As Long
ReDim maxVals(mainCols)
For x = 0 To mainCols - 1
maxVals(x) = mainTable.ListColumns(x + 1).TotalsCalculation = xlTotalsCalculationMax
Next x
The code above executes without error, but always returns 0
It is important that I don't hard code the columns/table location so that users can copy/paste a dataset of varying dimensions and run the script without errors. Also, assume that the user could run this on a dataset with whatever column headers they want.
Your code simply changes the calculation used in the Totals row of the table - it doesn't actually return the max value. You could use:
Dim maxVals() As Long
ReDim maxVals(mainCols)
For x = 0 To mainCols - 1
maxVals(x) = WorksheetFunction.Max(mainTable.ListColumns(x + 1).Range)
Next x

Google Spreadsheets replace cell range with data minus empty cells

I am making a script allows you to consolidate a todo list after finished items are erased.
I am getting a range, using an if statement to push all non-zero cell values from the specified range to an array, clearing the cell range and then re-pasting the new array (minus the cells with no data) back into the same range. Here is my code: (thanks mogsdad!)
function onOpen()
{
var spreadsheet = SpreadsheetApp.getActive();
var menuItems = [
{name: 'Consolidate To-Do list', functionName: 'consolidatetodolist_'}
];
spreadsheet.addMenu('LB functions', menuItems);
};
function consolidatetodolist_()
{
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheets()[0];
var range = sheet.getRange("A2:A19");
var rangeValues = range.getValues();
var todosArray = new Array();
for (var i = 0 ; i < rangeValues.length ; i++ ) {
var row = rangeValues[i];
var cell = row [0];
if (null !== cell && "" !== cell) {
todosArray.push(row);
};
};
Logger.log(todosArray);
range.clearContent();
range.offset(0,0,todosArray.length).setValues(todosArray);
};
Your edit trigger should not be manipulating the menu. You should have an onOpen trigger do that. If you want the consolidation to be automatic, then you could drive THAT from your onEdit.
In your consolidatetodolist_() function, you've got a handful of errors regarding value assignment, Javascript fundamentals.
Using the SpreadsheetApp services to get the value of valueInstance isn't necessary. You've already got it, in rangeValues[i][0]. Note the two indexes; the data stored in rangeValues is a two-dimensional array, even though your range was one column. The first index is rows, the second is columns. (Opposite order from A1Notation.)
if ( range.getLength(valueInstance) !== 0 ) should crash, because range is an instance of Class Range which has no getLength() method. What you intend to check is whether the current cell is blank. Since spreadsheet data can be of three javascript types (String, Number, Date), you need a comparison that can cope with any of them, as well as an empty cell which is null. Try if (null !== cell && "" !== cell)...
When you get to setValues, you will need to supply a two-dimensional array that is the same dimensions as range. First, you need to ensure todosArray is an array of arrays (array of rows, where a row is an array of cells). Easiest way to do that here is to use var row = rangeValues[i]; var cell = row[0];, then later todosArray.push(row);. Since you are clearing the original range first, you could then define a new range relative to it using offset(0,0,todosArray.length).setValues(todosArray).
Other nitpicking:
You've got the idea of using meaningful variable names, good. But instead of valueInstance, cell would make this code clearer.
Use of "magic numbers" should be avoided, because it makes it harder to understand, maintain, and reuse code. Instead of i < 18, for example, you could use i < rangeValues .length. That way, if you modified the size of the range you're consolidating, the for loop would adapt without change.
Declaring var i outside of the for loop. No error with what you've done (although a declaration with no value assigned is bad), and it makes no difference to the machine, but for humans it's clearer to define i in the for loop, unless you need to use it outside of the loop. for (var i = 0; ...

Resources