Using a formula across dynamically added sheets - google-apps-script

I'm trying to work out a formula to sum up values across different sheets for particular rows as in the following:
Sheet 1
A | B
---------
bob | 3
foo | 14
bar | 7
Sheet 2
A | B
---------
bob | 2
foo | 1
bar | 9
But with the caveat that a new 'Sheet 3' can be added with relevant data and will automatically be picked up.
How do I go about getting the summed values for each row in each sheet while handling new (properly named) sheets?
Results
-------
bob | 5
foo | 15
bar | 16
We can assume that the row values are all the same, such as in a named range, People = {bob, foo, bar}.
My attempt has been something like:
={People,ARRAYFORMULA('Sheet 1'!B1:B3+'Sheet 2'!B1:B3)}
but for new sheets, I would have to manually update the formula by adding in
+'Sheet 3'!B1:B3
I've tried using INDIRECT to dynamically generate the sheet names but it doesn't take an array. I'm guessing that I might have to use
SpreadsheetApp.getActiveSpreadsheet().getSheets()
in a script, but if I could do it just as a formula, it would be easier to explain to people inheriting the spreadsheet.
Thanks

I have alternate solution to your problem, using a slightly different approach.
I would suggest pulling all of the data into one results page and summing it there. Then you don't need a massive 'sum' function or a script.
Using indirect you can pull information from each sheet, provided the data is in the same cell location on each sheet. If that's not possible you could also use vlookup to pull the data. I've shared an example of how I would do this using your numbers.
Syntax for 'bob' value on Sheet1 =iferror(indirect("'"&C$2&"'!"&"b3"),"")
where C$2 is the Sheet name (in this case Sheet1) and B3 is the value for bob on Sheet1. Then you can copy this formula across the columns and update your sheet names at the top of the table.
https://docs.google.com/spreadsheets/d/15pB5CclseUetl5zSRPDOR9YA4u6_7TK8RB8CpSxqhnk/edit?usp=sharing

Sample File
The workaround is to use a custom function:
Warning! The function won't refresh automatically. It will refresh if you add and then delete row above it.
Syntax:
=sumBySheets("Sheet1", "Sheet2", "B1:B3")
"Sheet1" — sheet from
"Sheet2" — sheet to
"B1:B3" — range address for a sum.
There may be more sheets between Sheet1 and Sheet2:
The code:
function test_sumBySheets()
{
var sheet1 = 'Sheet1';
var sheet2 = 'Sheet2';
var range = 'b1:b3';
Logger.log(sumBySheets(sheet1, sheet2, range));
}
function sumBySheets(sheet1, sheet2, address)
{
var file = SpreadsheetApp.getActive();
var s1 = file.getSheetByName(sheet1);
var s2 = file.getSheetByName(sheet2);
var i1 = s1.getIndex() - 1; //get sheet indexes
var i2 = s2.getIndex() - 1;
var sheets = file.getSheets();
var result = [];
var arrays = [];
// remember all the data
for (var i = i1; i <=i2; i++)
{
var s = sheets[i];
var range = s.getRange(address);
arrays.push(range.getValues());
}
return sumByArrays_(arrays);
}
function sumByArrays_(arrays)
{
// take array #1
var arr = arrays[0];
l = arr.length;
ll = arr[0].length
// plus all arrays
for (var x = 1, xx = arrays.length; x < xx; x++) // loop arrays
{
for (var i = 0; i < l; i++) { // loop rows
for(var ii = 0; ii < ll; ii++) { // loop cells
arr[i][ii] += arrays[x][i][ii];
}
}
}
return arr;
}
Note:
please run the function test_sumBySheets first and get the permissions.

Been a while, but here's what I ultimately ended up with for my solution.
The following code dynamically loads all sheets associated with the spreadsheet (note that I edited it a bit for readability on SO, might be some typos):
// These vars indicate what sheet, column and rows to start
// the list of sheets.
var main_sheet = 'Sheet1';
var sheet_col = 'A';
var sheet_row_start = 1;
function onEdit(e) {
// The onEdit trigger is the most useful as it fires most often.
// Therefore it is more likely to update the list of sheets relatively
// quickly.
_set_sheet_cells();
}
function _clear_sheet_cells(num_sheets, sheets_length) {
// Clear sheet cells, in case number of sheets has dropped.
var sheet_ctr = Number(num_sheets);
var stats = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(main_sheet);
if (sheet_ctr) {
if (sheet_ctr >= sheets_length) {
for (var i=0 ; i<sheet_ctr ; i++) {
stats_sheet.getRange(sheet_col+(sheet_row_start+i)).clearContent();
}
}
}
}
function _get_sheets() {
// Gather our sheets, can be combined with a regex to prune sheets.
var out = new Array();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
for (var i=0 ; i<sheets.length ; i++) {
out.push( sheets[i].getName() );
}
}
return out
}
function _set_sheet_cells() {
var userProperties = PropertiesService.getUserProperties();
var num_sheets = Number(userProperties.getProperty('sheet_names'));
var sheets = _get_sheets();
if (num_sheets == sheets.length) {
// Do nothing, no changes, remove if concerned about renaming sheets.
return;
}
_clear_sheet_cells(num_sheets, sheets.length);
var stats = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(main_sheet);
for (var j=0 ; j<sheets.length ; j++) {
// Put one sheet name per row.
stats_sheet.getRange(sheet_col+(sheet_row_start+j)).setValue(sheets[j]);
}
userProperties.setProperty('num_sheets', sheets.length)
}
Once I had all the sheets loading dynamically, I just used #cgm990's answer, with the benefit that if I added another sheet to the spreadsheet, it automatically updated all my sums.

Related

Set cell value based on other cell's function result by google script

How to set "status" is 'UP' when the result of column "CountID" change from 0 to >= 1 by google script
I have 2 sheets, main sheet have table below, raw data sheet have raw data auto refresh every hour.
Column "CountID" from main sheet count id from raw data sheet using arrayfomula and CountA function. =arrayformula(COUNTIF('raw data'!A2:A11,A2:A))
In my official spreadsheet, the column index may change therefore, I must use the column header ("Status", "Count") to refer to column position.
"Status" column from main sheet using data validation dropdown list with up to 6 value like: (Cancel, Pending, UP, DOWN, Return ...)
Testing sheet: Testing sheet
ID
Status
CountID
a1
UP
1
a2
pending
0
a3
UP
2
a4
UP
5
a5
Cancel
0
a6
pending
0
The code below working and I just upgrade for column header reference to ensure script working when column index change.
Thanks
Jacques-Guzel Heron.
function updateStatus() {
var sheet = SpreadsheetApp.getActiveSheet()
var headers = sheet.getRange(1,1,1,sheet.getLastColumn()).getValues()[0];
var status_col = headers.indexOf('Status');
var countid_col = headers.indexOf('CountID');
var dataRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("main")
.getDataRange();
var statusRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("main")
.getRange(1,status_col+1,sheet.getLastRow(),1);
var data = dataRange.getValues();
var newData = []
for (var i = 0; i < data.length; i++) {
if (data[i][countid_col] > 0) {
data[i][status_col] = "UP";
}
newData.push(new Array(data[i][status_col]));
Logger.log(countid_col);
}
statusRange.setValues(newData);
}
You can develop a fast Apps Script project easily to update the Status column of your Sheet by using installable triggers. I will show you how in the example below. I chose to run the trigger every half an hour with the everyMinutes method, but you can adapt it to your needs. First you would have to run the createTrigger function once to create the trigger. Then the function updateStatus will run automatically every half hour. Please check the SpreadsheetApp class to learn more about the applied methods.
function createTrigger() {
ScriptApp.newTrigger('updateStatus').timeBased().everyMinutes(30).create();
}
function updateStatus() {
var dataRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("main")
.getDataRange();
var statusRange = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("main")
.getRange("B:B");
var data = dataRange.getValues();
var newData = []
for (var i = 0; i < data.length; i++) {
if (data[i][2] > 0) {
data[i][1] = "UP";
}
newData.push(new Array(data[i][1]));
}
statusRange.setValues(newData);
}
Please keep in mind that the code above only checks if the CountID column equals to 1 or greater to set up the UP status. Therefore, if the CountID gets lowered to 0 the status will still be UP. Please drop a comment if you need further advice with this approach.

How to choose a destination spreadsheet of copied data based on spreadsheet name

So I have two tables. Table 1 is a large table with 7 columns. In this table, C2:C has unique ID numbers of people. Table 2 is a smaller table with the names of all people in K3:K and the unique ID on J3:J. Table 2 only has 2 columns, the IDs and the Name. So, All IDs in C2:C exists in K3:K. All the names in K3:K are also the names of sheets in the same worksheet. So what I'm trying to do is:
Loop through the IDs in table 1 (large table) with the IDs in table 2 (small table). If the IDs are the same, then I will copy the whole row in table 1 into my destination sheet. To choose my destination sheet, I check the cell adjacent to the identified ID in table 2 and choose the sheet whose name is the same.
I hope i explained that decently. My problem it's copying things in every 10 intervals. So it copies the 10th row, then the 20th, then the 30th... and I'm confused how to even approach figuring out where I went wrong because I don't understand why it's in 10 interverals and why it's choosing the sheets that its choosing.
If it helps, link to the sheet is: https://docs.google.com/spreadsheets/d/1522hM3mO9AdaTpiS2oI1IK3wwwFXbOz1qHxcMrRzASY/edit?usp=sharing
function copystuff() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var key = SpreadsheetApp.getActiveSheet().getName();
var currentS = ss.getSheetByName(key);
var sheetNameArray = [];
sheetNameArray = sheets.map(function(sheet){ // puts sheet names into an array
return [sheet.getName()];
});
var name_master = currentS.getRange("K3:K").getValues().flat().filter(r=>r!=''); //key name
var enno_master = currentS.getRange("J3:J").getValues().flat().filter(r=>r!=''); //key ID
var enno_all = currentS.getRange("C2:C").getValues().flat().filter(r=>r!=''); // number of big table
for (x = 0; x< enno_master.length; x++){ //to loop through the key number
for (y = 0; y < enno_all.length; y++){ // to loop through the numbers of big table
if(enno_master[x]==enno_all[y]){ // if ID in column C = name in column J
for(z = 0; z < sheetNameArray.length; z++){ //looping through my sheets
if(name_master[x] == sheetNameArray[[z]]){ //if name in column K, which is of the same row as the key number
var copyrange = currentS.getRange("A"+y+2+":G"+y+2).getValues(); //y is the row in table 1 where the IDs are the same.
var destfile = ss.getSheetByName(sheetNameArray[[z]]); //copying to sheet Z.
destfile.getRange(destfile.getLastRow()+1,1,1,7).setValues(copyrange); //paste
}
}
}}}}
The thing that makes it easy is turning your small table into an object so that when the object is given the number in column 3 of the big table it returns the actual sheet object. The array pA in object sht provides you with the ability to check using indexOf() if the Sheet actually exists in the object without having to use hasOwnProperty().
function copyStuff() {
const ss = SpreadsheetApp.getActive();
const sh = ss.getSheetByName('Sheet1');
const objvs = sh.getRange(3,10,getColumnHeight(10,sh,ss)-2,2).getValues();
const vs = sh.getRange(2,1,sh.getLastRow() - 1,7).getValues();
let sht = {pA:[]};
objvs.forEach(r => {sht[r[0]]=ss.getSheetByName(r[1]); sht.pA.push(r[0])});//This creates an object which correlates enNo to a sheet making it possible to greatly simplify your code.
vs.forEach(r => {
if(~sht.pA.indexOf(r[2])) {//this determines if the sheet is defined in sht object if it is then you can do a copy because the sheet exists
let s = sht[r[2]];
if(s) {
s.appendRow(r);//this appends the entire row to the next empty row at the bottom of data. You could also use copyTo or setValues(). This is a simple approach.
}
}
});
}
Helper function: this function is used to the the height of column j. It's sort of the getLastRow() but only for a column:
function getColumnHeight(col, sh, ss) {
var ss = ss || SpreadsheetApp.getActive();
var sh = sh || ss.getActiveSheet();
var col = col || sh.getActiveCell().getColumn();
var rcA = [];
if (sh.getLastRow()){ rcA = sh.getRange(1, col, sh.getLastRow(), 1).getValues().flat().reverse(); }
let s = 0;
for (let i = 0; i < rcA.length; i++) {
if (rcA[i].toString().length == 0) {
s++;
} else {
break;
}
}
return rcA.length - s;
}
~ Bitwise Not
The easiest way to find information in the documentation is to use the index in it. Especially since they just changed it all and many of us no longer know where everything is.
SUGGESTION:
Since you want to check the large table A:G & then copy each table data that matches every user ID on the second table J:K and move those matched rows to the designated sheet named after the IDs' user names, you can refer to this sample implementation below:
function copystuff() {
var ss = SpreadsheetApp.getActiveSheet();
var main =SpreadsheetApp.getActiveSpreadsheet();
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var sheetNameArray = sheets.map(function(sheet){
return [sheet.getName()]});
var lastRowK = ss.getRange(ss.getLastRow(),11).getNextDataCell(SpreadsheetApp.Direction.UP).getRow(); //Get the last row of column K on any worksheet
var user = main.getSheetByName(sheetNameArray[0]).getRange("J3:K"+lastRowK).getValues();
var tableData = main.getSheetByName(sheetNameArray[0]).getRange("A2:G"+main.getLastRow()).getDisplayValues();
var container = [];
for(x=0; x<user.length; x++){ //Loop every user from the second table
for(y=0; y<tableData.length; y++){ //loop through the large table data
if(tableData[y][2] == user[x][0]){ //check each user's ID if it has any matches on the user ID from the large table data
container.push(tableData[y]); //any matched row data from the large table will be placed temporarily on this container
}
}
if(container.length != 0){
Logger.log("User "+user[x][1]+ " with ID *"+ user[x][0] +"* has these data:\n"+container);
main.getSheetByName(user[x][1]).getRange(1,1,container.length,7).setValues(container);
}
container = []; //clean container for the next user
}
}
This script will get all table data and place them into an array variables user & tableData where it will be checked and processed to make the script run faster instead of live processing the sheet rows.
Sample Result:
After running the script, here are some of the results:
User name RJ with ID of 42:
User name May with ID of 7:
User name Angelo with ID of 25:
Here's the Log Results for review:

How to remove duplicate rows in Google Sheets using script

I currently have a column of data titled JobID. In this column, there are duplicates from an import that runs daily and grabs the latest data on the JobID's in question and appends them to the top of the sheet.
Therefore the most recent JobID rows are the ones with the data we need.
I'd like to know if there is a script that can be run on the sheet called 'History' to look up the column JobID, search every row below for duplicates and remove them, leaving the top, most recent JobID rows in the sheet.
I know that it is really easy to remove duplicates using the "Remove Duplicates" tool in Google Sheets... but I'm lazy and I'm trying to automate as much of this process as possible.
The script I have below runs without an error but is still not doing what I need it to. Wondering where I am going wrong here:
function removeDuplicates() {
//Get current active Spreadsheet
var sheet = SpreadsheetApp.getActive();
var history = sheet.getSheetByName("History");
//Get all values from the spreadsheet's rows
var data = history.getDataRange().getValues();
//Create an array for non-duplicates
var newData = [];
//Iterate through a row's cells
for (var i in data) {
var row = data[i];
var duplicate = false;
for (var j in newData) {
if (row.join() == newData[j].join()) {
duplicate = true;
}
}
//If not a duplicate, put in newData array
if (!duplicate) {
newData.push(row);
}
}
//Delete the old Sheet and insert the newData array
history.clearContents();
history.getRange(1, 1, newData.length, newData[0].length).setValues(newData);
}
Remove Duplicate JobIDs
This function will keep the ones nearest to the top of the list. If you want to go the other way then resort the list in reverse order.
function removeDuplicates() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("History");
var vA=sh.getDataRange().getValues();
var hA=vA[0];
var hObj={};
hA.forEach(function(e,i){hObj[e]=i;});//header title to index
var uA=[];
var d=0;
for(var i=0;i<vA.length;i++) {
if(uA.indexOf(vA[i][hObj['JobID']])==-1) {
uA.push(vA[i][hObj['JobID']]);
}else{
sh.deleteRow(i+1-d++);
}
}
}
Remove Duplicate JobIDs in Python
Based on Cooper's answer I wrote the same function in Python:
gsheet_id = "the-gsheet-id"
sh = gc.open_by_url("https://docs.google.com/spreadsheets/d/%s/edit#gid=0" % gsheet_id)
wks = sh[0]
def removeDuplicates(gwks):
headerRow = gwks[1]
columnToIndex = {}
i = 0
for column in headerRow:
columnToIndex[column] = i
i += 1
uniqueArray = []
d = 0
row_i = 0
for row in gwks:
row_i += 1
if gwks[row_i][columnToIndex['JobID']] not in uniqueArray:
uniqueArray.append(gwks[row_i][columnToIndex['JobID']])
else:
d += 1
gwks.delete_rows(row_i + 1 - d, 1)
removeDuplicates(wks)

Create a new sheet for every value in column A and import a template

I have a Google Sheet that I use different queries to pull information. I am wanting to branch these into their own sheet.
I want to make a new sheet for every name in column A and rename it accordingly. I want the new sheet to have that name be placed in cell A1.
I also have a formula I want to be placed in A2. (The formula could be built in script of pulled from a random cell~doesn't matter)
Built in google but can also switch to Excel if needed
link
This is what I have been trying to build from but no success. My original script takes the list and makes new tabs, the second function places the name in cell A1...instead of tabs want a sheet and need functions to work together
function dupsheetsTemp()
{
var ss = SpreadsheetApp.getActiveSpreadsheet(),
temp = ss.getSheetByName('Master'),
all = ss.getSheetByName('Students'),
sheets = ss.getSheets(),
vals = all.getRange('a:b').getValues();
for (var i = 0, len = sheets.length, names = []; i < len; i++)
names.push(sheets[i].getName());
for (var i = vals.length-1; i > 0; i--)
{
var name = vals[i][0];
if (name !='' && names.indexOf(name) == -1)
{
temp.activate();
ss.duplicateActiveSheet()
.setName(vals[i][0])
.getRange(1, 1).setValue(vals[i][1]);
}
}
}
function sheetNames() {
SpreadsheetApp.getActive()
.getSheets()
.forEach(function (s) {
s.getRange('A1')
.setValue(s.getName())
});
}

How to check duplicates in an entire Google Spreadsheet

I'm trying to accomplish something perhaps to ambitious but perhaps you could give me a hand.
I have a Google Spreadsheet with 4 Sheets:
Master Senpai | Chevez San | Gabbie Sama | Mario Chan
"Master Senpai" is a sheet with Consolidated data meaning everything in the other 3 sheets is within "Master Senpai"
These spreadsheet is currently being use by 3 coworkers (as you might guess Chevez Gabbie and Mario) what I need is for each of them to have the ability to confirm or check if any of the info they are inputting is duplicated.
All the Sheets have the same headers from A to L (12 Columns total) I want to check the duplicates for Column J.
So for Example if Mario Want to know if the info he just inputted Gabbie or Chevez has already put it I want that cell to get Formated with a red background.
Basically read the info from the last 3 sheets (Ignore the "Master Senpai" sheet) and check for duplicates if any found color them with a red background.
I've been reading about it and found something similar to what I'm trying to do.
/**
* Finds duplicate rows in the active sheet and colors them red,
but only pays attention to the indicated columns.
*/
function findDuplicates() {
// List the columns you want to check by number (A = 1)
var CHECK_COLUMNS = [2,3,5,6];
// Get the active sheet and info about it
var sourceSheet = SpreadsheetApp.getActiveSheet();
var numRows = sourceSheet.getLastRow();
var numCols = sourceSheet.getLastColumn();
// Create the temporary working sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var newSheet = ss.insertSheet("FindDupes");
// Copy the desired rows to the FindDupes sheet
for (var i = 0; i < CHECK_COLUMNS.length; i++) {
var sourceRange = sourceSheet.getRange(1,CHECK_COLUMNS[i],numRows);
var nextCol = newSheet.getLastColumn() + 1;
sourceRange.copyTo(newSheet.getRange(1,nextCol,numRows));
}
// Find duplicates in the FindDupes sheet and color them in the main sheet
var dupes = false;
var data = newSheet.getDataRange().getValues();
for (i = 1; i < data.length - 1; i++) {
for (j = i+1; j < data.length; j++) {
if (data[i].join() == data[j].join()) {
dupes = true;
sourceSheet.getRange(i+1,1,1,numCols).setBackground("red");
sourceSheet.getRange(j+1,1,1,numCols).setBackground("red");
}
}
}
// Remove the FindDupes temporary sheet
ss.deleteSheet(newSheet);
// Alert the user with the results
if (dupes) {
Browser.msgBox("Possible duplicate(s) found and colored red.");
} else {
Browser.msgBox("No duplicates found.");
}
};
/**
* Adds a custom menu to the active spreadsheet
*/
function onOpen() {
var sheet = SpreadsheetApp.getActiveSpreadsheet();
var entries = [{
name : "Find Duplicates",
functionName : "findDuplicates"
}];
sheet.addMenu("My Scripts", entries);
fillFormulas();
};
However this only looks for duplicates in the same sheet what I'm trying to accomplish is to read and compare the info from the last 3 sheets.