Google Sheets script help moving data between sheets - google-apps-script

I am loading a list into a google sheet tab SheetA then amending the data cell by cell into the same google sheet in a different tab SheetB with some manipulation as it goes over.
But the only way I know how to do it is activate the sheet each time and its really slow. Is there a way to move the data without having to physically activate the sheet. So for eaxmple grap the value of cell A1 from SheetA without ever deactivating SheetB?
Here is a sample of the section of code where you can see it activating the sheets back and forth. I used to do a similar function in Excel but in Excel I didn't have to activate the sheets and I could turn off the visual during runtime which made the whole transfer quite fast.
Can you do the same in google sheets? If so what is the syntax?
while (SpreadsheetApp.getActiveSheet().getRange('A'+ COUNTERSheetA).getValue() != ""){
VALUEA = SpreadsheetApp.getActiveSheet().getRange('A'+ COUNTERSheetA).getValue()
VALUEB = SpreadsheetApp.getActiveSheet().getRange('B'+ COUNTERA).getValue()
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('SheetA'), true);
spreadsheet.getRange('A'+COUNTERSheetB).activate();
spreadsheet.getCurrentCell().setValue(VALUEA);
spreadsheet.getRange('B'+COUNTERSheetB).activate();
spreadsheet.getCurrentCell().setValue(VALUEB);
COUNTERSheetB = COUNTERSheetB + 1
COUNTERSheetA = COUNTERSheetA + 1
spreadsheet.setActiveSheet(spreadsheet.getSheetByName('SheetA'), true);
}

function myfunc() {
const ss=SpreadsheetApp.getActive();
const shA=ss.getSheetByName('SheetA');//gets SheetA by name
const rgA=shA.getDataRange();//gets all of the data in the sheet
var vA=rgA.getValues();//gets all the data at one time as one two dimensional array of data
vA.forEach(function(r,i){
r.forEach(function(c,j){
vA[i][j]=(WhateverYouWanttoDoToEachCell);
});
});
const shB=ss.getSheetByName('SheetB');//gets SheetB by name
const rgB=shB.getRange(1,1,vA.length,vA[0].length).setValues(vA);//1,1 is A1 but you could choose the upper left corner to be anywhere. Loads all of the data at one time.
//If you're going to be doing something with those spreadsheet values immediately you may wish to employ SpreadsheetApp.flush() to guarantee that all of the values have been loaded into the spreadsheet.
SpreadsheetApp.flush();
}
Array.forEach() method
Spreadsheet Reference

You sure can. You can simply declare each sheet as a variable, then use the sheet class to make the calls. Example below.
Take into account that each time you use the getRange() method it's taxing on the runtime.
A better way to do this would be to take all values at once into a 2D array, then iterate over the array, converting into a new 2D array, then write that 2D array to the other sheet.
function myFunction() {
let COUNTERSheetA = 1;
let COUNTERSheetB = 1;
let VALUEA;
let VALUEB;
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheetA = spreadsheet.getSheetByName('SheetA');
const sheetB = spreadsheet.getSheetByName('SheetB');
while (sheetA.getRange('A'+ COUNTERSheetA).getValue() != ""){
VALUEA = sheetA.getRange('A'+ COUNTERSheetA).getValue()
VALUEB = sheetA.getRange('B'+ COUNTERSheetA).getValue()
sheetB.getRange('A'+COUNTERSheetB).setValue(VALUEA);
sheetB.getRange('B'+COUNTERSheetB).setValue(VALUEB);
COUNTERSheetB = COUNTERSheetB + 1
COUNTERSheetA = COUNTERSheetA + 1
}
}
Here is the actual sheet in case you want to play with it:
https://docs.google.com/spreadsheets/d/1Gev2KxpX5mPik92Io3eOeF3nKngVcKxnWiNktlHxdDI/edit?usp=sharing
Here is an example of pulling all values, iterating over them, then inserting them into the new sheet. If you can figure out the logic you need here, this is by far the fastest method.
function myFunction() {
let VALUES;
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
const sheetA = spreadsheet.getSheetByName('SheetA');
const sheetB = spreadsheet.getSheetByName('SheetB');
VALUES = sheetA.getRange(1, 1,sheetA.getLastRow(), 2).getValues();
for (let row of VALUES){
for (let val of row){
//Do your manipulations here if you can.
Logger.log(val);
}
}
sheetB.getRange(1, 1,sheetA.getLastRow(), 2).setValues(VALUES);
}
Here is the sheet for the above code:
https://docs.google.com/spreadsheets/d/1Gev2KxpX5mPik92Io3eOeF3nKngVcKxnWiNktlHxdDI/edit?usp=sharing

Related

How to get a total across all sheets for cells based on criteria from another cell?

I have a budget spreadsheet with tabs for every pay period. These tabs are created as needed and don't have names I can easily know in advance. For instance, one will be "10/15 - 10/28" because that's the pay period. Next month I create a new one with "10/29 - 11/11." I'd like to be able to sum a value across all sheets. For example, every sheet has a row named "Save," some sheets have a row named "Rent", but not every sheet will contain rows with those names and when they do they won't always be in the same cell number.
Sample sheet
I've seen some examples where there's a bunch of SUMIFs and every sheet is manually named but I'd much rather not have to do that because this sheet gets copied fairly often and the sheet names will never be the same.
=SUMIFS('Tab 1' !A1:A10, 'Tab 1'!B1:B10, "Rent")
+SUMIFS('Tab 2' !A1:A10, 'Tab 2'!B1:B10, "Rent")
+SUMIFS('Tab 3' !A1:A10, 'Tab 3'!B1:B10, "Rent")
Is this possible with either a standard formula or a script?
Sample Data
Desired final tab
Column 1's values are known in advance so those can be hardcoded. For instance, there will never be a random "yet more stuff" appear which I wouldn't sum up by adding a new row to the final tab.
While there's another answer that works for this, I think the use of text finders and getRange, getValue and setFormula in loops is not the best approach, since it greatly increases the amount of calls to the spreadsheet service, slowing down the script (see Minimize calls to other services).
Method 1. onEdit trigger:
An option would be to use an onEdit trigger to do the following whenever a user edits the spreadsheet:
Loop through all sheets (excluding Totals).
For each sheet, loop through all data.
For each row, check if the category has been found previously.
If it has not been found, add it (and the corresponding amount) to an array storing the totals (called items in the function below).
If it has been found, add the current amount to the previous total.
Write the resulting data to Totals.
It could be something like this (check inline comments for more details):
const TOTAL_SHEET_NAME = "Totals";
const FIRST_ROW = 4;
function onEdit(e) {
const ss = e.source;
const targetSheet = ss.getSheetByName(TOTAL_SHEET_NAME);
const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
let items = [["Category", "Amount"]];
sourceSheets.forEach(sheet => { // Loop through all source sheets
const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
values.forEach(row => { // Loop through data in a sheet
const [category, amount] = row;
const item = items.find(item => item[0] === category); // Find category
if (!item) { // If category doesn't exist, create it
items.push([category, amount]);
} else { // If category exists, update the amount
item[1] += amount;
}
});
});
targetSheet.getRange(FIRST_ROW-1, 1, items.length, items[0].length).setValues(items);
}
Method 2. Custom function:
Another option would be to use an Apps Script Custom Function.
In this case, writing the data via setValues is not necessary, returning the results would be enough:
const TOTAL_SHEET_NAME = "Totals";
const FIRST_ROW = 4;
function CALCULATE_TOTALS() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
let items = [["Category", "Amount"]];
sourceSheets.forEach(sheet => { // Loop through all source sheets
const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
values.forEach(row => { // Loop through data in a sheet
const [category, amount] = row;
const item = items.find(item => item[0] === category); // Find category
if (!item) { // If category doesn't exist, create it
items.push([category, amount]);
} else { // If category exists, update the amount
item[1] += amount;
}
});
});
return items;
}
Once the script is saved, you can use this function the same you would use any sheets built-in function:
The problem with this approach is that the formula won't recalculate automatically when changing any of the source data. In order to do that, see the above method.
Method 3. onSelectionChange trigger:
From your comment:
I'd love to be able to trigger it when the totals sheet is opened but that doesn't appear to be possible
You can do this by using an onSelectionChange trigger in combination with PropertiesService.
The idea would be that, every time a user changes cell selection, the function should check whether current sheet is Totals and whether the previously active sheet is not Totals. If that's the case, this means the user just opened the Totals sheet, and the results should update.
It could be something like this:
function onSelectionChange(e) {
const range = e.range;
const sheet = range.getSheet();
const sheetName = sheet.getName();
const previousSheetName = PropertiesService.getUserProperties().getProperty("PREVIOUS_SHEET");
if (sheetName === TOTAL_SHEET_NAME && previousSheetName !== TOTAL_SHEET_NAME) {
updateTotals(e);
}
PropertiesService.getUserProperties().setProperty("PREVIOUS_SHEET", sheetName);
}
function updateTotals(e) {
const ss = e.source;
const targetSheet = ss.getSheetByName(TOTAL_SHEET_NAME);
const sourceSheets = ss.getSheets().filter(sheet => sheet.getName() !== TOTAL_SHEET_NAME);
let items = [["Category", "Amount"]];
sourceSheets.forEach(sheet => { // Loop through all source sheets
const values = sheet.getRange(FIRST_ROW, 1, sheet.getLastRow()-FIRST_ROW+1, 2).getValues();
values.forEach(row => { // Loop through data in a sheet
const [category, amount] = row;
const item = items.find(item => item[0] === category); // Find category
if (!item) { // If category doesn't exist, create it
items.push([category, amount]);
} else { // If category exists, update the amount
item[1] += amount;
}
});
});
targetSheet.getRange(FIRST_ROW-1, 1, items.length, items[0].length).setValues(items);
}
Note: Please notice that, in order for this trigger to work, you need to refresh the spreadsheet once the trigger is added and every time the spreadsheet is opened (ref).
Reference:
onEdit(e)
Custom Functions in Google Sheets
onSelectionChange(e)
I wrote 2 scripts:
budgetTotal which takes a budgetCategory parameter, for example "Rent", and loops through all the sheets in the file to sum up the amounts listed on each sheet for that category.
budgetCreation which looks at your Totals sheet and writes these budgetTotal formulas in for each category you have listed.
I ran into a challenge which was, as I added new sheets the formulas wouldn't be aware and update the totals. So, what I did was create a simple button that executes the budgetCreation script. This way, as you add new payroll weeks you just need to press the button and - voila! - the totals update.
There might be a better way to do this using onEdit or onChange triggers but this felt like a decent starting place.
Here's a copy of the sheet with the button in place.
const ws=SpreadsheetApp.getActiveSpreadsheet()
const ss=ws.getActiveSheet()
const totals=ws.getSheetByName("Totals")
function budgetCreation(){
var budgetStart = totals.createTextFinder("Category").findNext()
var budgetStartRow = budgetStart.getRow()+1
var budgetEndRow = ss.getRange(budgetStart.getA1Notation()).getDataRegion().getLastRow()
var budgetCategoies = budgetEndRow - budgetStartRow + 1
ss.getRange(budgetStartRow,2,budgetCategoies,1).clear()
for (i=0; i<budgetCategoies; i++){
var budCat = ss.getRange(budgetStartRow+i,1).getValue()
var budFormula = `=budgetTotal(\"${budCat}\")`
ss.getRange(budgetStartRow+i,2).setFormula(budFormula)
}
}
function budgetTotal(budgetCategory) {
var sheets = ws.getSheets()
var total = 0
for (i=0; i<sheets.length; i++){
if (sheets[i].getName() != totals.getName()){
var totalFinder = sheets[i].createTextFinder(budgetCategory).findNext()
if (totalFinder == null){
total = 0
} else {
var totalValueFinder = sheets[i].getRange(totalFinder.getRow(),totalFinder.getColumn()+1).getValue()
total += totalValueFinder
}
}
}
return total
}

Values from Ranges by named ranges to variables

I have many named ranges in Spreadsheet.
I need get values from areas by current row (e) and current column by namedranges and write it to variables. I write a lot of the same script
function sendEmailClerck(e){
var sheet = SpreadsheetApp.getActive().getSheetByName('Ответы на форму (1)');
var idRow = e.range.getRow();
var name = sheet.getRange(idRow, SpreadsheetApp.getActiveSpreadsheet().getRangeByName('ReqUser').getColumn()).getValue();
var cli = sheet.getRange(idRow, SpreadsheetApp.getActiveSpreadsheet().getRangeByName('ReqCLI').getColumn()).getValue();
var priority = sheet.getRange(idRow, SpreadsheetApp.getActiveSpreadsheet().getRangeByName('ReqPriority').getColumn()).getValue();
var date = sheet.getRange(idRow, SpreadsheetApp.getActiveSpreadsheet().getRangeByName('ReqDateStart').getColumn()).getValue();
var date2 = sheet.getRange(idRow, SpreadsheetApp.getActiveSpreadsheet().getRangeByName('ReqDateCloseU').getColumn()).getValue();
var tip = sheet.getRange(idRow, SpreadsheetApp.getActiveSpreadsheet().getRangeByName('ReqType').getColumn()).getValue();
var users = sheet.getRange(idRow, SpreadsheetApp.getActiveSpreadsheet().getRangeByName('ReqAddress').getColumn()).getValue();
I found one script in this site but I can't understand how can I use it here
function getRangeName(A1Notation) {
var rangeName = "";
//get all the name ranges of the sheet
var namedRanges = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().getNamedRanges();
// loop on all the names range of the sheet
for(i=0;i<namedRanges.length;i++){
//if it's the same A1 Notation
if(namedRanges[i].getRange().getA1Notation() == A1Notation)
rangeName = namedRanges[i].getName();
}
//will return the name of the range or "" if there is no range
return rangeName;
}
I think that's it I need. I think I can write to variables the values by put the value in parameter and send it to one function and return it back. Can you help me this this?
Thank you for your help!
Try Array.map() to make this a bit more manageable, like this:
function sendEmailClerck(e) {
const sheet = SpreadsheetApp.getActive().getSheetByName('Ответы на форму (1)');
const rangeNames = [
'ReqUser',
'ReqCLI',
'ReqPriority',
'ReqDateStart',
'ReqDateCloseU',
'ReqType',
'ReqAddress',
];
const [name, cli, priority, date, date2, tip, users] = rangeNames.map(rangeName => {
const column = sheet.getRange(rangeName).getColumn();
return sheet.getRange(e.range.rowStart, column).getValue();
});
}
You may want to rethink the whole approach though. The code is very inefficient because it reads every cell one by one. You should probably use just one sheet.getDataRange().getValues() call and parse the data from the the 2d array it returns.
Some of the best resources for learning Google Apps Script include the Beginner's Guide, the New Apps Script Editor guide, the Fundamentals of Apps Script with Google Sheets codelab, the Extending Google Sheets page, javascript.info, Mozilla Developer Network and Apps Script at Stack Overflow.

Google App script sheets - How to get the highlighted range and write data to it

I would like to write data that is generated from my own function to the highlighted cell of the user. It should be able to take single cell and multiple cell highlights.
if(isNaN(text))
ui.alert('Input is not a number');
else{
var sheet = SpreadsheetApp.getActiveSheet();
var activeRangeList = sheet.getActiveRangeList();
if(activeRangeList==null)
ui.alert('No range selected');
else{
var rgMyRange = sheet.getRange("B1:B10");
rgMyRange.setValue(myFunc(NumberOfRowsHighlighted,parseInt(text)))
}
}
I would like for the user to highlight a range, when the scripts run i would need to get the number of rows highligted along with the input number to be passed to myfunc. The function always generates an array the same size as the number of highlighted rows. Finally i would like to be able to write back to the highlighted cell using the array return by the ffunction.
sample return of myfunct
[1,2,3,...,100]
I am having a hard time reading the documentation since google doesnt really define what is a Range or a RangeList and an example of the data returned. So if anybody can help me it would be greatly appreciated.
I have solved it, using different objects and functions, it is somewhat similar to the first one
var sheet = SpreadsheetApp.getActiveSheet();
var selection = sheet.getSelection();
if(selection.getActiveRange().getA1Notation()==null)
ui.alert('No range selected');
else{
var range = SpreadsheetApp.getActiveSpreadsheet().getRange(selection.getActiveRange().getA1Notation());
range.setValues(GENERATEGROUP(10,parseInt(text)));
}
I'm not sure what the function GENERATEGROUP(10,parseInt(text)) outputs but here's a solution that writes to Row and Column numbers each cell in the active range (i.e. the users selection)
//es6-V8
function writeActiveRange() {
const ss=SpreadsheetApp.getActive();
const sh=ss.getActiveSheet();
const rg=sh.getActiveRange();
let v=rg.getValues();
const row=rg.getRow();
const col=rg.getColumn();
v.forEach(function(r,i){r.forEach(function(c,j){v[i][j]=`Row:${row+i},Col:${col+j}`;});});
rg.setValues(v);
}

How to make google sheets index from values in column and hyperlink those to sheets

I have a bunch of data I want to put in to multiple sheets, and to do it manually would take time and I would like to learn scripting too.
So say I have a sheet with the states in one column.
I would like to have a script make new sheets based off the values of that column, and make a hyperlink to those sheets, and sort the sheets alphabetically.
In each sheet, I need to have the A1 cell the same name as the sheet.
Here is an example of states
Any suggestions would be helpful
Edit:
This is code that can make sheets based on the values of the columns.
function makeTabs() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var last = sheet.getLastRow();//identifies the last active row on the sheet
//loop through the code until each row creates a tab.
for(var i=0; i<last; i++){
var tabName = sheet.getRange(i+2,1).getValue();//get the range in column A and get the value.
var create = ss.insertSheet(tabName);//create a new sheet with the value
}
}
(note the "sheet.getRange(i+2,1" assumes a header, so pulls from the first column, starting on the second row)
I still need to:
Add a hyper link in the index sheet to the State's sheet: example: A2 on the Index sheet
would be =HYPERLINK("#gid=738389498","Alabama")
Also I need the A1 cell of the State's page to have the same info as
the index. example: Alabama's A1 cell would be =Index!A2
You could take a look at this script:
function createSheets(){
var ss = SpreadsheetApp.getActiveSpreadsheet();
var indexSheet = ss.getSheetByName('Index');
var indexSheetRange = indexSheet.getDataRange();
var values = indexSheetRange.getValues();
var templateSheet = ss.getSheetByName('TEMPLATE_the_state');
templateSheet.activate();
var sheetIds = [],
state,
sheetId,
links = [];
for (var i = 1 ; i < values.length ; i++){
state = values[i][0];
sheetId = undefined;
try{
var sheet = ss.insertSheet(state, {template: templateSheet});
SpreadsheetApp.flush();
sheet.getRange("A1:B1").setValues([['=hyperlink("#gid=0&range=A' +(i+1)+'","go back to index")',state]]);
sheetId = sheet.getSheetId();
}
catch (e) { Logger.log('Sheet %s already exists ' , sheet)}
sheetIds.push([sheetId,state]);
}
sheetIds.forEach(function(x){
links.push(['=HYPERLINK("#gid='+x[0]+'&range=A1","'+x[1]+'")']);
});
indexSheet.getRange(2,1,links.length,links[0].length).setValues(links) // in this case it is clear to us from the outset that links[0].length is 1, so we could have written 1
}
Note that in my version, I created a template sheet from which to base all the state sheets from. This wasn't what you asked for, but I wanted to see what it would do.
The resulting sheet is here: https://docs.google.com/spreadsheets/d/1Rk00eXPzkfov5e3D3AKOVQA2UdvE5b8roG3-WeI4znE/edit?usp=sharing
Indeed, I was surprised at how long it took to create the full sheet with all the states - more than 250 secs. I looked at the execution log, which I have added to the sheet in its own tab. There it is plain to see that the code is quick, but sometimes (why only sometimes, I don't know) adding a new tab to the spreadsheet and/or flushing the formulas on the spreadsheet is very slow. I don't know how to speed it up. Any suggestions welcome. (I could try the Google Sheets API v4, probably would be much faster ... but that is much more work and tougher to do.)

Script to copy data between sheets

I'm trying to create a script to copy data from sheet 1 to sheet 2 and at the same time reorder it. I get my data from a Google form, so data is constantly updating.
Here are two images as examples. N°1 is how I have my data, N°2 is how I want it to be in sheet 2.
The idea is to have the script copying the data every time a new row appears.
Data from Forms.
This is how I would like it to be.
This is my initial code:
function copyrange() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Ingreso'); //source sheet
var testrange = sheet.getRange('J:J');
var testvalue = (testrange.getValues());
var csh = ss.getSheetByName('Auxiliar Ingreso'); //destination sheet
var data = [];
var columnasfijas = [];
var cadena = [];
//Condition check in G:G; If true copy the same row to data array
for (i=1; i<testvalue.length;i++) {
data.push.apply(data,sheet.getRange(i+1,1,1,9).getValues());
if ( testvalue[i] == 'Si') {
data = (sheet.getRange(i+1,1,1,9).getValues()).concat (sheet.getRange(i+1,11,1,9).getValues()); // this beaks up into 2 rows Idon't know why
/*cadena = (columnasfijas);
data.push.apply(data, columnasfijas);*/
}
csh.getRange(csh.getLastRow()+1,1,data.length,data[0].length).setValues(data);
}
//Copy data array to destination sheet
//csh.getRange(csh.getLastRow()+1,1,data.length,data[0].length).setValues(data);
}
In this line, I'm also having trouble concatenating different lengths of data. It should be: (i+1,1,1,6). concat.....(i+1,11,1,3)
data = (sheet.getRange(i+1,1,1,9).getValues()).concat (sheet.getRange(i+1,11,1,9).getValues()); // this beaks up into 2 rows Idon't know why
When I run it as it should by I receive an error that the length should be 9 instead of 3.
This can be accomplished more simply using formulas instead of app scripts:
=sort(importrange("spreadsheetURL", "Sheet1!A2:AA10000"),sort_col#,TRUE/FALSE,[sort_col2#],[TRUE/FALSE]...)
Documentation on importrange function: https://support.google.com/docs/answer/3093340
Documentation on sort function: https://support.google.com/docs/answer/3093150
Once you input the formula, there will likely red triangle on the cell, be sure to click on the cell and click the Allow Access button to give one spreadsheet access to the other.