How to increment simple formula by 1 in Google Apps Script? - google-apps-script

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 ;)

Related

How to speed up COLUMNS deleting function in Google App Script

I am working through data in Google sheets, attempting to delete EMPTY COLUMNS based I have 600 so it take too much time
Any suggestions on how to speed this up?
Here is my code :
function delCols() {
var s = SpreadsheetApp.getActive().getActiveSheet()
var c = 600
var data = s.getRange(1, 1, 1, c).getValues()
Logger.log(data[0].length)
var delCol=0
for (var i = data[0].length; i >0 ; i--) {
if(s.getRange(1, c ).getValue() =="") { s.deleteColumn(c);delCol=delCol+1 }
c=c-1
}
s.insertColumns(600-delCol, delCol);
Logger.log(delCol)
}
Thank you in advance if you may help
Create a new sheet, for example "Sheet0", for intermediate data. It can be hidden.
Run this script function to remove empty columns from the active sheet:
function removeEmptyColumns() {
var ss = SpreadsheetApp.getActive();
var activeSheet = ss.getActiveSheet(),
sheet0 = ss.getSheetByName('Sheet0');
// AHG is 600-th column (if I am not mistaken)
var formula = "=TRANSPOSE(QUERY(TRANSPOSE('" + activeSheet.getName() +
"'!A:AHG); \"select * where Col1 is not null\"; 0))";
sheet0.getRange('A1').setFormula(formula);
SpreadsheetApp.flush();
var values = sheet0.getDataRange().getValues();
sheet0.getRange('A1').clear();
activeSheet.clearContents();
activeSheet.getRange(1, 1, values.length, values[0].length).setValues(values);
}
I believe it is faster, than column by column deletion. Some remarks:
There is a restriction for use of formulas on the active sheet, because we copy only values.
If you do not remove the formula in the script, then calculations will take place on every active cheet change, resulting in delays.
Please note, empty column detection is based on top cell value, as in your code.

Google Sheets: How to automatically insert a word into a cell on a new row detected?

I have searched high and low but I have been unable to find an answer (I am sure I am not explaining it right)
I have a Google Sheet that have multiple sheets (tabs) labeled TabA, TabB and TabC.
On this Google Sheet, I submit a slash command on Slack, which then auto-fills a row on one of the tabs using apps script.
What I am trying to do is simply insert a word called TabA into a specific cell each time a new row has been detected. And insert a word called TabB when a new row has been made on TabB sheet etc.
I am sure I just am typing my questions wrong which is why I am unable to find an answer.
I am not actually sure which part of the code posts to the sheet, I think it is this?
if(sheetName) {
sheetName = sheetName.charAt(0).toUpperCase() + sheetName.slice(1)
} else {
sheetName = "Current"
}
// Find sheet
var sheetFlag = false;
for (var r = 1; r < settings.length; r++) {
if (settings[r][1] == channelID) {
var sheetID = settings[r][2];
var targetChannelID = settings[r][4];
var title = settings[r][0];
sheetFlag = true;
break;
}
}
if (sheetFlag) {
var sheet = SpreadsheetApp.openById(sheetID).getSheetByName(sheetName);
if(!sheet) {
sheet = SpreadsheetApp.openById(sheetID).insertSheet(sheetName);
}
var lastRow = sheet.getLastRow();
var slackDetails = ["", "", text1, "","","","","",realName, new Date(),title,text2];
// paste the slack details to the sheet
sheet.getRange(lastRow + 1,1,1,slackDetails.length).setValues([slackDetails]);```
Thank you in advance
If I understood you correctly, you want to:
Keep track of new rows that are added to each sheet in your spreadsheet (TabA, TabB, TabC).
Write the name of the sheet in successive rows of column D of each sheet every time news rows are detected.
As you were told in the comments, Apps Script has no triggers to track changes made to the spreadsheet by a script. For example, onEdit trigger "runs automatically when a user changes the value of any cell in a spreadsheet".
Workaround (time-based trigger and script properties):
A possible workaround to this limitation is using a time-based trigger that will fire a function periodically. You can create this trigger manually, or programmatically, by running this function once:
function createTrigger() {
ScriptApp.newTrigger("trackRows")
.timeBased()
.everyMinutes(1)
.create();
}
This will fire the function trackRows every minute. This function's purpose is to track changes to each sheet rows since last time it was fired (in this example, 1 minute ago) and write the sheet name to a certain cell if the sheet has more rows with content than during last execution.
To accomplish this, you can use the Properties Service, which lets you store key-value pairs and use them in later executions.
The function trackRows could be something along the following lines:
function trackRows() {
var props = PropertiesService.getScriptProperties();
var ss = SpreadsheetApp.openById("your-spreadsheet-id"); // Please change accordingly
var sheets = ss.getSheets();
sheets.forEach(function(sheet) {
var sheetName = sheet.getName();
var currentRows = sheet.getLastRow();
var oldRows = props.getProperty(sheetName);
if (currentRows > oldRows) {
var firstRow = 2;
var column = 4;
var numRows = sheet.getLastRow() - firstRow + 1;
var rowIndex = sheet.getRange(firstRow, column, numRows).getValues().filter(function(value) {
return value[0] !== "";
}).length;
var cell = sheet.getRange(rowIndex + firstRow, column);
cell.setValue(sheetName);
}
props.setProperty(sheetName, currentRows);
});
}
This function does the following:
Retrieve the script properties that were stored in previous executions.
Get all the sheets in the spreadsheet.
Check the last row with content in each sheet (via Sheet.getLastRow()), and compare the value with the one previously stored in script properties.
If the current last row is higher than the one stored in properties, write the sheet name in the first empty row of column D of the corresponding (starting at D2).
Store the current last row in script properties.
Notes:
The script is adding the sheet name to the first empty row of column D once, if it detects that new rows were added. It's not taking into account how many rows were added since last execution, it only considers if rows were added. This could easily be changed though, if that's what you wanted.
If you want to start from fresh, it would be useful to delete all previously stored properties. To do that, you could run this function once:
function deleteProps() {
var props = PropertiesService.getScriptProperties();
props.deleteAllProperties();
}
Reference:
Class ClockTriggerBuilder
Class PropertiesService
Sheet.getLastRow()

Functions in Google Sheets broken when script is run

I'll start this off by saying I have no clue what I'm doing. I'm surviving off copying and pasting code off the internet for a spreadsheet me and my friends use for watching films together.
I've run into an issue where I'm updating a cell with the current date when another cell in that row is updated if its blank with a script.
This issue is I then use a function in the cell next to it to give the difference in days for another date marked down in a cell (like a normal spreadsheet as that easier for me to do). But every time the script runs the function breaks and is replaced with the text "#NUM!" (Actually has that text as the function disappears from inside it).
I tried changing it to =U2 and that breaks also. Is this something that can't be done? The great almighty google god has not provided me with an answer so I've made an account here in hope of salvation.
tl;dr Scrips look like they are breaking my cell references for any sheet function that looks at cells they edit. How stop?
In cell V2 I have the function =DATEDIF(S2,U2,"D")
Script bellow (I know not how to format)
function onEdit(event) {
var eventRange = event.range;
var sheetName = SpreadsheetApp.getActiveSheet().getSheetName();
if (sheetName == "Scores") {
if (eventRange.getColumn() == 10) { //Check which is updated
var columnXRange = SpreadsheetApp.getActive().getSheetByName("Scores").getRange(eventRange.getRow(), 21, eventRange.getNumRows(), 21);//where to write
var values = columnXRange.getValues();
for (var i = 0; i < values.length; i++) {
if (!values[i][0]) { // If cell isn't empty
values[i][0] = new Date();
}
}
columnXRange.setValues(values);
}
}
}
Ok, I see the problem. You are looking at a way bigger range than you want with
var columnXRange = SpreadsheetApp.getActive().getSheetByName("Scores").getRange(eventRange.getRow(), 21, eventRange.getNumRows(), 21);
You only really need the value of one cell to check if it is empty. Try replacing your function with :
function onEdit(event) {
var eventRange = event.range;//makes shit happen?
var sheetName = SpreadsheetApp.getActiveSheet().getSheetName();//checks current shit
if (sheetName == "Scores") {//name of sheet want shit to happen
if (eventRange.getColumn() == 10) { // 1 is column A, 2 is B ect
// getRange(row, column, numRows, numColumns) sheet name to make not everywhere
var columnXRange = SpreadsheetApp.getActive().getSheetByName("Scores").getRange(eventRange.getRow(), 21, 1, 1);//num is where will write 1 is a ect
var values = columnXRange.getValues();//takes all shit from above to use as range
if (!values[0][0]) { // If cell isn't empty
values[0][0] = new Date();//set date to the vaules in the range
}
columnXRange.setValues(values); //use the values set above and write them in
}
}
}
..and that should fix your problem. The problem with your current script is that the script is copying the "value" of your column v cells and replacing it with just a text value. This limits the range you are grabbing to just the cell you need, eliminates the for() loop, and steps over the problem entirely.

setFormula then get and set Value - is this the best way?

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!
}
}

How to solve #N/A value using Google Sheet =importxml function

I have a sheet that want to import value value of Google search result using importxml function. Sometimes I get #N/A in cell A2.
I used while loop to keep trying to fetch the data and still I don't get data even if I wait for long time. Is it possible to setup a JavaScript timer so runs until I get value in cell A2 then timer stops and allow the rest of code to continue?
What should I do in order to avoid such cases and always get value on cell A2? Any Alternative solution?
var queryString = Math.random();
var cellFunction1 = '=IMPORTXML("' + SpreadsheetApp.getActiveSheet().getRange('C2').getValue() + '&randomNumber=' + queryString + '","'+ SpreadsheetApp.getActiveSheet().getRange('D2').getValue() + '")';
SpreadsheetApp.getActiveSheet().getRange('A2').setValue(cellFunction1);
var stop = 0;
while (SpreadsheetApp.getActiveSheet().getRange('A2').getValue() === "#N/A" && stop++<10) {
Utilities.sleep(5000);
var queryString3 = Math.random();
var cellFunction1 = '=IMPORTXML("' + SpreadsheetApp.getActiveSheet().getRange('C2').getValue() + '&randomNumber=' + queryString3 + '","'+ SpreadsheetApp.getActiveSheet().getRange('D2').getValue() + '")';
SpreadsheetApp.getActiveSheet().getRange('A2').setValue(cellFunction1);
}
I faced the same problem. I suspect it's because the code tries to .getValue from the cells before the cells' values are updated from IMPORTXML.
My workaround was to write two separate functions, one to force the IMPORTXML to refresh, and then the other to write the data into the sheet. Then I gave the IMPORTXML a trigger to run every 5 minutes, and then the write function with its own trigger.
You might want to try this:
function refresher() {
var ss = SpreadsheetApp.getActiveSheet();
var sheet = ss.getSheetById("input");
// Cell input!A2 holds the formula '=IMPORTXML(A1, XMLpath)'
//So now we give input!A1 a URL with a random ?number behind...
//...to force IMPORTXML to refresh
sheet.getRange(1, 1)
.setValue("http://www.urlWhereInfoResides.com/?" + Math.floor(Math.random() * 40));
}
^ Give refresher() a trigger to run every 5 minutes
function writer() {
var ss2 = SpreadsheetApp.getActiveSheet();
//assumes desired value sits in cell input!A2
var input = ss2.getSheetById("input")
.getRange(2, 1)
.getValue();
//adds input value into a new row in output sheet
var outputSheet = ss2.getSheetById("output");
var output = outputSheet.getRange(outputSheet.getLastRow() + 1, 1)
.setValue(input);
}
^ Give writer() a trigger to run every 5 minutes or whatever is needed