Script to permute columns, rows or any ranges - google-apps-script

EDIT: I changed the code to include possibility of providing ranges by name (in A1 notation) as this could be potentially more efficient than providing Range object (if the range ends up not moved) and for sure is easier to use in simple cases. Idea by AdamL (see answers bellow).
In some spreadsheets I need to permute rows or columns. Requiring user to do this manually isn't very nice. So making proper commands in menu which would run script seemed a reasonable solution.
Oddly I wasn't able to find any function (either build in or wrote by someone else) which would permute rows/columns. So I wrote one myself and then considered publishing it. But since my experience with JavaScript and Google Apps Script is low I wanted to have someone else check on this function. Also I have some questions. So here we go.
// Parameters:
// - ranges An Array with ranges which contents are to be permuted.
// All the ranges must have the same size. They do not have to be
// vectors (rows or columns) and can be of any size. They may come from
// different sheets.
// Every element of the array must be either a Range object or a string
// naming the range in A1 notation (with or without sheet name).
// - permutation An Array with 0-based indexes determining desired permutation
// of the ranges. i-th element of this array says to which range
// should the contents of i-th range be moved.
// - temp A range of the same size as the ranges in "ranges". It is used to
// temporarily store some ranges while permuting them. Thus the initial
// contents of this range will be overwritten and its contents on exit is
// unspecified. Yet if there is nothing to be moved ("ranges" has less
// than 2 elements or all ranges are already on their proper places) this
// range will not be used at all.
// It is advised to make this range hidden so the "garbage" doesn't
// bother user.
// This can be either a Range object or a string naming the range in A1
// notation (with or without sheet name) - just as with the "ranges".
// - sheet An optional Sheet object used to resolve range names without sheet
// name. If none is provided active sheet is used. Note however that it
// may cause issues if user changes the active sheet while the script is
// running. Thus if you specify ranges by name without sheet names you
// should provide this argument.
//
// Return Value:
// None.
//
// This function aims at minimizing moves of the ranges. It does at most n+m
// moves where n is the number of permuted ranges while m is the number of
// cycles within the permutation. For n > 0 m is at least 1 and at most n. Yet
// trivial 1-element cycles are handled without any moving (as there is nothing
// to be moved) so m is at most floor(n/2).
//
// For example to shift columns A, B and C by 1 in a cycle (with a temp in
// column D) do following:
//
// permuteRanges(
// ["A1:A", "B1:B", "C1:C"],
// [1, 2, 0],
// "D1:D",
// SpreadsheetApp.getActiveSheet()
// );
function permuteRanges(ranges, permutation, temp, sheet) {
// indexes[i] says which range (index of ranges element) should be moved to
// i-th position.
var indexes = new Array(permutation.length);
for(var i = 0; i < permutation.length; ++i)
indexes[permutation[i]] = i;
// Generating the above array is linear in time and requires creation of a
// separate array.
// Yet this allows us to save on moving ranges by moving most of them to their
// final location with only one operation. (We need only one additional move
// to a temporary location per each non-trivial cycle.)
// Range extraction infrastructure.
// This is used to store reference sheet once it will be needed (if it will be
// needed). The reference sheet is used to resolve ranges provided by string
// rather than by Range object.
var realSheet;
// This is used to store Range objects extracted from "ranges" on
// corresponding indexes. It is also used to store Range object corresponding
// to "temp" (on string index named "temp").
var realRanges;
// Auxiliary function which for given index obtains a Range object
// corresponding to ranges[index] (or to temp if index is "temp").
// This allows us to be more flexible with what can be provided as a range. So
// we accept both direct Range objects and strings which are interpreted as
// range names in A1 notation (for the Sheet.getRange function).
function getRealRange(index) {
// If realRanges wasn't yet created (this must be the first call to this
// function then) create it.
if(!realRanges) {
realRanges = new Array(ranges.length);
}
// If we haven't yet obtained the Range do it now.
if(!realRanges[index]) {
var range;
// Obtain provided range depending on whether index is "temp" or an index.
var providedRange;
if(index === "temp") {
providedRange = temp;
} else {
providedRange = ranges[index];
}
// If corresponding "ranges" element is a string we have to obtain the
// range from a Sheet...
if(typeof providedRange === "string") {
// ...so we have to first get the Sheet itself...
if(!realSheet) {
// ...if none was provided by the caller get currently active one. Yet
// note that we do this only once.
if(!sheet) {
realSheet = SpreadsheetApp.getActiveSheet();
} else {
realSheet = sheet;
}
}
range = realSheet.getRange(providedRange);
} else {
// But if the corresponding "ranges" element is not a string then assume
// it is a Range object and use it directly.
range = providedRange;
}
// Store the Range for future use. Each range is used twice (first as a
// source and then as a target) except the temp range which is used twice
// per cycle.
realRanges[index] = range;
}
// We already have the expected Range so just return it.
return realRanges[index];
}
// Now finally move the ranges.
for(var i = 0; i < ranges.length; ++i) {
// If the range is already on its place (because it was from the start or we
// already moved it in some previous cycle) then don't do anything.
// Checking this should save us a lot trouble since after all we are moving
// ranges in a spreadsheet, not just swapping integers.
if(indexes[i] == i) {
continue;
}
// Now we will deal with (non-trivial) cycle of which the first element is
// i-th. We will move the i-th range to temp. Then we will move the range
// which must go on the (now empty) i-th position. And iterate the process
// until we reach end of the cycle by getting to position on which the i-th
// range (now in temp) should be moved.
// Each time we move a range we mark it in indexes (by writing n on n-th
// index) so that if the outer for loop reaches that index it will not do
// anything more with it.
getRealRange(i).moveTo(getRealRange("temp"));
var j = i;
while(indexes[j] != i) {
getRealRange(indexes[j]).moveTo(getRealRange(j));
// Swap index[j] and j itself.
var old = indexes[j];
indexes[j] = j;
j = old;
}
getRealRange("temp").moveTo(getRealRange(j));
// No need to swap since j will not be used anymore. Just write to indexes.
indexes[j] = j;
}
}
The questions are:
Is this properly implemented? Can it be improved?
How about parameters validation? Should I do it? What should I do if they are invalid?
I wasn't sure whether to use copyTo or moveTo. I decided on moveTo as it seemed to me more what I intended to do. But now in second thoughts I think that maybe copyTo would be more efficient.
Also I noticed that the Range moved from not always is cleared. Especially when in Debugger.
Undo/redo seems to be an issue with this function. It seems that every moveTo is a separate operation (or even worse, but maybe that was just a low responsiveness of the Google Docs when I was testing) on the spreadsheet and undoing the permutation is not a single action. Can anything be done about it?
The documentation I wrote for the function claims that it works across different sheets or even different spreadsheets. I haven't actually checked that ;) but Google Apps Script documentation doesn't seem to deny it. Will it work that way?
I'm not sure whether this is a proper place to ask such questions (since this is not truly a question) but since Google Apps Script community support is moving to Stack Overflow I didn't knew where else to ask.

Don't you think it might be more efficient in terms of execution speed to do it with arrays ?
try this for example : (I added logs everywhere to show what happens)
(Note also that sheets are limited to 255 columns... take care of the list length)
function permutation() {
var sh = SpreadsheetApp.getActiveSheet();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var lr = ss.getLastRow()
var lc=ss.getLastColumn();
var data = sh.getRange(1,1,lr,lc).getValues()
Logger.log(data)
var temp2= new Array();
var h=data.length
Logger.log(h)
var w=data[0].length
Logger.log(w)
for(nn=0;nn<w;++nn){
var temp1= new Array();
for (tt=0;tt<h;++tt){
temp1.push(data[tt][nn])
}
temp2.push(temp1)
}
Logger.log(temp2)
Logger.log(temp2.length)
Logger.log(temp2[0].length)
sh.getRange(1,1,lr,lc).clear()
sh.getRange(1,1,lc,lr).setValues(temp2)
}
best regards,
Serge

Adam, from my limited experience on the Apps Script GPF, I have learned that it is best to limit get and set calls as much as possible (and you could include moveTo/copyTo in that as well).
Do you think it would be better to pass the range names, rather than the ranges, as parameters (and to that end, you might need a mechanism to pass sheet names and spreadsheet keys as well, to support your requirement of working across different sheets/spreadsheets), and then trivial "getRange's" can be avoided as well as a trivial "moveTo's".
Also, if you are just transferring values only, it would probably be better to not move it to a temporary range but rather assign those arrays to a variable in the script which can then be later "set" in the correct spot. But if you need to copy over formats or formulae, that's a different story.

Related

How to delete Named Ranges with Tab names via Apps Script

I'm trying to clean a spreadsheet of a slew of Named Ranges I no longer need, and leave behind the few I'm still using. All of these Named Ranges include the Tab name, because they originate on a template Tab (named tmp), from which other Tabs are duplicated. Even after I delete all the spinoff Tabs from the sheet and leave behind only tmp, the 'tmp'! appears in the names of the Ranges, both as displayed in the Named Ranges sidebar and as they come in on getNamedRanges().
When I try to selectively delete obviated Named Ranges, no matter how I spec the name of the Ranges, I get errors saying no such Named Ranges exist. Basically, I'm feeding back the same information getNamedRanges() and getRange().getSheet().getSheetName() give me, only to have it garbled along the way.
The problem is isolated in the following test snippet, and involves rendering the single quotes around the Tab name. I have tried several approaches, including escaping the single quotes with slashes, and have added to the code the Comments of the errors I got on the line targetDoc.removeNamedRange(namedRange).
const analyzerDoc = '1pYgcX2dxzHd4cCofy0RFZTzEl36QesiakMGIqCC2QlY'
const openAnalyzerDoc = SpreadsheetApp.openById(analyzerDoc)
function testDeleteNamedRange (){
var docUrl = openAnalyzerDoc.getRangeByName('docUrl').getValue();
var targetDoc = SpreadsheetApp.openByUrl(docUrl);
// var namedRange = 'dyCl_MoodEntries' // The named range "dyCl_MoodEntries" does not exist.
// var namedRange = 'tmp!dyCl_MoodEntries' // The named range "tmp!dyCl_MoodEntries" does not exist.
// var namedRange = "'tmp'!dyCl_MoodEntries" // The named range "'tmp'!dyCl_MoodEntries" does not exist.
// var namedRange = "\'tmp\'!dyCl_MoodEntries" // The named range "'tmp'!dyCl_MoodEntries" does not exist.
targetDoc.removeNamedRange(namedRange);
}
This bug is in the way of a longer function, which is working fine but for the part isolated in this test function.
The longer function gets the names and Tabs of Ranges to delete from this sheet:
What is the right way to do this? Thank you!
This function will remove all of the named range that have their sheet name within the range name.
function deleteAllNamedRange() {
const ss = SpreadsheetApp.getActive();
ss.getNamedRanges().filter(r => ~r.getName().indexOf(r.getRange().getSheet().getName())).forEach(r => r.remove());
}
I have an answer to my own question. There is probably more than one solution, but I have chosen to sidestep the challenge I am facing, and instead of specifying the Named Ranges by name, I am going to spec them by their position in the document's Named Ranges, and simply use remove() instead of removeNamedRange(namedRange). I had gotten so caught up in the recommended method involving forEach, that I had forgotten that the outcome of getNamedRanges() is not an object, but an array.
The solution then lies in amending my process of collecting the Names and other information from the result of getNamedRanges(). Instead of using forEach, I loop over the results of getNamedRanges(), and while I get the information I desire concerning each Named Range, I also log the loop iteration and thereby get the Index Number of each Named Range.
I proceed as before, pasting this information in a Tab where I can select which Ranges to delete.
My delete function then loops over the Named Ranges directly, in reverse order, and checks the loop iteration against the Ranges I have ticked off in that analysis Tab.
I have tested this in a sample document; you may view it here.
In this demo, all functions are within the same document, so I'm using getActive() instead of openByUrl.
This document has 3 Tabs named Sheet1, Sheet2 and Sheet3. Each Tab has 3 Named Ranges named Moe, Larry and Curly. There is also a Tab NamedRanges which the following function collects Named Range into:
function getnamedRanges() {
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
var namedRangeData = [];
for (i=0; i<namedRanges.length; i++) {
var namedRange = namedRanges[i];
var nrName = namedRange.getName();
var nrRange = namedRange.getRange().getA1Notation();
namedRangeData.push([nrName,nrRange,i])
}
SpreadsheetApp.getActive().getSheetByName('NamedRanges').getRange(2,1,namedRangeData.length,3).setValues(namedRangeData)
}
Here's the Named Range Tab after running that function, and choosing 3 Named Ranges to delete:
Next, here is the function that removes the selected Named Ranges:
function deleteSelectedNamedRanges () {
var namedRangeData = SpreadsheetApp.getActive().getSheetByName('NamedRanges').getDataRange().getValues();
namedRangeData.shift(); // Remove header row data.
var rangesToDelete = namedRangeData.filter(function(nrDatum) {if (nrDatum[3]==true) return nrDatum});
// [3] equivalates to Column D, the checkboxes where I select which Named Ranges to delete.
console.log (rangesToDelete.map(value => value[0])); // [ 'Sheet3\'!Moe', 'Sheet2\'!Curly', 'Sheet1\'!Moe' ]
console.log (rangesToDelete.map(value => value[2])); // [ 0, 1, 5 ] // [2] is the index number of the Named Ranges.
/* The order here derives from how values in Tab Named Ranges happen to be sorted.
In this instance, I have not changed that order, so the Named Ranges To Delete are in ascending order.
For one thing, this is the opposite of what we want;
for another, I want to be able to sort the Named Range Tab freely before making my selections.
So, we must sort this data in DESCENDING order. */
rangesToDelete.sort(function(value1,value2){if (value1[2]<value2[2]) return 1; if (value1[2]>value2[2]) return -1; return 0});
console.log (rangesToDelete.map(value => value[0])); // [ 'Sheet1\'!Moe', 'Sheet2\'!Curly', 'Sheet3\'!Moe' ]
var rangesToDeleteIndexNumbers = rangesToDelete.map(value => value[2])
console.log (rangesToDeleteIndexNumbers); // [ 5, 1, 0 ]
var namedRanges = SpreadsheetApp.getActive().getNamedRanges();
for (i=namedRanges.length-1; i>=0; i--) {
/* We must loop in descending order because deleting Named Ranges will change the index numbers
of all Named Ranges that come after each we delete. */
if (rangesToDeleteIndexNumbers.indexOf(i) !== -1) {namedRanges[i].remove(); console.log ('Removed NR # '+i)}
// Delete Named Range if this iteration number can be found in rangesToDeleteIndexNumbers.
}
}
After running this function, you can see that the 3 Names Ranges have been removed, leaving 6 behind:

Changing info on a different sheet in the same spreadsheet

I have two ranges of equal size on different sheets in the same spreadsheet. I am trying to find a row (based off of user input) in the first sheet and then use that index to modify a table in the second sheet that counts how many times that certain index has been used before (to make a nice looking pie chart).
This code runs but will not produce results on the second sheet. I've gone through the debugging process and my best guess is that for some reason, my for in loop is not running through. Attached is my code that takes in the beforementioned index and attempts to perform the second half of my goal.
function acceptToEncounterChart(ghostrow) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
SpreadsheetApp.setActiveSheet(ss.getSheets()[1]);
ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Average Encounter Chart");
var range = sheet.getRange("B3:B14")
for(var i in range) {
if(ghostrow == i) {
var before = range[i][0].getValue()
range[i][0].setValue(before + 1);
}
}
SpreadsheetApp.setActiveSheet(ss.getSheets()[0]);
};
Explanation:
I am not entirely sure what is your goal.
However, here is some fixes / improvements starting from the beginning:
You define 2 times the same variable ss with exactly the same value.
You don't need to set the active sheet, if your goal is to just get the sheet, therefore this line is redundant:
SpreadsheetApp.setActiveSheet(ss.getSheets()[1]);
Variable range is not an array but a range object. You can't index it and therefore you can't also use a for loop to iterate over a single object. For the same exact reason, the code inside the if statement is wrong, you can't index range. But you don't see any errors because the if statement evaluates to false.
In JavaScript and in many other programming languages, array indexes start from 0. Since your range starts from cell B3 or row 3, you need to use i+3 to match the data with the range.
For the same reason as the previous point, ghostrow is an index, not a row. The if statement compares an array index i with ghostrow, so ghostrow should not be confused with the actual sheet row. For example, if you choose ghostrow=5 then the current script will increment the value of the cell B8 (remember i+3) by 1.
Solution:
Here is a workable code snippet:
function acceptToEncounterChart(ghostrow) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("Average Encounter Chart");
var data = sheet.getRange("B3:B14").getValues().flat();
data.forEach((v,i)=>{
if(ghostrow == i){
sheet.getRange(i+3,2).setValue(v+1)
}
});
ss.setActiveSheet(ss.getSheets()[0]);
}
Related:
Please explore the official google apps script documentation.

How slow can the google sheets api be? [duplicate]

This question already has answers here:
How much faster are arrays than accessing Google Sheets cells within Google Scripts?
(2 answers)
Closed 3 years ago.
In my script I iterate through cells each of which represents a day on The calendar grid. So it goes from an earlier date (from a cell changed by the user) to a later one. Top down and left to right.
The script starts when a user changed value of a cell. And it must fill every subsequent days-cells by the same value up to certain cell. Lets say it must stop on the cell with red font. Thus every iteration the script must get the cell font color.
...Or, The iterations must stop when the script gets a cell representing a certain date. Thus every iteration the script must verify which date the cell represets. To understand what date a cell represents I get The values from the helper cells (headers) and use getValue(). Whatever.
Everything is bearable: looping through cells, changing the values in each cell, getting helper cells(ranges). But! As soon as I add getValue() to the given headers it starts to work unbelievably slowly. Or even I just get font color... Any function starting from "get" included in iteration makes the job unbelievably slowly!
A script with looping as many as you like getRange(), setValue() works out in tolerable time, but with just one getFontColor() or getValue this job runs in the same time for just one cell.
Either I do somethin illegal or google ?
Is there an opportunity to accelerate this job significantly?
Or job like this should be done quite differently?
function onEdit(evt) {
var aSheet = evt.source.getActiveSheet();
// veryfy which sheet
switch( aSheet.getName().toLowerCase() ) {
case "wage":
// get range - calendar grid
var wageGrid = aSheet.getParent().getRangeByName("wageGrid");
var editedCell = evt.range;
// loop exit flag
var weBreak = false;
editedCell.setFontColor("red");
// loop through rows
for(var rowIndex = editedCell.getRow(); rowIndex <= wageGrid.getLastRow(); rowIndex++) { if(weBreak) break;
// loop through columns
for(var collIndex = (rowIndex == editedCell.getRow())?editedCell.getColumn():wageGrid.getColumn(); collIndex <= wageGrid.getLastColumn(); collIndex++) {
// as many as you like
var currentLoopCell = aSheet.getRange(rowIndex, collIndex);
var dayHeaderCell = aSheet.getRange(rowIndex, 1);
var monthHeaderCell = aSheet.getRange(1, collIndex);
cell.setValue(evt.value);
// but getValue() or getSomeAttribute() will slow down the process
//var cellFontColor = cell.getFontColor();
//if(cellFontColor=="red") weBreak = true; break;
}
}
break;
case "nonexistentyet":
break;
default:
Logger.log("What was it?")
}
}
What you have encountered is normal behaviour. Each call to the sheet such as getValue() and getFontColor() takes a fair amount of time, often 1 to 2 seconds each. You should avoid calling these functions in a loop.
When you want to loop over a large set of cells and work with their values, define the entire range with getRange() and use getValues() and getFontColors() instead. These functions will return the data from an entire range in a two-dimensional array. You can even bring in all the data in the sheet with getDataRange().
Similarly, it is best to write in blocks as well, using setValues() and setFontColors() rather than setting values/colors on cells one by one

I have a pseudocode, but can't code yet

thank you and sorry for my incredibly unexperienced question in advance. So, I want to make a code and I know what I want it to do, I just don't know how to program. What I need is:
function GenPre()
1.- delete range Presupuesto!A12:C42
2.- copy range Imp!A2:Imp!C33 VALUES in Presupuesto!A12:Presupuesto!C42 (Imp cells are formulas, and I want to copy just the values)
3.- show only used rows in column A in Presupuesto!A12:A42 (consider some rows will be already hidden, so unhiding them first would be an idea)
4.- go to sheet Presupuesto (once I do this function, I want to end up on the sheet Presupuesto
end Generar
This function will be runned by a button in another sheet in the same spreadsheet.
and so far, I have this:
function GenPre() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetbyname(Presupuesto);
//next step is to select and delete the content of the range on the sheet
}
I know I'm asking for much, I just can't find much about selecting defined cells... and I really don't know how to program yet.
Thanks a bunch!!
Edit
So, I started tweaking with what k4k4sh1 answered and got this (AND reading other posts on hiding rows containing "x" on a given cell):
function GenPre() {
var sheetp = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Presupuesto') //name a variable to the sheet where we're pasting information
var sheetc = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Imp') //name a variable to the sheet frome where we're copying information
sheetp.getRange('a12:c41').clearContent() //delete all values in the range where we're copying
sheetc.getRange('A2:C31').copyValuesToRange(sheetp,1,3,12,41); //copy from source range to destination range
sheetp.showRows(12,41); //make sure all rows in the destination range are shown
for( i=12 ; i<=41 ; i++) {
if (sheetp.getRange('A'+i).getValue() == '') { // status == ''
sheetp.hideRows(i);
}
}
}
Te script is running how it should, but now, I want it to run faster (takes 12 seconds to run, when it doesn't really look that heavy), and is there a function to switch my view to sheetp? thank you all!
You're asking us to do all the work :)
Let's start from your piece of code:
the method .getSheetByName(shName) accepts a string as argument, so you should change it to
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Presupuesto');.
Mind that JavaScript is case-sensitive, so .getSheetbyname is not the same as .getSheetByName().
According to Sheet Class Reference use sheet.getRange() to get your Range Object. Take a look to Range Class Reference: to clear the range content including formats use .clear(), to clear just the content leaving the formatting intact use .clearContent().
To hide unused rows try:
function hideRows(sheetName, column) {
var s = SpreadsheetApp.getActive().getSheetByName(sheetName);
s.showRows(1, s.getMaxRows());
s.getRange(column)
.getValues()
.forEach(function (r, i) {
if (r[0] == '') {s.hideRows(i + 1);}
});
}
// hideRows('Presupuesto', 'A12:A42');

Google Apps Script to VMerge tables WITH FORMATTING

Does anyone know if there is a Google apps script out there that does what VMerge does but keeps the formatting of the tables being merged together? (in Google Spreadsheets)
VMerge is a script that can be used as a custom formula but a script that I can trigger myself will do just fine too.
Any help would be much appreciated.
VMerge expects arrays-of-values as parameters, and therefore does not know what cells were referenced creating those arrays. When used as a Custom Formula, the sheet parser resolves all range parameters into their values before passing them to VMerge. Additionally, the parameters may be hard-coded or be the result of Queries or other functions that return ranges. Because of this alone, it's not feasible to modify VMerge to copy cell formats to the new merged table.
Complicating things further, Custom Functions cannot modify cells outside of the one they are attached to, they can only return values or arrays of values. From comment in Issue 37:
2) Scripts used as cell functions are not allowed to do complex things
like connect to other APIs or set the values of other cells. Scripts
used as cell functions are only allowed to return a value.
So you're going to have to settle for a function you call from scripts. The following function will join multiple ranges into a new table at a given anchor point. Because I started out trying to make this a custom function callable from a sheet, the parameters are string expressions of ranges, in a1Notation. (It could easily be refactored to deal directly with Range objects.)
The "Anchor" for the new range is expected to be a cell. One or more ranges of any size may be joined - each will be positioned directly below the previous.
Examples:
VJoin("D1","A1:B"); - All of columns A & B duplicated in columns D & E
VJoin("Sheet2!A1","Sheet1!C9:E10","Sheet1!A14:B15"); - Two different ranges in Sheet 1 joined and copied to Sheet 2.
Here's the code:
/*
* Vertically join the ranges from multiple sources into a new table
* starting at the given anchor point. Values and formatting are copied.
*
* #param {a1Notation} anchorA1 Anchor for joined table.
* #param {a1Notation} sources One or more source ranges.
*/
function VJoin(anchorA1,sources) {
var sheet = SpreadsheetApp.getActiveSheet();
var anchor = sheet.getRange(anchorA1);
var anchorSheet = anchor.getSheet(); // in case anchorA1 is not on the "active sheet"
var nextAnchor = anchor;
for (var i in arguments) {
// Arguments are expected to be Strings, containing a1Notation.
if (i == 0) continue; // First argument was anchorA1, skip it.
if (arguments[i].constructor == String) {
var source = sheet.getRange(arguments[i]);
var destination = anchorSheet.getRange(nextAnchor.getRow(), nextAnchor.getColumn(),
source.getNumRows(), source.getNumColumns() );
// Copy all values & formatting to new location.
source.copyTo(destination);
// Prepare for next range by moving our anchor
nextAnchor = sheet.getRange(nextAnchor.getRow() + source.getNumRows(),
nextAnchor.getColumn());
}
else {
throw new Error ("Expected String containing a1Notation.")
}
}
}
If you need a separate script to bring over the formatting...
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName('Sheet1');
s.getRange('A1').copyFormatToRange(sheet, column, columnEnd, row, rowEnd);
}
I find the below built in functions to work well pulling information from different Google Sheet files. I have defined named ranges to define what columns to pull into the Master, and also know I am having an issue with Feb.
=sort(arrayformula({
importrange("1sTS3AUfoXqXYrMYJrro9pGEKwqVL_k854yhniNOHNWc","JCJan");
importrange("1ETSD4J-8AI-7pVK0hXJKaWtG3RlHKpnco88Yj8sqNN8","JCFeb")}),1,True)