Simple Google Spreadsheet Script to loop copy and paste cells - google-apps-script

So this is most likely the simplest question but I am just getting to learn this myself. Sorry for the silly question.
I have a huge Google Spreadsheet (2700+ rows) I need to loop and automate the move of data from one cell (dataA) to another (in front of the next row of data starting with dataB), then remove the 3 rows above the data row and move to the next record (block of 4 rows). Before I do anything I have to make room by inserting a column.
I have created a function that inserts the column, copies the first data cell (now B2) to its new destination (A4) and then deletes the useless three rows above the destination correctly. Because this is relative and iterative, I do not know how to adapt this part with a loop that will go through the rest of the entire sheet.
How do I create a loop that moves through each block of 4 rows, performs the actions mentioned, then moves on to the next block of 4 rows leaving each completed row one after another at the top?
This is an example of the starting data structure:
starting data structure - one record of many
function moveValuesOnly() {
var ss = SpreadsheetApp.getActiveSpreadsheet ();
var sheet = ss.getSheets()[0];
// This inserts a column in the first column position
sheet.insertColumnBefore(1);
var start = ss.getRange("B2");
var source = ss.getRange("B2");
source.copyTo (ss.getRange ("A4"), {contentsOnly: true});
source.clear ();
sheet.deleteRows(1, 3);
}

To create a loop you first need to know where you should start, when you stop and how fast you progress.
You start on the first row, go until the last one which you can get using getLastRow() and you progress 4 rows in one step.
I suggest that you adapt moveValuesOnly() to accept an input rowstart and use the loop to call it several times but with different row values. Note that you will have to change you currently hardcoded input of "B2" and "A4" to something based on the rowstart value. The getDataRange().getValues(); might be useful for you to edit your sheet.
The loop would look like
for (i=0; i<sheet.getLastRow(); i+=4) {
moveValuesOnly(i);
}

Related

Snapshot data and append at a specific location in Google Sheets

I have a Google Sheet I use to track data. I have a sheet that pulls data from multiple sheets in a single row. The row has the current date for Column B and then pulls in data for columns C through AC. I am trying to create a mechanism to snapshot that data and put it on the next line below it. I want the ability to continue doing this and keep pushing the data down and dropping the current on the next line. This allows me to select data in column A to use for graphing purposes. This is what I was using:
function recordHistory() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("History");
var source = sheet.getRange("B2:AC2");
var values = source.getValues();
var now = new Date();
values[0][0] = now;
for (var col in values) {
sheet.getRange(sheet.getLastRow(),2,1,28).setValues(values[col]);
}
I used a combination of examples and I think I got my wires crossed with the translation from one to the other. Looking for help to clean this up or point me to a better option. I was originally using appendRow, but that limits me to using the first column. I want the ability to have the snapshot placed in the 2nd column and the corresponding columns after it. Hopefully, that makes sense.
In this sheet, you can see I am pulling data from the first 2 sheets into the last sheet. I am skipping the first column and using Row 2 as the exact values. The script above is supposed to take what is in Row 2, snapshot it as values only, and move the data to Row 3, moving the previous rows down. This provides me a history of the values. I will be using the triggers to run this function every night at midnight, so the data will be a daily capture of the values. Hopefully, this makes it a bit more clear.
EDIT 2: Let me try and simplify the explanation. I have a sheet that has data in cells B2 through AC2. I want to grab that data and copy it to cells B3 through AC3, moving the data down a row. So on the sheet, you should see cells B3:AC3 having yesterdays data. B4:AC4 has the day before. B5:AC5 has the day before that. Basically keeping a log of the data that is captured in B2:AC2 each day.
Is it clearer what I am trying to accomplish or should I explain it further? I really want to get this script corrected so I can schedule it to run over the weekend.
After a few hours of playing with syntax a bit and realizing where my mistake was, I noticed some issues with the way I was capturing the data and trying to apply it to a range. Here is the solution to my problem:
function recordHistory() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("History");
var source = sheet.getRange("B2:AC2");
var values = source.getValues();
sheet.insertRowBefore(3);
sheet.getRange(3, 2, 1, 28).setValues([values[0]]);
};
As you can see in the solution, I realized how the data was being stored in the array and matched it to the setValues part of the script. It is a pretty basic issue I was having, but the use case was difficult to explain. The insertRowBefore was also a vital piece to establish the structure of the sheet.

How to get actual position of range, when new row(s) above was added

consider this situation.
var sheet = SpreadsheetApp.getActiveSheet();
var range = sheet.getRange("2:2");
var values = range.getValues();
values[0][0] = "updated value"; //updating retrieved data
sheet.insertRowAfter(1); //here the problem appears
range.setValues(values); //trying to save updated row data - FAIL!
Updated row values will be written in a row number 2 (as per range selector "2:2") not in new position "3:3".
Result: It will overwrite data at row 2 instead of updating previously selected/loaded row, which is now at position "3:3" and not anymore at "2:2".
Im looking for a way how to keep "address" of a range even when the range was shifted because of added/removed rows/cells somewhere else in the sheet.
In this dummy example I can track range changes. But in parallel processing/updating sheet i can't simply track all changes from many different places.
So far I came up with a solution add new rows only at the end of SS (that doesn't change range of any rows above, but I would like to have ability add new rows on top of sheet without influencing already selected ranges.
Deleting rows is also dangerous situation - it changes position of ranges also.
LockService can't really solve the situation, because I have plenty scripts working on the same sheet (not using centralized library because of speed performance in addon).
Metadata for cell or row seems too complicated to handle for such an easy task.
From my point of view Object Range should keep its position even when its moved/shifted somewhere else. Otherwise I can't see reason to have Range as an Object - if it keeps only fixed information about from where it was picked.
Any advices are welcomed. Thank you in advance..
EDIT:
Just to add a context. I'm using Google sheets as a database for orders (10 thousands so far) - each row means one order (customer) and not all orders are in one sheet - different products have different sheets (+- 10 products/sheets)
There is an suggestion using named ranges to solve this issue - So what will happen if a spreadsheet will have ten of thousands named ranges - can that work without serious performance issues? Im thinking about to creating named range for each order row, so I can easily pick up right row by orderId and not to be afraid of moving a row when new order arrive during processing another one
You want to achieve the following flow.
Retrieve the value from "2:2" (row 2).
Modify the retrieved value.
Insert a row just below of the row 1.
By this, the initial row 2 is moved to the row 3.
Put the retrieved value to the row 3 which is the initial row 2.
You want to achieve this using Google Apps Script.
If my understanding is correct, how about this answer? Please think of this as just one of several answers.
Issue and solution:
In your script, range of var range = sheet.getRange("2:2"); is the constant. By this, even when the row is inserted, this value is not changed. This is the reason of your issue.
I think that the idea using the named range will be resolved for your situation. But your comment says as follows.
I just found, that namedRange is not capable actualize its own address, if something had changed in the sheet after initialize of a range.
When the row 2 is installed as the named range, when a row is inserted to just below of the row 1, the range of the named range is also changed. But from your comment, I thought that the named range might have not been reloaded.
In order to confirm this, I would like to propose a sample script.
Sample script:
Before you run the script, please install a named range to row 2 as the name of sampleNamedRange. Then, run this script.
var ss = SpreadsheetApp.getActiveSpreadsheet(); // Added
var sheet = ss.getActiveSheet(); // Modified
// Retrieve the range from the namedRange.
// namedRange of "sampleNamedRange" is installed for the range of "2:2" of the active sheet.
var range = ss.getRangeByName("sampleNamedRange"); // Modified
var values = range.getValues();
values[0][0] = "updated value"; //updating retrieved data
sheet.insertRowAfter(1);
// Retrieve the range from the updated namedRange.
var range = ss.getRangeByName("sampleNamedRange"); // Added
range.setValues(values);
Result:
Note:
The point is to retrieve the range from the updated named range. By this, the updated range can be used. So in above script, the updated value is put to the row 3.
References:
getRangeByName()
Class NamedRange
If I misunderstood your question and this was not the result you want, I apologize

insertRowBefore the last Row returning error

I have a script that (gross, I know) writes to a spreadsheet. It dynamically adds and deletes rows based on input data from another sheet. I like doing this because the user has to copy and paste the resulting data, formatted into a table, into an email after (Let's skip creating an email directly from this script for now, that's part two).
I figured the easiest way to let the script know where to paste new data is to always insert a row before the very last row of the spreadsheet and paste said data.
I have been using getMaxRows(), getLastRow(), and insertRowBefore(), but all combos of these functions returns an "those rows are out of bounds" error!
I really don't understand what's going on. I use logs and see the variables getLastRow() and getMaxRows both get updated after I've done some copying of a template table to the sheet which then "inserts" rows. For some reason, even after re-calling the getMaxRows() and getLastRow() functions, they don't recognize the new rows. The script can still manipulate these new rows.
This leads me to believe getMaxRows() and getLastRow() can only get info about the number of rows in the sheet only when the script first runs, or only when the sheet is first activated? Has anyone faced this issue before? Does anyone have details about how those two functions work? The GAS 'sheet' class page didn't help.
spread = SpreadsheetApp.getActiveSpreadsheet();
emailsSheet = spread.getSheetByName("emails");
emailsMaxRows = emailsSheet.getMaxRows(); //# of rows regardless of content.
emailsSheet.deleteRows(2,emailsMaxRows-1); //I want to start from scratch each time I run the script, having just 1 row from which I append 'template' boxes where I can put data for the user to copy and paste into emails.
emailsMaxRows = emailsSheet.getMaxRows(); //now that I've deleted, Logger.log() shows that getMaxRows() correctly updates with just 1 row.
emailsSheet.insertRowsBefore(emailsMaxRows, 2);//insert two more, so there are 3 rows in the sheet.
emailsMaxRows = emailsSheet.getMaxRows(); //logger correctly shows 3
templateEmail = spread.getRangeByName("templateEmail"); //email template, A1:E5
templateEmail.copyTo(emailsSheet.getRange(emailsMaxRows,1)); //paste a table of 5 rows to row 3, so there are 7 total.
emailsMaxRows = emailsSheet.getMaxRows(); //should be 7, no?
//NO. Logger shows MaxRow is still 3, how come????? I can visibly see in the sheet there are 7 rows, with formatting and text. I simply don't understand how getMaxRows() doesn't update to return 7.
I need either getMaxRows() or getLastRow() to correctly update each time I delete or insert a row, why can't they do that for me?
SpreadsheetApp.flush() fixed the problem.

copy spreadsheet rows from one sheet to another conditional on name of sheet

Please help this non-expert -- his job depends upon it.
I have a Google Form that feeds answers into a sheet on a Google Spreadsheet -- let's call it the "main sheet." One of the form's questions asks for a job number. I have been able to figure out how to take each unique job number respondents have entered and create a sheet by that name in the same spreadsheet. I even have copied the headers for the form's answers at the top of each of the new sheets.
What I cannot get to work is rifling through all of the answers in the main sheet, sans the column headers, and copy the rows to the sheet names based on a conditional match with the job number value equaling the sheet name value. So if someone applies for job number 65, that response gets copied from the main sheet to sheet with the name "65."
For starters, I may have the loops set up incorrectly, trying to exclude the header in the main sheet and creating the array of all of the sheet names.
But a second problem I have is that I need to use the variables for both the sheet name value and the job number value. I need to be able to account for an ever increasing job numbers. Staff do not want to have to create a new sheet with every new job number -- they want that done automatically when a user fills in a new job number.
I am happy to share my work, as it is, with anyone who can help point me in the right direction.
+++
Solved, so I yanked down the link to the spreadsheet. In spite of myself, it appears, this community was able to help me out. Thanks.
This function that I wrote will help you find your job number:
function searchColumn(value, rangeValues) {
for (var i = 1; i < rangeValues.length; ++ i) {
if (rangeValues[i] == value) return i;
}
return -1;
}
Give it an array of values (usually by Range.getValues()), and the value you want to search and it will return the row number where that value was found. If it didn't find it, it returns -1.
example:
// Gets all data in the first column of the sheet
var valuestoSearch = mySheet.getRange(1, 1, sheet.getLastRow()).getValues();
// then
var rowNum = searchColumn(job_number, valuesToSearch);
Now that you have the row number, you can use:
var rowData = mySheet.getRange(rowNum, 1, 1, mySheet.getLastColumn()).getValues();
This will give you all the values in that row up to the last column in the sheet.
Once you have that data, you can copy it to another sheet by using:
var jobSheet = mySpreadsheet.getSheetByName(job_number);
jobSheet.getRange(jobSheet.getLastRow() + 1, 1, 1, rowData.length).setValues(rowData);
Or something very similar to this. Then you may want to erase the data in the old spreadsheet so that you don't keep repeating the same operation. But that's up to you how you want to handle that. Take a good look at the documentation for SpreadsheetApp, as it has everything you're looking for and more :)

Can I specify a header row when sorting in a script?

I have a spreadsheet that I'd like to sort by multiple columns many times as I work on the document. This is a multi-step process using the GUI, and seemed far too time-consuming. I have to check the box for a header row, set up the first sorting parameter, and then add another one. Phew! It gets old quick.
Sorting the sheet was no problem, but the header row was also sorted! I don't find any way to specify that I have a header row as a parameter to the sort() function, nor do I see any way to globally define a header row that will be automatically noticed by sort().
In lieu of that, I saved the header row, deleted the first row, sorted the sheet, inserted a new row at the very top, re-created the header row, and formatted the header row. Wow! It didn't take that long to write and it works well, but it's awfully complicated!
From the user perspective, the header row briefly disappears before popping back up. It's not too bad, but it's plainly obvious something is going on.
So finally I get around to my question: Have I overlooked the ability to acknowledge the presence of a header row when I'm sorting? Or does sorting a sheet with a header row present in a script also sort the header row, with no recourse?
If my method, or one like it, is required to avoid including the header row in the sort process, is there somewhere that I can notify the Google Apps team so they can consider adding this feature? Since specifying the existence of a header row exists in the GUI with a simple checkbox, I hope it's present or can be added when sorting within a script.
EDIT
My original code (with documentation/comments removed):
sheet.deleteRow(1);
rows.sort([<1st col>, <2nd col>]);
sheet.insertRowsBefore(1, 1);
for (var j = 0; j <= (numCols - 1); j++) {
sheet.getRange(1, (j + 1)).setValue(firstRow[j]);
}
sheet.getRange(1, 1, 1, numCols).setFontWeight("bold");
sheet.getRange(1, 1, 2, numCols).setBorder(true, true, true, true, true, true);
What I've tried in place of the above code thanks for Thomas' suggestion:
sheet.setFrozenRows(1);
sheet.sort(<2nd col>);
sheet.sort(<1st col>);
sheet.setFrozenRows(0);
Unfortunately, this just sorts by column, not by row. Replacing the 2 sheet.sort(); calls with the rows.short(); line in the first code block doesn't work. Trying this results in the initial problem I reported in my comment where the header row is sorted along with the other data, even though the first row is frozen before sorting.
Also, unless another line of code is added below this I get a perpetual "Working..." notification at the top of the spreadsheet. It doesn't seem to affect anything, however.
In all cases, var sheet = SpreadsheetApp.getActiveSheet();, var rows = sheet.getDataRange();, and firstRow is an array of the header row data.
In all cases, var sheet = SpreadsheetApp.getActiveSheet();, var rows =
sheet.getDataRange();, and firstRow is an array of the header row
data.
Rather than using getDataRange(), you can just get the range you want to sort (that is, exclude the header row):
var sheet = SpreadsheetApp.getActiveSheet();
var rows = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
This next part is just an answer/observation to a "secondary" question:
After the script is finished, the header row was sorted along with all
the other rows although the first row is frozen.
I think this is a bit of a limitation in GAS where some of these "spreadsheet tool" methods don't play nice with one another - this issue is not the same as yours but I think it's (kind of) related.
So with the "frozen header" workaround, I'd be interested to see if SpreadsheetApp.flush(); inserted just after sheet.setFrozenRows(1); makes it work.
You want to do this:
Create a spreadsheet
Put in some random Data
Freeze the first row, http://support.google.com/docs/bin/answer.py?hl=en&answer=54813
and then call this script:
function myFunction() {
SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().sort(1);
}
And there you go :)
With kind regards,
Thomas van Latum