Snapshot data and append at a specific location in Google Sheets - google-apps-script

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.

Related

Google sheets script - copy varying data from one sheet to another in same spreadsheet

I wonder if you could please help me solve this. My knowledge of scripts is quite poor, despite attempts to learn.
This is an example of my spreadsheet.
Each month, in the Monthly sheet, I copy data from all the rows below the frozen ones (columns B, C and D only) and make a note in my head of the number of rows copied (the figure shown in cell E2, which can vary). I then go to the Daily sheet, insert the required number of rows at the top of the unfrozen section, and paste in the data values (CTRL Shift V).
I've been trying unsuccessfully to automate this with a script. I've tried piecing bits of code together from other solutions, but the different methods used and my lack of knowledge mean I can't get them to work.
Hoping you can help.
Many thanks.
You can do the following:
Retrieve the desired range from Monthly, using Sheet.getRange.
Taking into account the source range number of rows via Range.getNumRows(), insert the corresponding number of blank rows to Daily (starting at row 4, first unfrozen one) using Sheet.insertRowsBefore.
Copy the retrieved range to Daily!B4 (first unfrozen row) using Range.copyTo.
Code snippet:
function copyRange() {
const ss = SpreadsheetApp.getActive();
const monthlySheet = ss.getSheetByName("Monthly");
const dailySheet = ss.getSheetByName("Daily");
const firstRow = 5;
const sourceRange = monthlySheet.getRange(firstRow,2,monthlySheet.getLastRow()-firstRow+1,3);
dailySheet.insertRowsBefore(4, sourceRange.getNumRows());
const destRange = dailySheet.getRange("B4");
sourceRange.copyTo(destRange,SpreadsheetApp.CopyPasteType.PASTE_VALUES,false);
}
Having tried and failed to do this quite a few times, I've now managed to produce something that seems to work by recording a macro, working out what each line or section in the resulting script does, and editing it, with some additional online research required. It might be a bit clunky, but here's the result of my efforts:
function CommitMonthly() {
// Script is only ever run from the Monthly sheet via the Commit button. Get the number of rows.
var spreadsheet = SpreadsheetApp.getActive();
var NumberOfRows = spreadsheet.getRange('E2').getValue();
// Select the Daily sheet, select top row, insert required number of rows above it.
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('Daily'), true);
spreadsheet.getRange('B4').activate();
spreadsheet.getActiveSheet().insertRowsBefore(spreadsheet.getActiveRange().getRow(), NumberOfRows);
// Copy and paste the data from Monthly to new rows in Daily.
spreadsheet.getRange('Monthly!B5:D').copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
};
Many thanks for your help.

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

Google Sheets: Hiding Columns based on date in row 1

I have no experience with scripting in Excel or Google Sheets, so I'm trying to branch out a bit and see if there's a solution to my problem. We use Google Sheets for a weekly calendar at our kitchen remodeling business. We organize the weeks from left to right and list the jobs we're currently working on in those columns. I would like to automatically hide all columns that have a date older than 4 weeks, so when the sheet opens, we're not starting with a date from a year ago. I can hide these columns manually each week, but when I do need to go back and look at previous weeks, I'm forced to unhide all thosecolumns and then highlight all of the columns I want to rehide. Having a script seems like the better solution.
Is there a way to have a script run every time the file is open so that we're always only displaying the previous 4 weeks and everything in the future? If so, would you be willing to help me understand how I might write that and get it working? Again, I'm a novice when it comes to anything beyond formulas, but very interested in learning more about the scripting capabilities.
Thank you!
With Apps Script via Tools->Script Editor, you could create a menu with an onOpen() function. The function in the menu (e.g. hidePast), would then need to check a given value in each column (to see what date that column refers to), and then flag it to be hidden or not. The onOpen function, because it is a "simple trigger", cannot do anything that requires "authorization" (such as interacting with non-local Spreadsheet data), hence the intermediate method. By creating a menu, you can make it easy for anyone using the spreadsheet to authorize and activate the function.
Example:
/* #OnlyCurrentDoc */
function onOpen() {
SpreadsheetApp.getActive().addMenu("Date Tools",
[{name:"Hide Past", functionName:"hidePast"},
{name:"Show All", functionName:"showAll"}]);
}
function showAll() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
sheet.unhideColumn(sheet.getDataRange());
ss.toast("All columns unhidden.");
}
function hidePast() {
var ss = SpreadsheetApp.getActive();
var sheet = ss.getActiveSheet();
// Acquire the 1st row of all used columns as an array of arrays.
var datelist = sheet.getSheetValues(1, 1, 1, sheet.getLastColumn());
// Drop the hours, minutes, seconds, etc. from today.
var now = new Date();
var today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
// Inspect the datelist and compare to today. Start from the rightmost
// column (assuming the dates are chronologically increasing).
var col = datelist[0].length;
while(--col >= 0) {
var then = new Date(datelist[0][col]);
if(then < today) {
break;
}
}
// Bounds check, and convert col into a 1-base index (instead of 0-base).
if(++col < 1) return;
// col now is the first index where the date is before today.
// Increment again, as these are 2-column merged regions (and
// the value is stored in the leftmost range). If not incremented,
// (i.e. hiding only part of a merged range), spreadsheet errors will occur.
sheet.hideColumn(sheet.getRange(1, 1, 1, ++col));
ss.toast("Hid all the columns before today.");
}
Because you don't have a "database like" source it will be very difficult, but you can try to create a very complicated
QUERY()
you should filter the dates in another sheet (and you may face a dead end).
So I will suggest using this kind of structure and it will also allow you to make other kinds of filters (or Pivot Tables) in the future (maintainable and scalable).

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 :)

Simple Google Spreadsheet Script to loop copy and paste cells

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);
}