I have a growing dataset on a google sheet with which I need to run some complex filters/queries/vlookups on. However I only need to do this daily as the new data arrives. The number of complex formulas is starting to slow the spreadsheet to a grinding halt, particularly with our woeful broadband connection.!
I therefore came up with a workaround of using GAS to set the formula in a cell, then to get the Value and then to set the Value, knowing that GAS doesn't run the spreadsheet functions natively (as per VBA). As I have already worked up the filters and vlookups on the sheets I need, I didn't go into scripting the formulas to achieve the same thing.
Here is a simplified version of the code:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sh = ss.getSheetByName('Sheet1');
var rng = sh.getRange('B11');
rng.setFormula('=sum(filter(C2:E5,A2:A5="Bob"))');
var val = rng.getValue();
rng.setValue(val);
}
In my production spreadsheet I can have # 300 formulas on each of 30 sheets, so if these are all pinging away at the dataset I get lengthy periods of the grey progress bar. (In essence the formulas are filtering/summing or counting daily data to weekly data) My example above shows everything happening on one sheet.
Wondered if there was a better/different way of doing this?
Thanks
Tim
Well, I have not come up with anything better so will post my solution. Two scripts. First one checks that the user actually want to update their values, if they say yes, then checks with the user again and shows them the date range it will update. Then runs the second script, which in simple terms just applies a formula to a cell then copies the value generated and pastes the value. On testing with full data load, spreadsheet does no "waiting/progress grey box" at all so solves my issue.
function runWriteBehavs() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sy = ss.getSheetByName("summary");
//gets last row in column B - data is contigious
var BVals = sy.getRange("B1:B").getValues();
var BLast = BVals.filter(String).length;
var rowBeh = BLast + 1;
var lastDate = sy.getRange("A" + rowBeh).getValue();
var lastEndDate = new Date(lastDate.getTime()+6*3600000*24);;
//formats dates
lastDate = Utilities.formatDate(new Date(lastDate), "GB", "dd/MM/yyyy");
lastEndDate = Utilities.formatDate(new Date(lastEndDate), "GB", "dd/MM/yyyy");
//message box for confirmation
var output = Browser.msgBox("This script will run the summaries for the week:\\n" + lastDate + " - " + lastEndDate + "\\n Are you really sure you want to continue?",Browser.Buttons.YES_NO);
if ( output == "yes" ) {
//calls main update script
writeBehavs();
}
}
//Needs to be run once all data is entered
function writeBehavs() {
//get variables
var ss = SpreadsheetApp.getActiveSpreadsheet();
var db = ss.getSheetByName("database");
var sy = ss.getSheetByName("summary");
var sL = ss.getSheetByName("lists");
//gets number of behaviours, a counta() of list on sheet
var bCount = sL.getRange("H1").getValue();
//gets column listing hard coded on sheet
var bCol = sL.getRange("H2:H30").getValues();
//gets last row in column B - data is contigious
var BVals = sy.getRange("B1:B").getValues();
var BLast = BVals.filter(String).length;
//for each number on behaviour count
for (var i=0; i<bCount; ++i) {
//set the column
var colBeh = [bCol[i]];
//set the correct row for data entry and start date check
var rowBeh = BLast + 1;
//sets correct row for end date check
var rowBeh2 = rowBeh + 1;
//gets first empty row in Column from iteration
var rng = sy.getRange(colBeh+rowBeh);
//enters the formula in the cell
rng.setFormula('=iferror(sum(filter(database!$E$2:$E,database!$D$2:$D='+ colBeh + '$1,database!$A$2:$A=lists!$G$2,database!$B$2:$B>=$A' + rowBeh + ',database!$B$2:$B<$A' + rowBeh2 + ')),"0")');
//captures the value generated by the formula
var val = rng.getValue();
//pastes the formula to the cell
rng.setValue(val);
//Job Done!
}
}
Related
So I have a table that will have data to the last row except column B. (for example, cols A, C, D etc. stop at row 40, but B stops at maybe row 25). I want to programmatically keep the series going starting at the last cell with data in B and autofill down to the last row in the spreadsheet. (Hopefully a script doing this will recognize the series and not just copy the same data to all the empty cells. When I do it manually it works.) I have something started here but I can't figure out how to call out the range of where to start the series. I get an error on line 7 "Exception: Range not found".
function fillDownFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var sc = ss.getRange("B1:B").getValues();
var scr = sc.filter(String).length;
var lr = ss.getLastRow();
var fillDownRange = ss.getRange(scr,2,lr)
ss.getRange(scr).copyTo(fillDownRange);
}
In addition to the answers already provided, Apps Script already has an autoFill() method that you can use to do this. With this you just have to define a source and a destination range. This works the same as selecting the range and dragging down your mouse manually on the corner.
function fillDownFunction() {
var ss= SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()
var lastsheetrow = ss.getLastRow() //last row with data
//last row in the autofill range with data
var lastrangerow = ss.getRange("B1:B").getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow()
var sourcerange = ss.getRange(2, 2,lastrangerow-1) //in your screenshot, this would be B2:B6
var destinationrange = ss.getRange(2, 2, lastsheetrow-1) //This would be B2:B12
sourcerange.autoFill(destinationrange, SpreadsheetApp.AutoFillSeries.DEFAULT_SERIES)
}
Note that I skipped the header row and offset the range lengths by -1 to compensate. This is because the autofill logic uses the entire range, so it would also take into account "Item2" and repeat it every 6 rows as "Item3", "Item4" and so on. A disadvantage in this case, but may prove useful if you plan to use autofill in more complex ways in the future.
Here is one way you could have the last fill value copy down. The function grabs all of the values, maps the row that needs to fill down and then copies that last value down the rest of the sheet.
function fillDownFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var tab = ss.getActiveSheet();
var sc = tab.getDataRange().getValues();
var lastRow = sc.length;
var values = sc.map(function(r) { return r[1]; }).filter(i => i);
var lastFillRow = values.length;
var fillDownValue = values.slice(-1);
tab.getRange(lastFillRow + 1, 2, lastRow - lastFillRow).setValue([fillDownValue]);
}
Disclaimer: I'm a Google Apps Script newbie.
I'm trying to create a timesheet in Google Sheets that lets a user clock in & clock out to log hours on a given project. I've borrowed code from a YouTube video on the general structure of setting the whole thing up.
Here's what the blank time sheet looks like. It's pretty basic:
I've created a user button (off to the right) where the user presses "Start" and cell A2 will input a timestamp. Then the user can press an "End" button, and a second timestamp, this time in B2, will appear, along with a simple calculation in C2 that measures the delta in the two timestamps, thus giving a duration of time spent on a given task or project. Here's what it looks like:
When the user needs to press "Start" again, a new timestamp appears in cell A3, and so on so forth, along with a new delta calculation for each new row.
Problem: I'm unable to get the simple delta calculation in column C to increment down each new rows so that the setFormula function doesn't contain hardcoded references to cells A2 & B2. See below code for what I have so far:
function setValue(cellName, value) {
SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).setValue(value);
}
function getValue(cellName) {
return SpreadsheetApp.getActiveSpreadsheet().getRange(cellName).getValue();
}
function getNextRow() {
return SpreadsheetApp.getActiveSpreadsheet().getLastRow() + 1;
}
function addStartRecord (a) {
var row = getNextRow();
setValue('A' + row, a);
}
function addEndRecord (b, c) {
var row = getNextRow()-1;
setValue('B' + row, b);
setValue('C' + row, c);
}
function punchIn() {
addSRecord(new Date());
}
function punchOut() {
addERecord(new Date(), '=B2-A2');
}
The problem is with the punchOut() function there at the bottom. Any idea on the best way to increment this delta calculator down each new row?
Note: I saw a pretty good answer to a similar question here, but the code is throwing an error in the script editor after the line containing data[i] = ['=A' + i+1.toString() + ' + 1 ' ]. Also, I don't want to set a definitive last row for the delta calculation (such as 20 in this example). I'd want the user to be able to record as many new start/end times for a project as they'd want.
Edit: Here's a link to the timesheet so you can test the code.
Try modifying your punchOut method like this:
function punchOut() {
var ss = SpreadsheetApp.getActiveSheet();
var row = ss.getLastRow();
addEndRecord(new Date(), '=B' + row + '-A' + row);
}
I tested it in the sheet and it worked well.
setFormula() - this enables you to describe the formula to be inserted into column C.
The following is two simple functions that handle "Punch in" and "Punch Out" (with its calculation).
function so5695101401in() {
// punchin
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lR = sheet.getLastRow();
// Logger.log("DEBUG: the last row is "+lR);
var punchinRange = sheet.getRange(lR+1, 1);
// Logger.log("DEBUG: the punchinRange = "+punchinRange.getA1Notation());
punchinRange.setValue(new Date());
}
function so5695101401out() {
// punchout
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getActiveSheet();
var lR = sheet.getLastRow();
//Logger.log("DEBUG: the last row is "+lR);
var punchoutRange = sheet.getRange(lR, 2);
// Logger.log("DEBUG: the punchoutRange = "+punchoutRange.getA1Notation());
punchoutRange.setValue(new Date());
var timeElapsed = sheet.getRange(lR, 3).setNumberFormat("hh:mm:ss");
timeElapsed.setFormula("=B2-A2");
}
setFormula
I use a workaround for this problem, via app script copy the cell with the formula to de new row or range!.
for you problem:
var formula1 = sheetDatos.getRange(lastRow, 3); //get the formula
var copyRange = sheetDatos.getRange(lastRow+1, 3);
formula1.copyTo(copyRange);
for me is more easy in this way, try to do in sheet to understand how this work.
you need a initial formula to go in this way ;)
I have this script, which works perfectly (although I'm sure there's a better way to do this).
function dataTransfer() {
var value1 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('BACKLOG').getRange('G18').getValue();
var value2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('BACKLOG').getRange('AF8').getValue();
var sheet2 = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('AM COMP');
var cell1 = sheet2.getRange('D8');
var cell2 = sheet2.getRange('D18');
cell1.setValue(value1);
cell2.setValue(value2);
}
This code reads cells for data and copies that data to another sheet when I press a button I created. What I need is for the cells that I'm getting data from and putting data into to change (some by an increment of 2 rows over, some by 1) each time I run the script.
For example: in January I press the button and the value in G18 (BACKLOG) is copied to D8 (AM COMP), the value in AF8 (BACKLOG) gets copied to D18 (AM UPSELL). But in February I would like to push the button again but this time have the value from I18 (BACKLOG) copied to E8 (AM COMP), AF8 (BACKLOG) stays the same but gets copied to E18 (AM COMP). For March they would need to move again, etc.
What is the best way to accomplish this?
This might not be a complete solution, but it should give you an idea of what needs to be done. You need to fill in the values for the rest of the months in the object literal.
function dataTransfer() {
var addr_2_ToGet,cellAddressToGet,cellAddressToSet,
currentMonthNumber,d,innerObj,objectMap,sourceSheet,ss;
d = new Date();//Get the current date
currentMonthNumber = d.getMonth();//Get a number which represents
//the current month - 0 = January
Logger.log('currentMonthNumber: ' + currentMonthNumber)
objectMap = {
0:{cellToGet:'G18',cellTwoToGet:'',cellToSet:'D8',cellTwoToSet:''},
1:{cellToGet:'I18',cellToSet:'E8'},
2:{cellToGet:'',cellToSet:''},
3:{cellToGet:'',cellToSet:''},
4:{cellToGet:'',cellToSet:''},
5:{cellToGet:'',cellToSet:''},
6:{cellToGet:'',cellToSet:''},
7:{cellToGet:'',cellToSet:''},
8:{cellToGet:'',cellToSet:''},
9:{cellToGet:'',cellToSet:''},
10:{cellToGet:'',cellToSet:''},
11:{cellToGet:'',cellToSet:''}
}
innerObj = objectMap[currentMonthNumber]
Logger.log('innerObj: ' + innerObj)
cellAddressToGet = innerObj.cellToGet;
Logger.log('cellAddressToGet: ' + cellAddressToGet)
addr_2_ToGet = innerObj.cellTwoToGet;
cellAddressToSet = innerObj.cellToSet;
Logger.log('cellAddressToSet: ' + cellAddressToSet)
addr_2_To_Set = innerObj.cellTwoToSet;
Logger.log('addr_2_To_Set: ' + addr_2_To_Set)
ss = SpreadsheetApp.getActiveSpreadsheet();
sourceSheet = ss.getSheetByName('BACKLOG');
var value1 = sourceSheet.getRange(cellAddressToGet).getValue();
var value2 = sourceSheet.getRange(addr_2_ToGet).getValue();
var sheet2 = ss.getSheetByName('AM COMP');
var cell1 = sheet2.getRange(cellAddressToSet);
var cell2 = sheet2.getRange(addr_2_To_Set );
cell1.setValue(value1);
cell2.setValue(value2);
}
Monthly Data Transfers
All of your settings for each month are neatly stashed away in the sheet named 'DATA'.
function dataTransfer() {
var ss=SpreadsheetApp.getActive();
var dataSh=ss.getSheetByName('DATA');
var m=new Date().getMonth()+1;//The + 1 is to get past the header row
var values=dataSh.getRange(1,1,dataSh.getLastRow(),dataSh.getLastColumn()).getValues();
var hdrA=values[0];
var valA=values[m];
var vObj={};
for(var i=0;i<valA.length;i++){
vObj[hdrA[i]]=valA[i];//the hdrA is the key and the valA is the value
}
ss.getSheetByName(vObj.SH).getRange(vObj.RG1).setValue(ss.getSheetByName(vObj.valSH1).getRange(vObj.valRG1).getValue());
ss.getSheetByName(vObj.SH).getRange(vObj.RG2).setValue(ss.getSheetByName(vObj.valSH2).getRange(vObj.valRG2).getValue());
Logger.log('SH: %s,RG1: %s,valSH1: %s,valRG1: %s,RG2:%s,valSH2: %s,valRG2: %s,MONTH: %s',vObj.SH,vObj.RG1,vObj.valSH1,vObj.valRG1,vObj.RG2,vObj.valSH2,vObj.valRG2,vObj.MONTH);
}
This is what the 'DATA' sheet looks like:
Of course I just filled the table in with the data that you provided for one month and I added the month column. You can add more columns and then give them a name in the header row and that will become the key for extracting in out of vObj.
Background: My coworkers originally each had a worksheet within the same Google Sheets file that makes a lot of calculations (and was getting unusable). Now, everyone has their own (known) Google Sheets file. To run the same calculations, we need to consolidate all that data into a master sheet (image ref below). We tried =importrange(...), but it's too heavy and breaks often (i.e., Loading... and other unfilled cells).
I've written some code to do this import, but right now its only manual: manually repeating the code and manually add the sheet IDs and changing the destrange.getRange(Cell range) each time. We have 80+ analysts, and fairly high turnover rates, so this would take an absurd amount of time. I'm new to Sheets and Apps Script, and know how to make the script use a cell as reference for a valid range or a valid ID, but I need something that can move a cell down and reference the new info.
Example:
Sheet 1 has a column of everyone Sheet ID
Script Pseudocode
get first row's id(Row 1), get sheet tab, get range, copies to active sheet's corresponding row(Row 1).
gets second row's id(Row 2), get sheet tab, get range, copies to active sheet's corresponding row (Row 2)
etc.
My script understanding is way to low to know how to process this. I have no idea what to read and learn to make it work properly.
function getdata() {
var confirm = Browser.msgBox('Preparing to draw data','Draw the data like your french girls?', Browser.Buttons.YES_NO);
if(confirm == 'yes'){
// I eventually want this to draw the ID from Column A:A, not hard-coded
var sourcess = SpreadsheetApp.openById('1B9sA5J-Jx0kBLuzP5vZ3LZcSw4CN9sS6A_mSbR9b26g');
var sourcesheet = sourcess.getSheetByName('Data Draw'); // source sheet name
var sourcerange = sourcesheet.getRange('E4:DU4'); // range
var sourcevalues = sourcerange.getValues();
var ss = SpreadsheetApp.getActiveSpreadsheet(); //
var destsheet = ss.getSheetByName('Master Totals'); //
// This range needs to somehow move one down after each time it pastes a row in.
var destrange = destsheet.getRange('E4:DU4');
destrange.setValues(sourcevalues); // Data into destsheet
}
}
Any suggestions are greatly appreciated!
Thanks to tehhowch for pointing me in the right direction!
function getdata() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var destsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Master Totals');
var confirm = Browser.msgBox('Drawing Data','Would you like to update the sheet? It may take 2 to 5 minutes.', Browser.Buttons.YES_NO);
if(confirm =='yes'){
var lr = ss.getLastRow();
for (var i = 4; i<=lr; i++) {
var currentID = ss.getRange(i, 1).getValue();
var sourcess = SpreadsheetApp.openByUrl(currentID);
var sourcesheet = sourcess.getSheetByName('Data Draw');
var sourcerange = sourcesheet.getRange('E4:DU4');
var sourcevalues = sourcerange.getValues();
var destrange = destsheet.getRange('E' +i+':'+ 'DU'+ i);
destrange.setValues(sourcevalues);
I just had to learn how to use a variable loop.
Edit: thanks also to Phil for making my question more presentable!
Now that you've figured out one way to do it, I'll offer an alternative that uses batch methods (i.e. is much more time- and resource-efficient):
function getData() {
var wb = SpreadsheetApp.getActive();
var ss = wb.getActiveSheet();
var dest = wb.getSheetByName('Master Totals');
if (!dest || "yes" !== Browser.msgBox('Drawing Data', 'Would you like to update the sheet? It may take 2 to 5 minutes.', Browser.Buttons.YES_NO))
return;
// Batch-read the first column into an array of arrays of values.
var ssids = ss.getSheetValues(4, 1, ss.getLastRow() - 4, 1);
var output = [];
for (var row = 0; row < ssids.length; ++row) {
var targetID = ssids[row][0];
// Open the remote sheet (consider using try-catch
// and adding error handling).
var remote = SpreadsheetApp.openById(targetID);
var source = remote.getSheetByName("Data Draw");
var toImport = source.getRange("E4:DU4").getValues();
// Add this 2D array to the end of our 2D output.
output = [].concat(output, toImport);
}
// Write collected data, if any, anchored from E4.
if(output.length > 0 && output[0].length > 0)
dest.getRange(4, 5, output.length, output[0].length).setValues(output);
}
Each call to getRange and setValues adds measurable time to the execution time - i.e. on the order of hundreds of milliseconds. Minimizing use of the Google interface classes and sticking to JavaScript wherever possible will dramatically improve your scripts' responsiveness.
I'm trying to write a script where the dates in a column are compared against todays date, and if the dates in the column match, an email is sent.
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSheet();
var rotDate = sheet.getRange("F6").getValue();
var today = sheet.getRange("F1").getValue();
//var today = new Date().toLocaleDateString(); // Today's date, without time
var dumpCell = sheet.getRange("J3");
var dumpCell2 = sheet.getRange("J4");
var dumpCell3 = sheet.getRange("J5");
if(rotDate==today) {
//dumpCell is there to dump a value in a cell if the IF statement is true
dumpCell.setValue(rotDate);
MailApp.sendEmail("this is where my email would go", "subject", "body");
}
//these dump the compared vars into cells so they can be checked against one another manually
dumpCell2.setValue(rotDate)
dumpCell3.setValue(today)
}
This is as far as I've gotten. The Values in F6 and F1 are identical, I've typed them out, retyped them, copied and pasted, etc. But for some reason, my if statement just won't run. It behaves as if the two values are different, and I can't work out why.
If I change var rotDate and var today to matching strings, eg "123" then it seems to work as expected.
This is a screenshot of my test data sheet. There are other columns there with other data which were meant to be used for more testing, but I didn't get that far.
Does anyone know what I might be doing wrong?
After trying a variety of approaches, I cracked it using a code snippet from Jon Lin's answer here:
Compare two dates Google apps script
After realizing that the fault was with trying to compare two dates (either a date in an adjacent cell, or a procedurally generated date whenever the function is run, I knew I had to do some better formatting with the data I was intending to compare. This is my repaired code that now works as expected:
function sendEmails() {
var sheet = SpreadsheetApp.getActiveSheet();
var rotDate = sheet.getRange("F6").getValues();
var today = new Date();
//sample values
var sYyyy = Utilities.formatDate(new Date(rotDate), "GMT+8","yyyy");
var sMm = Utilities.formatDate(new Date(rotDate), "GMT+8","MM");
var sDd = Utilities.formatDate(new Date(rotDate), "GMT+8","dd");
//Test Values
var tYyyy = Utilities.formatDate(new Date(today), "GMT+8","yyyy");
var tMm = Utilities.formatDate(new Date(today), "GMT+8","MM");
var tDd = Utilities.formatDate(new Date(today), "GMT+8","dd");
//var rotDate = sheet.getRange("F6").getValue();
//var today = sheet.getRange("F1").getValue();
//var today = new Date().toLocaleDateString(); // Today's date, without time
var dumpCell = sheet.getRange("J3");
var dumpCell2 = sheet.getRange("J4");
var dumpCell3 = sheet.getRange("J5");
if (sYyyy + sMm + sDd == tYyyy + tMm + tDd) {
//if(rotDate===today) {
//dumpCell is there to dump a value in a cell if the IF statement is true
dumpCell.setValue(rotDate);
MailApp.sendEmail("tv18766#gmail.com", "subject", "body");
}
//these dump the compared vars into cells so they can be checked against one another manually
dumpCell2.setValue(rotDate)
dumpCell3.setValue(today)
}