Replicate formula with integer and referencing cell above current row - google-apps-script

I am trying to replicate the following formula in an if function in google apps script for a spreadsheet.
=IF(AND((G2/(1/96)/(INT(G2/(1/96))))=1,B2<>B1),F2, "")
Populating the cell in the D column. I am trying to get it to work so that it goes through each row and works applies this formula.
I think where I am struggling to replicate the formula is how to make it an integer and how to get the script to read the cell in the row above.
Any help would be much appreciated! See below for my rudimentary attempt at this.
function releaseTimes() {
var sheet = SpreadsheetApp.getActiveSheet();
var startRow = 2;
var numRows = 249;
var dataRange = sheet.getRange(startRow, 1, numRows, 16)
var data = dataRange.getValues();
for (var i = 0; i < data.length; ++i) {
var row = data[i];
var startTime = row[1];
var quarterHour = (1/96);
var committedSeats = (startTime-halfHour);
var startTime1 = sheet.getRange(startRow + i, 2);
var startTime2 = sheet.getRange(startRow + i-1, 2);
var G2 = (startTime2-quarterHour);
var G2divided = (G2/quarterHour);
var int = parseInt(G2divided);
var firstCondition = G2divided/int;
if ( firstCondition = 1 && startTime2 != startTime1) {
sheet.getRange(startRow + i, 4).setValue(committedSeats);
} else {
sheet.getRange(startRow + i, 4).setValue(blank);
}
}
}

The spreadsheet function you've provided:
=IF(AND((G2/(1/96)/(INT(G2/(1/96))))=1,B2<>B1),F2, "")
Can be expressed like this:
IF (G2 is a time ending on the quarter hour) AND (B2 is not equal to B1)
THEN
DISPLAY F2
ELSE
DISPLAY nothing
ENDIF
The magic number 96 appears because there are 96 quarter hours in a 24 hour period. The code would be much clearer if it just dealt with time, like this:
var firstCondition = (startTime.getMinutes() % 15 == 0); /// BOOLEAN!
There's some confusion in your question, though. The spreadsheet formula refers to column "G", Release all Holds, for a 15-minute check. Your script, though, uses column "B", Start time, for that. Looking at your example data, there are rows like "Show 9" where the start time is a "15" while release-all is not, so you'll get different results depending on which of those two calculations is "correct". I'll assume that your script is correct, and that all times are relative to the start time.
One last point... in your script, you mix two methods of manipulating range data. You start into the (more efficient) javascript method, with var data = dataRange.getValues();, but then return to touching individual cells with statements like sheet.getRange(). That's confusing, since the first uses 0-based indexes while spreadsheets use 1-based. If possible, pick one way, and stick with it.
Script
Here's an updated version of your script. The spreadsheet data is accessed just twice - it's read and written in bulk, saving time vs cell-by-cell updates. All times are calculated relative to startTime, something you can change if you wish. Finally, javascript Date object methods are used for all time calculations, avoiding the use of magic numbers.
function releaseTimes() {
var sheet = SpreadsheetApp.getActiveSheet();
var headRows = 1; // # header rows to skip
var dataRange = sheet.getDataRange();
var data = dataRange.getValues();
// Handy time definitions
var minute = 60 * 1000; // 60 * 1000 ms
var quarterHour = 15 * minute;
var halfHour = 30 * minute;
var hour = 60 * minute;
// Process data, skipping headRows
for (var i = headRows; i < data.length; ++i) {
var row = data[i];
var startTime = row[1]; // Assume all times are relative to startTime
var prevStartTime = data[i-1][1];
// Test whether startTime is on a quarter-hour
var firstCondition = (startTime.getMinutes() % 15 == 0);
// Committed blank unless unique quarter hour start time
// NOTE: You can't compare javascript time objects using ==
// See: http://stackoverflow.com/a/7961381
if ( firstCondition && (prevStartTime - startTime) != 0) {
row[3] = new Date(startTime.getTime() - halfHour); // Monitor Committed Seats. Return to sale orders that
} else { //have been in the basket for over 1 hour.
row[3] = '';
}
row[4] = new Date(startTime.getTime() - hour); // Release Press Holds bar 2
row[5] = new Date(startTime.getTime() - halfHour); // Release all Company Holds. Release Venue Holds bar 2
row[6] = new Date(startTime.getTime() - quarterHour); // Release all remaining holds
}
// Write out updated information
dataRange.setValues(data);
}
Result
As you can see, the result is a bit different from your initial example, because of the decision to base decisions on startTime.

Related

Finding the Date that matches the condition in a loop of another column?

I have a google sheet which contains multiple columns, one of the column is date (it is column F and is sorted from oldest to latest)and another column(G) is time duration values. I made a condition whenever a combination of three durations is taken if it is greater than 10 hours, the respective combination is took and the oldest date in the combination is taken as reference to another program. This combination should be of the latest one ( I mean by date). The code I have tried is
function calculation(){
var source = SpreadsheetApp.getActiveSpreadsheet();
var sheet = source.getSheetByName("10017135ASR");
var lastRow = sheet.getLastRow();
var time = sheet.getRange('G1:G'+lastRow).getValues();
var cell = sheet.getRange('F1:F'+lastRow).getValues();
var range1 = sheet.getRange('H1:H'+lastRow).setValues(time).setNumberFormat('[hh]:mm');
var refer = sheet.getRange('N2').setValue("10:00").setNumberFormat("[hh]:mm");
var refer1= refer.getValue();
var range = range1.getValues();
var time = sheet.getRange('H1:H');
time.setNumberFormat('[hh]:mm');
sheet.getRange('I2:I'+lastRow).setValue('0').setNumberFormat('[hh]:mm');
var one= sheet.getRange('H'+(lastRow)).getValue();
sheet.getRange('I'+lastRow).setValue(one).setNumberFormat('[hh]:mm');
var two = sheet.getRange('H'+(lastRow-1)).getValue();
sheet.getRange('I'+(lastRow-1)).setValue(two).setNumberFormat('[hh]:mm');
loop1:
for (var i = lastRow-3; i>0; i--){
var value1 = range[i][0];
var V2 = sheet.getRange('K'+lastRow).setValue(value1).setNumberFormat("[hh]:mm");
var L2 = sheet.getRange('J'+lastRow).setFormula("=LARGE(I2:I,1)").setNumberFormat('[hh]:mm');
var L3= sheet.getRange('J'+(lastRow-1)).setFormula("=LARGE(I2:I,2)").setNumberFormat('[hh]:mm');
var sum = sheet.getRange('M2').setFormula("=SUM(J2:J)").setNumberFormat("[hh]:mm");
var vB= sum.getValue();
var V1 = V2.getValue();
var sum2 = vB + V1;
if((vB)>= refer1){
sheet.getRange('L2').setValue(sum2).setNumberFormat('[hh]:mm');
var da=sheet.getRange('F'+(i+2)).getValue();
sheet.getRange('N2').setValue(da).setNumberFormat('DD-mm-yyyy');
break loop1;
}
else {
sheet.getRange('I'+(i+1)).setValue(value1).setNumberFormat('[hh]:mm');
}
}
}
Here I am not getting output,the loop ends at first row, Even though there is no need to go the first row.Since the required output is at row number 30 and the respective date is 30/7/2020.
The worksheet is shown below
The results obtained is
Working with durations:
Working with sheets durations in Apps Script is a bit messy: see Working With Durations In Google Apps Script.
When retrieved via Apps Script, a duration becomes a JavaScript Date, based on the zero mark for date/time in Google Sheets (12/30/1899 0:00:00) (see this answer).
In JS, in order to compare the dates, you can use Date.getTime(), which returns the number of milliseconds since the Unix Epoch, which refers to 1 January 1970. Therefore, a date retrieved from a sheet duration will result in a negative number of milliseconds. In order to avoid problems with this, you can use a reference date every time you compare dates. One option would be to set a sheets cell to 00:00 duration and use that value as reference. That's what I did in the sample below, in cell N3.
Workflow:
Set N3 to the reference duration (00:00) and N2 to 10:00.
Loop through all the durations in G2:G, using an array (comboData) to store the possible combination of durations. For each element, (1) check if comboData has less than 3 elements, and add current element if that's the case, (2) otherwise, find the minimum in comboData, check if current element is higher, and replace if that's the case, (3) calculate total duration of current combination, and break loop if it's higher than 10:00.
Transform the duration from milliseconds to your desired duration format (using the function msToHMS(ms) below, based on this answer by Serge insas), and write it to the sheet, using setValue() for the total duration, and setValues() for the three durations and dates.
Code sample:
function getCombinationRowIndex() {
var source = SpreadsheetApp.getActiveSpreadsheet();
var sheet = source.getSheetByName("10017135ASR");
var lastRow = sheet.getLastRow();
var reference = sheet.getRange('N3').setValue('00:00').setNumberFormat('[hh]:mm').getValue().getTime();
var sourceData = sheet.getRange('F2:G' + lastRow).getValues().reverse().map(row => [row[1].getTime() - reference, row[0]]);
var limit = sheet.getRange('N2').setValue("10:00").setNumberFormat("[hh]:mm").getValue().getTime() - reference;
var comboData = []; // Array to store the combination
var totalDuration = 0;
for (var i = 0; i < sourceData.length; i++) {
var row = sourceData[i];
if (comboData.length < 3) comboData.push(row); // If combination has less than 3 elements, add current element
else {
var minimum = Math.min(...comboData.map(val => val[0])); // Find minimum in current combination
var index = comboData.findIndex(val => val[0] == minimum); // Find index for minimum in current combination
if (row[0] > minimum) comboData[index] = row; // Replace minimum if current element is higher
}
totalDuration = comboData.reduce(((acc, combo) => acc + combo[0]), 0); // Total duration in milliseconds
if (totalDuration > limit && comboData.length === 3) break; // If total duration is higher than 10 hours, break loop
}
sheet.getRange("K2:L4").setValues(comboData.map(combo => [msToHMS(combo[0]), combo[1]])); // Write combo data to K2:L4
var formattedDuration = msToHMS(totalDuration); // Total duration (formatted hh:mm:ss)
sheet.getRange("J2").setValue(formattedDuration); // Write total duration to J2
}
function msToHMS(ms) {
var seconds = ms / 1000;
var hours = parseInt( seconds / 3600 ); // 3,600 seconds in 1 hour
seconds = seconds % 3600; // seconds remaining after extracting hours
var minutes = parseInt( seconds / 60 ); // 60 seconds in 1 minute
seconds = seconds % 60;
return Utilities.formatString("%02d",hours) + ':' + Utilities.formatString("%02d",minutes) + ':' + Utilities.formatString("%02d",seconds);
}
Notes:
Only the data in columns F, G and N is needed for this sample. No need for H, I, J, K, M, etc.

Why does my spreadsheet script throw a TypeError when I call getTime()?

I am writing a small function using the Google Apps Script editor that checks that an event date in a spreadsheet is within 12 hours of the current date to send me a morning email notification. The for loop below loops through the dates column in my spreadsheet to find the current date (entered in the spreadsheet formatted as a date expressed as MM/DD/YYYY). I can't figure out why schedule[i][0].getTime() is giving me the following error:
TypeError: Cannot find function getTime in object .
Here's the function:
function sendEmail() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Schedule"); // get sheet
var lastRow = spreadsheet.getLastRow(); // find length of sheet
var schedule = spreadsheet.getRange(6, 1, lastRow, 5).getValues(); // this an array with dates and info on events (first column has dates)
var now = new Date().getTime(); // get today's date in ms
var MILLIS = 1000 * 60 * 60 * 12 // get number of milliseconds in 12 hours
for (var i=0; i <= lastRow; i++) {
var eventDate = schedule[i][0].getTime()
if (now - eventDate < MILLIS && now > eventDate) {
...send email...
};
};
};
I've checked to make sure that the schedule[i][0] object is a valid date (e.g. myvar instanceof Date, debugger, format in google sheet, etc.), and everything indicates that it is. Yet, any method I've tried that nests the if clause in a function or calls getTime() more than once in the if statement causes a TypeError.
What am I doing wrong that is causing this error? How can I edit my code so that the condition in my if statement only runs if the difference between now and the date in schedule[i][0] is less than 12 hours?
Here's a link to a spreadsheet showing how the data is formatted.
Thanks for your help!
Edit: I edited the question to match the "minimal reproducible example" format. The problem is with the for loop, which doesn't account for the header. Thanks to tehhowch for the fix.
This works on your data.
Using your data and a few minor tweaks to the code. I also used valueOf() instead of getTime() ...it's some thing I prefer using.
function sendEmail() {
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName("Sheet177");
var schedule = sh.getRange(6, 1, sh.getLastRow(), 1).getValues(); // this an array with dates and info on events (first column has dates)
var halfday = 1000 * 60 * 60 * 12;
var idxA=[];
for (var i=0;i<schedule.length;i++) {
var now=new Date().valueOf();
var sked=new Date(schedule[i][0]).valueOf();
var diff=now-sked;
if ((diff<halfday) && (now>sked)) {
idxA.push(i);//I am just collecting the indexes of the var schedule that are within 12 hours of now.
//send email
}
}
Logger.log(idxA);
}
Per tehhowch, the problem is that my for loop did not account for the header rows in my spreadsheet or that an array is 0-indexed.
Removing the empty rows in the array and giving the right limit in the for loop fixed the problem:
...
function sendEmail() {
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Schedule"); // get active sheet
var lastRow = spreadsheet.getLastRow(); // find length of sheet
var schedule = spreadsheet.getRange(6, 1, lastRow - 5, 5).getValues(); // get values from first column of dates
var now = new Date().valueOf(); // get today's date
var MILLIS = 1000 * 60 * 60 * 12 // get number of ms in 12 hours
for (var i=0; i <= schedule.length - 1; i++) {
var eventDate = schedule[i][0].getTime()
if (now - eventDate < MILLIS && now > eventDate) {
...send email...
};
};
};

Delete Cells Based on Date

I need a help with a cell-deletion script. In general, I want to run a reset script that clears out all of the data up to the day I run it. Because I am statically inputting values into those cells that are matching up with information from a filter, I believe I need to delete those cells to properly line up my inputs with where the filter information will be after I delete the expired rows from the exporting page.
Here's what I want to do in my script: If the Column F value < today's date, then delete the cells in I, J, and K and shift the cells below them up. I think I found code to do this, but it takes so long to run that the program times out before it can get through more than a few rows. I will use a for loop to run it over 73 pages, so if it is lagging out on one...yeah, I need help!
function deleteEntries() {
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var datarange = ss.getDataRange();
var lastrow = datarange.getLastRow();
var values = datarange.getValues();
var currentDate = new Date();
for (i = lastrow; i >= 5; i--) {
var tempdate = values[i-1][5];
if (tempdate < currentDate)
{
ss.getRange(i-1, 8).deleteCells(SpreadsheetApp.Dimension.ROWS);
ss.getRange(i-1, 9).deleteCells(SpreadsheetApp.Dimension.ROWS);
ss.getRange(i-1, 10).deleteCells(SpreadsheetApp.Dimension.ROWS);
}}}
In accordance with Apps Script "best practices", you will want to limit the use of the Spreadsheet Service to improve execution times. There are two "immediate" optimizations that can be considered:
Delete more than 1 cell at a time in a row
To do this, simply select a 1-row x 3-column range: ss.getRange(i-1, 8, 1, 3) instead of selecting (i-1, 8), (i-1, 9), (i-1, 10) and calling deleteCells on each of the three Ranges.
Sort your sheet before deleting such that only 1 delete call is necessary (e.g. the C++ stdlib "erase-remove" idiom). If your data is sorted based on column F, such that all data that should be removed is at the end, then you simply need to iterate the in-memory array (a very fast process) to locate the first date that should be removed, and then remove all the data below & including it.
An implementation of option 2 would look like this (I assume you use frozen headers, as they do not move when the sheet or range is sorted).
function sortDescAndGetValuesBack_(s, col) {
return s.getDataRange().sort({column: col, ascending: false}).getValues();
}
function deleteAllOldData() {
const sheets = SpreadsheetApp.getActive().getSheets()
.filter(function (sheet) { /** some logic to remove sheets that this shouldn't happen on */});
const now = new Date();
const dim = SpreadsheetApp.Dimension.ROWS;
sheets.forEach(function (sheet) {
var values = sortDescAndGetValuesBack_(sheet, 6); // Col 6 = Column F
for (var i = sheet.getFrozenRows(), len = values.length; i < len; ++i) {
var fVal = values[i][5]; // Array index 5 = Column 6
if (fVal && fVal < now) { // if equality checked, .getTime() is needed
console.log({message: "Found first Col F value less than current time",
index: i, num2del: len - i, firstDelRow: values[i],
currentTime: now, sheet: sheet.getName()});
var delRange = sheet.getRange(1 + i, 8, sheet.getLastRow() - i, 3);
console.log({message: "Deleting range '" + sheet.getName() + "!" + delRange.getA1Notation() + "'"});
delRange.deleteCells(dim);
break; // nothing left to do on this sheet.
}
}
console.log("Processed sheet '" + sheet.getName() + "'");
});
}
References:
Array#filter
Array#forEach
Range#sort
Range#deleteCells

Send email from G-Sheets after 4 weeks

I am using Google Sheets, and I'm trying to send an email to myself, after 4 weeks of a specified date, with information in the body of that email extracted from a certain cell. The cell is in the same row (Col B) that the date is in (Col Q). I have successfully figured out the timing issue, but I just can't get the info from the cell to go into the body of the email.
Here is the code I have.
function myFunction() {
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getRange(9,17,31).getValues();
var now = Date.now();
for (var i = 0; i < data.length; i++) {
var row = data[i];
var date = new Date(row[0]);
var remind_date = new Date(date.getTime() + 28 * 24 * 60 * 60 * 1000);
var diff = now - remind_date;
var case_name = row[2];
if ((diff >= 0) && (diff < 24 * 60 * 60 * 1000)) {
GmailApp.sendEmail('myemail#me.com', 'Case Inquiry Reminder','Reminder, inquire about status of ', case_name);
}
}
}
First part
On the question it's mentioned that the date is on column Q but the code is getting column A instead.
var date = new Date(row[0]);
Maybe the correct code is
var date = new Date(row[16]);
While some Spreadsheet Service methods use 1 based index, JavaScript arrays use zero based index. Because of this, the index of column Q is 16.
Second part
For the same reason mentioned previously regarding JavaScript indexes, as you said that you need the value of column B, instead of
var case_name = row[2];
the code should be
var case_name = row[1];
Reference
Array
Third part
There is a problem on the following line
GmailApp.sendEmail('myemail#me.com', 'Case Inquiry Reminder','Reminder, inquire about status of ', case_name);
It uses four arguments but the the fourth isn't of the proper type. Maybe you intend to write
GmailApp.sendEmail('myemail#me.com', 'Case Inquiry Reminder','Reminder, inquire about status of ' + case_name);
(replace the last comma , by a plus sign +)
Reference
sendEmail(recipient, subject, body, options)

Google Apps Script Spreadsheet Range Processing (Get-Process-Set; Set Not Working)

Once again I'm back to SO for GAS problems because I'm not too familiar with Javascript/GAS yet. I'm having a bit of trouble with how slowly a script is running based on the method of handling function calls in an efficient manner.
I've read in several places (ah, it was here), that doing a "read-all" then "write-all" for getting-parsing-setting values (in Spreadsheets at least) is faster than doing a "get-one, write-one" method (for obvious reasons, this makes sense).
I know how to get all the values in a Range, but I'm not really sure how to go through the (multidimensional) array, process the data, and set a new Range accordingly.
The problem: The function takes about 2 seconds to start up, and takes about 5 seconds to run for 50 rows. The problem is, I will likely have thousands of rows in this Spreadsheet, and to have this function run to ensure that data is post-processed correctly so that Boomerang Calendar picks up the time data correctly is a tad ridiculous in my opinion. The script takes twice as long for every column of data needed to process, which is horrendous. I fear that during the next school semester I will often be going over my "GAS processing time limit."
Here is the starting code (bad, I admit):
function fixApostrophes() {
// Get the active spreadsheet to run the script on
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
// Get the active sheet within the document to run the script on
var sheet = spreadsheet.getActiveSheet();
// Get max number of rows needed to process
var maxRows = sheet.getLastRow();
// Get the range for the startTime column and endTime column
var apostropheRange = sheet.getRange(1, 10, maxRows, 2);
// Get the value in each cell, remove apostrophes from the start,
// and replace the value in that cell
for (var i = 1; i < apostropheRange.getNumRows(); ++i) {
// Get the cells for startTime and endTime to speed things up a bit
var startCell = apostropheRange.getCell(i, 1);
var endCell = apostropheRange.getCell(i, 2);
// Get the values for startTime and endTime
var startTime = startCell.getValue();
var endTime = endCell.getValue();
// Remove apostrophes from start of startTime
while(startTime.charAt(0) == "'") {
startTime = startTime.substring(1);
}
// Remove apostrophes from start of startTime
while(endTime.charAt(0) == "'") {
endTime = endTime.substring(1);
}
// Set the values for startTime and endTime
startCell.setValue(startTime);
endCell.setValue(endTime);
}
}
This is highly related to my previous question about fixing a time-format apostrophe issue that was breaking Boomerang Calendar functionality for scheduling events.
The solution: Pull the values from a Range into a 2D array using the Range.getValues() function. Process each value in the 2D array (row-by-row is probably the most logical method), then update the index of the edited value with the new value (see Srik-answer comments in code). After that, put the elements in the 2D array back into the original Range by using Range.setValues(2Darray). I hope this helps you if you came across it! This is also much faster than calling the API multiple times as seen in my original code.
function myNewLibraryFunction(startCol, numColumns) {
// Get the active spreadsheet to run the script on
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
// Get the active sheet within the document to run the script on
var sheet = spreadsheet.getActiveSheet();
// Get max number of rows needed to process
var maxRows = sheet.getLastRow();
// Get the range for the startTime column and endTime column
var dataRange = sheet.getRange(1, startCol, maxRows, numColumns);
var values = dataRange.getValues();
// Get the value in each cell, remove apostrophes from the start,
// and replace the value in that cell
for (var i = 1; i < maxRows; ++i) {
// Get the values for startTime and endTime
var startTime = values[i][0];
var endTime = values[i][1];
// Remove apostrophes from start of startTime
while(startTime.charAt(0) == "'") {
startTime = startTime.substring(1);
}
// Remove apostrophes from start of startTime
while(endTime.charAt(0) == "'") {
endTime = endTime.substring(1);
}
values[i][0] = startTime; // New stuff from Srik's answer
values[i][1] = endTime; // New stuff from Srik's answer
}
dataRange.setValues(values);
SpreadsheetApp.flush(); // New stuff from Srik's answer
}
Basically, most of the function calls that you make to the APIs listed in GAS will require more time than regular JavaScript. In your case, reduce the number of calls to Range and Sheet classes
function fixApostrophes() {
// Get the active spreadsheet to run the script on
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
// Get the active sheet within the document to run the script on
var sheet = spreadsheet.getActiveSheet();
// 1. Replace two calls and many other calls later with one.
var dataRange = sheet.getDataRange();
var values = dataRange.getValues();// a 2D array
// Get the value in each cell, remove apostrophes from the start,
// and replace the value in that cell
for (var i = 1; i < values .length ; ++i) {
// Get the values for startTime and endTime
var startTime = values [i][0];
var endTime = values [i][1];
// Remove apostrophes from start of startTime
// These are okay. Regular Javascript - not time consuming
while(startTime.charAt(0) == "'") {
startTime = startTime.substring(1);
}
// Remove apostrophes from start of startTime
while(endTime.charAt(0) == "'") {
endTime = endTime.substring(1);
}
}
dataRange.setValues(values);
}
TIP: You can see how much time each call took in the Execution transcript