Replace entire sheet with another in Google Apps Scripts - google-apps-script

What I'd like to do is warehouse information from a particular sheet within a spreadsheet and copy it to a second spreadsheet at the end of every day. The second spreadsheet will run complex pivots and reports against the copied information that don't need to run throughout the day.
I can set up a time-driven trigger which will run the job every day within an hour block.
I'm working on the following script which uses SpreadsheetApp.getActiveSpreadsheet to get the current Spreadsheet. Then gets the individual sheet to backup with spreadsheet.getSheetByName. And then uses the sheet.copyTo method to add the current sheet to a new spreadsheet. I'm getting the new spreadsheet by looking up the id with SpreadsheetApp.openById all like this:
function startBackupJob() {
var currentSpreadSheet = SpreadsheetApp.getActiveSpreadsheet()
var masterSheet = currentSpreadSheet.getSheetByName("Sheet1")
var backupSpreadSheetId = "#######################################";
var backupSpreadSheet = SpreadsheetApp.openById(backupSpreadSheetId);
// var backupSheet = backupSpreadSheet.getSheetByName("Sheet1");
// backupSpreadSheet.deleteSheet(backupSheet);
masterSheet.copyTo(backupSpreadSheet).setName("Sheet1");
}
The issue I'm having is that copyTo will create a new worksheet rather than overwrite the existing spreadsheet. The point of moving to the new workbook is to run pivot tables off the data and not re-wire them to point to a new sheet.
I can delete the previous sheet to make room for the new one, but this kills the references on the PivotTable as well, so it doesn't help much.
Is there an easy way to transfer the entire contents of one worksheet to another?
This is similar to (but different from) the following questions:
How do I script making a backup copy of a spreadsheet to an archive folder? - However, I don't want to move the whole file, but a specific sheet within the spreadsheet.
How can copy specifics sheet to another spreadsheet using google script & copy one spreadsheet to another spreadsheet with formatting - However copying produces a new sheet, whereas I need to replace the contents of an existing sheet
Scripts, copy cell from one sheet to another sheet EVERYDAY at a specific time - However, I do want to replace the entire sheet, rather than just specific cells within the sheet.
Update
I might be able to do this by calling getRange on each sheet and then using getValues and setValues like this:
var currentValues = masterSheet.getRange(1, 1, 50, 50).getValues()
backupSheet.getRange(1, 1, 50, 50).setValues(currentValues)
But I'm worried about edge cases where the master sheet has a different available range than the backup sheet. I also don't want to hardcode in the range, but for it to encompass the entire sheet. If I call .getRange("A:E") then the two worksheets have to have the exact same number of rows which is not likely.

Your update has you about 90% of the way there. The trick is to explicitly check the size of the destination sheet before you copy data into it. For example, if I did something like this:
var cromulentDocument = SpreadsheetApp.getActiveSpreadsheet();
var masterSheet = cromulentDocument.getSheetByName('master');
var logSheet = cromulentDocument.getSheetByName('log');
var hugeData = masterSheet.getDataRange().getValues();
var rowsInHugeData = hugeData.length;
var colsInHugeData = hugeData[0].length;
/* cross fingers */
logSheet.getRange(1, 1, rowsInHugeData, colsInHugeData).setValues(hugeData);
...then my success would totally depend upon whether logSheet was at least as big as masterSheet. That's obvious, but what's less so is that if logSheet is bigger then there will be some old junk left over around the edges. Ungood.
Let's try something else. As before, we'll grab the master data, but we'll also resize logSheet. If we don't care about logSheet being too big we could probably just clear() the data in it, but let's be tidy.
var cromulentDocument = SpreadsheetApp.getActiveSpreadsheet();
var masterSheet = cromulentDocument.getSheetByName('master');
var logSheet = cromulentDocument.getSheetByName('log');
var hugeData = masterSheet.getDataRange().getValues();
var rowsInHugeData = hugeData.length;
var colsInHugeData = hugeData[0].length;
/* no finger crossing necessary */
var rowsInLogSheet = logSheet.getMaxRows();
var colsInLogSheet = logSheet.getMaxColumns();
/* adjust logSheet length, but only if we need to... */
if (rowsInLogSheet < rowsInHugeData) {
logSheet.insertRowsAfter(rowsInLogSheet, rowsInHugeData - rowsInLogSheet);
} else if (rowsInLogSheet > rowsInHugeData) {
logSheet.deleteRows(rowsInHugeData, rowsInLogSheet - rowsInHugeData);
}
/* likewise, adjust width */
if (colsInLogSheet < colsInHugeData) {
logSheet.insertColumnsAfter(colsInLogSheet, colsInHugeData - colsInLogSheet);
} else if (colsInLogSheet > colsInHugeData) {
logSheet.deleteColumns(colsInHugeData, colsInLogSheet - colsInHugeData);
}
/* barring typos, insert data with confidence */
logSheet.getRange(1, 1, rowsInHugeData, colsInHugeData).setValues(hugeData);
What's going on here is pretty straightforward. We figure out how big the log needs to be, and then adjust the destination sheet's size to match that data.

Related

Is there a getter for an entire worksheet or range including formulas, values, merges, formatting etc. in Apps Script?

I'm trying to make a backup of an entire worksheet, including formulas, values, formatting, row and column size, cell merges, etc. so that when a user is finished editing I can reset the sheet. Currently I'm using a Range.getFormulas() to create a stringified object (that I can then paste into my code as a constant) to reset all of the content of the cells, but if the user changes the row size or deletes a cell, I'd like to be able to quickly rebuild the entire sheet without iterating through rows and columns (Apps Script is too slow for that). My previous method was to create a duplicate of the sheet and simply hide it, but someone can still unhide and edit that.
I've been digging through the documentation, but I haven't found anything useful. To sum up, I'd like to have something like this:
function resetHandler() {
var destinationWorksheet = SpreadsheetApp.getActiveSpreadsheet().getRange("A1:H132"),
backupWorksheet = [...object...];
backupWorksheet.copyTo(destinationWorksheet);
}
where "[...object...]" is the output of a getter that contains the entire sheet as an object. I tried JSON.stringify(SpreadsheetApp.getActive().getSpreadSheetByName("Workorder").getRange("A1:H132")) but it just outputs "{}" since the Range class is all private.
If there isn't a way, I can always fall back on a hidden backup sheet.
Description
I've created an example where a sheet has formulas, conditional format and data validation. Using testCopyTo I copy Sheet1 to a Backup spreadsheet. All attributes are copied. Using testCopyFrom first copy from the Backup spreadsheet then copy the range within the spreadsheet and finally delete the copy of the backup sheet.
Before restore
After restore
Script
function testCopyTo() {
try {
let source = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet1");
let dest = SpreadsheetApp.openById("xxxx.......");
source.copyTo(dest);
}
catch(err) {
console.log(err);
}
}
function testCopyFrom() {
try {
let dest = SpreadsheetApp.getActiveSpreadsheet();
let source = SpreadsheetApp.openById("xxxx.......").getSheetByName("Copy of Sheet1");
source.copyTo(dest);
source = dest.getSheetByName("Copy of Copy of Sheet1");
dest = dest.getSheetByName("Sheet1");
source.getDataRange().copyTo(dest.getRange(dest.getLastRow()+1,1));
SpreadsheetApp.getActiveSpreadsheet().deleteSheet(source);
}
catch(err) {
console.log(err);
}
}
There is no Apps Script method to retrieve a spreadsheet by its name, you need to retrieve it by id or url
Keep in ind that in Google Drive, multiple spreadsheets with the same name can exist within a folder, this is why it would not make any sense to try and retrieve a spreadsheet by its name
Instead, you need to use the method openById(id) or openByUrl(url) (less recommended)
To get a sheet from an external spreadsheet and copy it to the current one, you can do the following:
function resetHandler() {
var destinationWorksheet = SpreadsheetApp.getActiveSpreadsheet();
backupWorksheet = SpreadsheetApp.openById("123456789").getSheetByName("The Name of the Sheet");
backupWorksheet.copyTo(destinationWorksheet);
}
where 123456789 is the id of the backup spreadsheet which you can find among others in the url in the address bar, which looks something like https://docs.google.com/spreadsheets/d/1234456789/edit#gid=1392862199
To copying a range from a sheet from an external spreadsheet into a sheet in the curent spreadsheet with copyTo is not possible, you can only use getValues() and setValues respectively, however this will not copy the formatting. Also for the formulas, you need to perform additoinally getFormulas() and setFormulas(formulas).
UPDATE
If you would like to copy a range from one sheet into another sheet of the same spreadsheet, you can modify your code as following:
function resetHandler() {
var activeSpreadsheet = SpreadsheetApp.getActive();
var destinationSheet = activeSpreadsheet.getSpreadSheetByName("Workorder");
var destinationRange = destinationSheet.getRange("A1:H132");
var backupWorksheet = activeSpreadsheet.getSpreadSheetByName("Backup").getRange("A1:H132");
var backupRange = backupWorksheet.getRange("A1:H132");
backupRange.copyTo(destinationRange);
}
The main problem with your code was that you mixed up the methods copyTo(destination) for ranges with copyTo(spreadsheet).
Both methods have the same name, but the first copies a range into another range (into the same sheet or another sheet of the same spreadsheet; overwriting the previous contents), while the other one inserts a whole (additoinal) sheet from a spreadsheet into the same or different spreadsheet
Have a careful look at the sample in the documentation for appplying the correct syntax in each case
Also: The method getSpreadSheetByName() does not exist, it is getSheetByName()
Careful with the wording: Google talks about sheets and spreadsheets, which are the equivalents of Excel worksheets and workbooks respectively.

Google sheets appscript to copy tabs to new sheets

I have a google sheet with around 190 tabs on that i need to split into 190 different files
The files need to be named the same as the tab, the contents of the tab need to be copied as values but i also need to bring the formatting accross (just not the formulas).
I have looked around, and through a combination of previous questions and answers plus using the function list help have formed the following code. It actually works for the first few tabs but then throws up an error about being unable to delete the only sheet.
function copySheetsToSS() {
var ss = SpreadsheetApp.getActive();
for(var n in ss.getSheets()){
var sheet = ss.getSheets()[n];// look at every sheet in spreadsheet
var name = sheet.getName();//get name
if(name != 'master' && name != 'test'){ // exclude some names
var alreadyExist = DriveApp.getFilesByName(name);// check if already there
while(alreadyExist.hasNext()){
alreadyExist.next().setTrashed(true);// delete all files with this name
}
var copy = SpreadsheetApp.create(name);// create the copy
sheet.copyTo(copy);
copy.deleteSheet(copy.getSheets()[0]);// remove original "Sheet1"
copy.getSheets()[0].setName(name);// rename first sheet to same name as SS
var target_sheet = copy.getSheetByName(name);
var source_range = sheet.getRange("A1:M50");
var target_range = target_sheet.getRange("A1:M50");
var values = source_range.getValues();
target_range.setValues(values);
}
}
}
I am hoping someone can tell me what i have done wrong as I cannot figure it out at this point. I am also open to better solutions though please be aware I am very much a beginner on google appscript, nothing too complex please.
thankyou
In principle your script correctly adds a new sheet to the new spreadsheet before removing the preexisting one
However, mind that calls to service such as SpreadsheetApp are asynchronous.
And this becomes the more noticeable, the longer your script runs.
In your case it apparently leads to behavior that the only sheet is being deleted before the new sheet is being created.
To avoid this, you can force the execution to be synchronous by implementing calls to SpreadsheetApp.flush().
This will ensure that the old sheet won't be deleted before the new one gets inserted.
Sample:
copy.deleteSheet(copy.getSheets()[0]);// remove original "Sheet1"
SpreadsheetApp.flush();
copy.getSheets()[0].setName(name);
You might want to introduce call toflush()` also at other positions where it is important for the code to run synchronously.

Sync the first column(s) and row(s) of sheets in the same Google Sheets file

I need to sync all sheets in the same Google Sheets file.
I only want to sync the first row and the first column (which are frozen), while the rest of the contents should not be synced.
I'd also like to make it so that if I insert/remove a row to/from a sheet, the same action is performed to all other synced sheets too. Also, if other modifications are done, e.g. a row is frozen, the same should be done on all sheets.
I have seen the codes in these questions:
How to sync two sheets with =importrange() in two googlespreadsheet?
Is there a way to keep two sheets synchronized?
In short, I want to be able to use a "master" sheet form, and have several sheets with different values in them. I a change to the master is applied, changes to the sheets should reflect the changes in the master. Two way sync would be nice but is not a stringent requirement.
So far I have managed to put down this:
var masterSheetN = 2; /*2 means the master sheet is the second sheet. Sheets before the master sheet are ignored*/
function importData() {
/*Input sheet*/
var fromSpreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var fromSheets = fromSpreadsheet.getSheets();
var fromWorksheet = fromSheets[masterSheetN-1];
var fromData = fromWorksheet.getDataRange();
var fromRowsN = fromData.getNumRows()
var fromColsN = fromData.getNumColumns()
var fromRow1 = fromWorksheet.getRange(1, 1, 1, fromColsN);
var fromCol1 = fromWorksheet.getRange(1, 1, fromRowsN);
for (i=masterSheetN; i<fromSheets.length; i++) {
/*Output sheet*/
var toWorksheet = fromSheets[i];
var toRow1 = toWorksheet.getRange(1, 1, 1, fromColsN);
var toCol1 = toWorksheet.getRange(1, 1, fromRowsN);
/*Sync row and col 1*/
toRow1.setValues(fromRow1.getValues());
toCol1.setValues(fromCol1.getValues());
/*Sync format for the whole sheet*/
var toGID = toRow1.getGridId()
fromData.copyFormatToRange(toGID, 1, fromColsN, 1, fromRowsN)
}
}
This successfully syncs all sheets with the master sheet for the first row and column, and also for formatting. However, it is quite slow, especially for syncing the formatting (why? syncing the first row/column seems much faster), and it does not sync things like frozen/protected rows, etc. Is there a way to sync everything from the Master sheet when a new sheet is created?
Partial answer
Instead of getValues(), setValues() and copyFormatToRange() use copyTo(Range).
Notes
I think that it only will copy values and format. Perhaps you should also use getDataValidation() / setDataValidation(rule) to sync the data validation rules.
See also
Script To Copy Format No Longer Working

Copying range of data from one spreadsheet to another deletes data from destination

The end goal is to successfully copy a range of cells from one spreadsheet to another every 10-15 minutes. Those cells get populated by an external script via the Drive API.
The issue is when the script is ran, it just deletes all the data in the destination range, which makes no sense to me!!
function ExportHourlyCitizenData() {
var ssSource = SpreadsheetApp.openById("1kDupxE8csxYibYz-cfzkhzehwzTQGjFXOlCU3bJNUIg");
var sheet = ssSource.getSheetByName("Hourly Citizen Capture");
var ssDest = SpreadsheetApp.openById("1tMAP0fg-AKScI3S3VjrDW3OaLO4zgBA1RSYoQOQoNSI");
var sheetDest = ssDest.getSheetByName("Hourly Citizen Capture");
// Grabs the source range, some of the grabbed cells are empty..
var rangeSource = ssSource.getRange("B70:Z168");
var dataSource = rangeSource.getValues();
// sets the destination cells.
sheetDest.getRange("B85:Z183").setValues(dataSource);
};
So the idea was pretty simple, but I messed up something in here somehow. I am moving a decently sized set of data, so maybe there is a more efficient way as well?
EDIT: I tried using importrange but the issue with that is that it did not update when I pushed data into the spreadsheet via the drive api which was a significant problem.
This may be the issue:
var rangeSource = ssSource.getRange("B70:Z168");
Use source data sheet variable sheet instead of the spreadsheet variable ssSource.

On Change Trigger Not Running Script On Change

I have a script to update named ranges when new rows of data are added to the spreadsheet in question:
function updateNamedRanges() {
// get to the right place
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('ga weekly data pull');
//now update the named ranges if they have changed in length
var openEnded = ["gaCampaign", "gaMedium", "gaSource", "gaSubscriptions", "gaUsers", "gaWeek"];
for(i in openEnded) {
var r = ss.getRangeByName(openEnded[i]);
var rlr = r.getLastRow();
var s = r.getSheet();
var slr = s.getMaxRows();
if(rlr==slr ) continue; // ok as is-skip to next name
var rfr = r.getRow();
var rfc = r.getColumn();
var rnc = r.getNumColumns();
var rnr = slr - rfr + 1;
ss.removeNamedRange(openEnded[i]);
ss.setNamedRange( openEnded[i], s.getRange(rfr, rfc, rnr, rnc ));
}
sheet.getRange("D2").setValue(0); // this gets all the formulas in the sheet to update - just changing any cell
}
Then, within Aps Script editor I go Resources > Current Projects Triggers > Run updateNamedRanges > From Spreadsheet > On change.
Now, if I manually add in a row of data the script runs - great!
But I'm pulling in data with the Google Analytics add on. This add on expands the tab in question when the length of data is longer than the sheet. But when this happens the script does not update.
Is there anything I can do here?
As a backup I'm thinking if I can figure out how to get GAS to add a row from the bottom of the sheet that might do it but that seems like a workaround. Before I go down that path is there a better way?
as you found out, apps script triggers only work when apps script does the changes. yea its lame. if an api outside of apps script modifies the sheet, they wont trigger.
your only option is to use a time trigger to detect a change and process the entire sheet again (since you dont know what changed). One way to achieve this more efficiently is to remember (in a script property) the last modified date from triggers. then a 1minute time trigger checks if modified date is now bigger than the last one saved. if so process the entire sheet.
Run it on a time trigger that runs every minute until Google addresses the issues of not catching the on change event and/or not being able to define open-ended named ranges.
Edited for running the script on open
To keep the sheet from recalculating everytime it is opened whether needed or not.
above the loop place:
var recalc = false;
within the loop below if(rlr==slr ) continue;
recalc = true;
recalculate the sheet only if necessary:
if(recalc) {sheet.getRange("D2").setValue(0)};