Google Spreadsheet Triggers - google-apps-script

I'm trying to create a trigger that activates on weekdays only and at a specific time, but I don't know what I'm doing wrong. Here is my code.
function createTriggers() {
var days = [ScriptApp.WeekDay.MONDAY, ScriptApp.WeekDay.TUESDAY,
ScriptApp.WeekDay.WEDNESDAY, ScriptApp.WeekDay.THURSDAY,
ScriptApp.WeekDay.FRIDAY];
var d = new Date();
var time = d.toLocaleTimeString();
if (time == '3:05:00 PM EDT') {
for (var i = 0; i < days.length; i++) {
ScriptApp.newTrigger(Lunch1)
.timeBased().onWeekDay(days[i])
.everyMinutes().create();
}
}
}

I'm guessing that you mean you want to make it so a function is triggered (runs) every weekday at a certain time of the day. Here's what I would do.
First, in the Script Editor:
Go to Resources - Current Project's Triggers
Click Add a new trigger
Select the function that you want to be triggered
Change the next box to "Time-driven"
Change the next box to "Hour timer"
Then change the last box to "every hour"
Then at the very beginning of your function, add this code:
var d = new Date();
if (d.getDay() == 6 || d.getDay() == 0) return;
// more info here: http://www.w3schools.com/jsref/jsref_getday.asp
That will stop the rest of the script from running if it's a Saturday or Sunday.
Then, say you want the script to only run at one specific time each weekday, you could set the time-driven trigger to "Every minute" and then add another if statement after the one above:
if (d.getHours() != 15 && d.getMinutes() != 5) return;
That will stop the rest of the script from running if it's not exactly 15:05 (3:05 PM).
This is definitely not the most efficient way to do this, but it works.
Also, this is worth mentioning: I'm not sure if there's anything that would prevent you from triggering the script every minute. Google does impose quotas for what your scripts can do, but I didn't see anything about how many times a script can be triggered in a day. Here's the chart: https://developers.google.com/apps-script/guides/services/quotas

You should do something like this:
function createTriggers(func, hour) {
var weekdays = [ScriptApp.WeekDay.MONDAY, ScriptApp.WeekDay.TUESDAY,
ScriptApp.WeekDay.WEDNESDAY, ScriptApp.WeekDay.THURSDAY,
ScriptApp.WeekDay.FRIDAY];
for (var dayIndex in weekdays) {
var day = weekdays[dayIndex];
ScriptApp.newTrigger(func).timeBased()
.onWeekDay(day).atHour(hour).create();
}
}
This code creates a trigger for weekdays in a specific time calling the func function.

Related

Alternative to using triggers to automatically send email for each calendar event created

I am trying to automate certain parts of my workflow for scheduling clients with Google Calendar. I've successfully managed to capture new/edited/deleted events in Google Apps Script using a trigger which detects changes and Calendar.event.list to sync those changes with a spreadsheet.
I create a new row, or edit an existing one, in my spreadsheet of all the clients. What I desire to do is three days before the appointment with the client, automatically generate a custom email with all of their details, to send them as a reminder regarding the appointment.
My plan was every time a new row was created in the Spreadsheet (when a new Calendar event was created), was to make a new email trigger. That trigger would execute code to create an email, with all of the clients info.
function createEmailTrigger(event) {
var today = new Date();
today.setHours(0,0,0,0); // Don't care about time
// Create Email Trigger three days before
const sendDaysBefore = 3;
var daysBefore = new Date(event.start);
daysBefore.setDate(daysBefore.getDate() - sendDaysBefore);
var trigger = ScriptApp.newTrigger('sendEmail')
.timeBased()
.at(daysBefore)
.create();
associateEventWithTrigger(trigger.getUniqueId(), event);
return trigger.getUniqueId();
}
associateEventWithTrigger connects the trigger id with the Calendar event. sendEmail would then create a new email with all of the client's info, which came from the Calendar event. When the trigger is executed, it deletes the trigger since it won't be used again.
All of this was working fine, as I was testing one Calendar event at a time. However, once I decided to sync all of this year's Calendar events, the script very quickly threw this error:
Exception: This script has too many triggers. Triggers must be deleted from the script before more can be added.
Apparently you can only have 20 triggers per user/script. This is very inconvenient, as I was expecting to create hundreds of triggers.
Therefore, I need to rethink how to go about doing this. Any suggestions? I appreciate it.
Proposed workaround
This script is designed to be run on a time-driven trigger that runs daily.
function sendReminderEmails() {
let file = SpreadsheetApp.getActive();
let sheet = file.getSheetByName("Sheet1");
let range = sheet.getDataRange();
let values = range.getValues();
// removing headers
values.shift()
values.forEach(row => {
let name = row[0]
let email = row[1]
let date = row[2]
// Date object representing time now
let now = new Date();
// helper variables
let second = 1000;
let minute = second * 60;
let hour = minute * 60;
let day = hour * 24;
// gets time to appointment in milliseconds
let timeToApp = date.getTime() - now.getTime()
if (timeToApp > 2 * day && timeToApp < 3 * day) {
MailApp.sendEmail(
email,
"Remember Your Appointment",
"Hello " + name + ",\nYou have an appointment coming up soon."
)
}
})
}
This is based on a sample spreadsheet like this:
So you would need to adapt it to your particular format.
Script walkthrough
It is based on the date object.
If your dates are stored as formatted dates in your spreadsheet, when you getValues, Apps Script will automatically pass them as Date object. Alternatively you can cast them as Date objects in Apps Script if needed.
The script starts off by getting all the values in the target sheet.
It initialized a new Date object that represents the time now.
It then goes through each row of the target sheet and gets a value for how long until the appointment.
If the value is between 2 days and 3 days, then an email is sent.
I Think you could make an script to search events every day , events that are 3 days ahead , select then and send email. So it will be just one script that will be triggeres every day, using the date trigger mode.

Search a named range between two times in Google Sheets and populate the difference

I'm trying to create a schedule heat map so that we can adjust staffing times and days. In order to make this as easy as possible, I've come up with a tabular structure that allows the user to input the employee's name, then select their shift start time and their shift end time from a drop-down, and then use checkboxes to indicate which days they will work, as shown:
The end result would be a heat map that counts the number of instances that a value exists in the range between the start time and end time, broken down by hour and by day. My original thought was to use COUNTIFS thusly: =COUNTIFS(Calculations!D:D, ">=9:00:00", Sheet9!D:D, "=TRUE") Where Calculations!D:D is the column of the selected Start Time, where ">=9:00:00" checks to see if the start time is greater than or equal to 9AM, and where Sheet9!D:D, "=TRUE" checks to see if the checkbox for that day is checked. So this example would check to see if someone is working at 9AM on Monday.
However, this didn't pan out since we're checking for any value greater than 9AM, and most employees won't be working more than 10 hours, so I'm getting false positives.
My next thought was to use a named range that would start at the Start Time value and then, if necessary, loop back through to the End Time (for example, if an employee started at 10PM and their shift ended at 7AM). Since this range would be dynamic (not all employees will work strictly 8 hours per day), I would need to check to see if a value exists within the range, however, I'm not sure how to A: Loop through or B: check to see if a value is in the dynamic range. I assume this will require Google Apps Script to pull off, but I'm not well-versed in it, and I've been beating my head against a wall trying to figure this out. Any help would be appreciated!
Oh, and here's a screenshot of the desired output, with a couple of values filled in:
I don't know if this is exactly what you wanted but I wanted to practice using times is Google Scripts so I had a crack at solving your problem. However, there are a couple of little bits I didn't have time to write that I've commented in the script.
If I've understood your problem correctly, you want to take a series of shifts life this:
And convert it into a heatmap like this:
(I've not done all your formatting, but I'm guessing you want to see how many people are available for every hour of the day)
In my code, I called the first sheet above "Roster" and the second sheet "RosterOverview" - note the capitalisation and lack of spaces.
Approach
I wrote two scripts. One (rostering) to check which days a staff member worked and a second to see the hours they worked and update the values in RosterOverview (updateCalendar).
For each member of staff, I took their working hours and working days. I checked to see if they worked on a certain day and, if they did, sent their working hours to a script called updateCalendar. This second script then looked to see if they worked a night shift i.e. over midnight or not. The script then adds a 1 to each hour window a staff member is rostered for.
The scripts
function rostering() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var rosterSheet = ss.getSheetByName("Roster");
var overviewSheet = ss.getSheetByName("RosterOverview");
var range = rosterSheet.getRange(2,2,4,9); //You will need to work out how to accurately get this range for your data - 2 and 2 are the starting row and column respectively i.e. B2; 4 indicates the number of rows; 9 indicates the number of columns
var times = range.getValues();
var calendarRange = overviewSheet.getRange("B2:I25"); //On RosterOverview, this is the range of cells for Monday-Monday, midnight-2300. You need 8 columns of data because the extra one is for Sunday night overflowing into Monday morning.
//You will need to write a bit of code yourself to make sure all the values in calendarRange are set to 0. I have set this manually and pull them below when I was testing.
var calendarValues = calendarRange.getValues();
//This section loops through each staff member's hours and days.
//It looks to see if the check box is ticked for each day of the week in turn.
//If a checkbox is ticked, it calls a second script: updateCalendar
//It's probably possible to write a separate loop to go through the days of the week
for (var i = 0; i < range.getHeight(); i++){
//if Monday shift
if(times[i][2] == true){
calendarValues = updateCalendar(times[i][0], times[i][1], calendarValues, 0)
}
//if Tuesday shift
if(times[i][3] == true){
calendarValues = updateCalendar(times[i][0], times[i][1], calendarValues, 1)
}
//if Wednesday shift
if(times[i][4] == true){
calendarValues = updateCalendar(times[i][0], times[i][1], calendarValues, 2)
}
//if Thursday shift
if(times[i][5] == true){
calendarValues = updateCalendar(times[i][0], times[i][1], calendarValues, 3)
}
//if Friday shift
if(times[i][6] == true){
calendarValues = updateCalendar(times[i][0], times[i][1], calendarValues, 4)
}
//if Saturday shift
if(times[i][7] == true){
calendarValues = updateCalendar(times[i][0], times[i][1], calendarValues, 5)
}
//if Sunday shift
if(times[i][8] == true){
calendarValues = updateCalendar(times[i][0], times[i][1], calendarValues, 6)
}
}
//Once ithas looped through al the staff member's shifts, it updates the RosterOverview
//sheet with how many people are on each hour
calendarRange.setValues(calendarValues);
}
function updateCalendar(startTime, endTime, roster, day){ //Let day be a digit: 0 for Monday, 1 for Tuesday.... Roster is the calendarValues array from the roster function
if(startTime > endTime){ //night shift over midnight
for (var j = startTime.getHours(); j < 24; j++){
roster[j][day] = roster[j][day] + 1;
}
for (var j = 0; j < endTime.getHours(); j++){
roster[j][day + 1] = roster[j][day + 1] + 1;
}
} else {
for (var j = startTime.getHours(); j < endTime.getHours(); j++){
roster[j][day] = roster[j][day] + 1;
}
}
return roster;
}
What you need to do
It's probably best to add a custom menu so you can run it from the sheet: see this Fundamentals codelab to learn how: https://developers.google.com/codelabs/apps-script-fundamentals-3#0
You need to make sure your Google Sheet and the Google script are set to the same timezone. To set the timezone for the script, from the script editor you want to click on [Use legacy editor] in the top right hand corner then go to File > Project properties. Once set, return to the new editor.
You will need to write a little bit of code that selects the range of data for your staff members' shifts and a bit of code that resets the RosterOverview spreadsheet values to 0s before you run the script again.
Let me know if you have any problems.

How to set timer for a row in google sheets

I would like to set a timer for an entire row in google sheets where a user can start entering data in the second row only after a certain time after starting row one.
Example: If a user starts filling cells in row 1 then they should be able to fill the data in the second only after the timer ends.
Could anyone suggest me how to get started or suggest me a chrome extension for this use?
You could also suggest me on how to build the chrome extension I can try it along with my colleagues.
This function uses an onEdit trigger to impose a 20 second delay between editing rows. It may not be exactly what you want but perhaps it's a start. It uses PropertiesService to keep state. I think user properties would be a better choice but script properties are easier to develop with since you can modify them directly in the script editor.
function onEdit(e) {
const sh=e.range.getSheet();
const delay=20000;
let ms=Number(new Date().valueOf()).toFixed();
if(sh.getName()=='Sheet10') {
const ps=PropertiesService.getScriptProperties();
let dObj=ps.getProperties();
if(dObj.hasOwnProperty('row') && dObj.hasOwnProperty('delay')) {
if(dObj.row!=e.range.rowStart && Number(ms-dObj.delay)<delay) {
e.range.setValue(e.oldValue);
e.source.toast('Sorry you have ' + (delay-Number(ms-dObj.delay))/1000 + ' seconds left.');
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}else{
ps.setProperties({'row':e.range.rowStart,'delay':ms});
}
}
}
Issue with Protections:
Class Protection is commonly used to protect ranges from being edited. It is not appropriate for your situation, though, because, as specified here, users who are executing the script cannot remove themselves from the list of editors:
Neither the owner of the spreadsheet nor the current user can be removed.
Using oldValue:
Because of this, the best way to go would be to use the parameter oldValue from the onEdit event object.
An onEdit trigger runs every time a user edits a cell. In it, you can use:
PropertiesService to store useful information: (1) whether it is the first time row 1 is edited (isNotFirstTime), and (2) when was last time first row was edited (startTime).
Event object to get information on the edited cell (its row, its old value, etc.).
You can do something along the following lines (check comments):
function onEdit(e) {
var current = new Date(); // Current date
var range = e.range;
var editedRow = range.getRow();
var sheet = range.getSheet();
var props = PropertiesService.getScriptProperties();
var waitingTime = 20 * 1000; // 20 seconds
var isNotFirstTime = props.getProperty("isNotFirstTime"); // Check if first row was previously edited
var startTime = new Date(props.getProperty("startTime")); // Time when first row was first edited
if (editedRow === 1 && !isNotFirstTime) { // Check that (1) edited row is first one, (2) it was not edited before
props.setProperty("startTime", current.toString()); // If it's first time first row was edited, store current time
Utilities.sleep(waitingTime); // Wait for 20 seconds
props.setProperty("isNotFirstTime", true); // Store: first row was previously edited
}
// Check that (1) second row edited, (2) Less than 20 seconds passed since first time first row was edited:
if (editedRow === 2 && (current - startTime) < waitingTime) {
range.setValue(e.oldValue || ""); // Set previous value to edited cell (this avoids editing cells)
}
}
Reference:
onEdit(e)
onEdit Event object
Class PropertiesService

Clock Trigger Builder Not calling function when scheduled - Google sheets app Script

I am using the app script provided by Google to access their prediction API through sheets. I am trying to predict thousands of rows at once, however, after 6 minutes the maximum execution time is reached at the code stops.
I implemented a solution that I found using clock trigger builder. Once I run the function it goes for 5 mins, then it stops sets a trigger to recall the function within 2 mins.
The major problem is that the function is not called when scheduled. I see it in the current triggers list, but it never gets called again. Can you please explain why this is occurring.
My intention is to predict as many lines as possible in 5 min then stop set a trigger to call the predict function again within a few minutes start where it left off and continue until ever element has been predicted.
I also need to know how would I store then values in cache so that it would know all the information that it needs when the function is called again.
//This is the function that is used to predict a selection of data
function predict() {
try {
clearOutput();
var startTime= (new Date()).getTime();
var sheet = SpreadsheetApp.getActiveSheet();
var selection = sheet.getActiveSelection();
var instances = selection.getValues();
var project_number = getProjectNumber();
var model_name = getModelName();
var startRow = stRow;
var MAX_RUNNING_TIME = 300000;
var REASONABLE_TIME_TO_WAIT = 60000;
for (var i = startRow; i < instances.length; ++i) {
var currTime = (new Date()).getTime();
if(currTime - startTime >= MAX_RUNNING_TIME) {
var builder = ScriptApp.newTrigger('predict').timeBased().after(REASONABLE_TIME_TO_WAIT);
builder.create();
break;
} else {
var result = predictSingleRow(project_number, model_name, instances[i]);
selection.getCell(i + 1, 1).setValue(result);
}
}
} catch(e) {
Browser.msgBox('ERROR:' + e, Browser.Buttons.OK);
}
}
Few things as to why your code is not functioning as intended:
1) Since you mentioned,"I see it in the current triggers list, but it never gets called again" and looking at your code, I am unsure whether you intended to call the function again after it's execution has completed. If you do, this is because your for loop runs for a while until the length of the instances is obtained. Nothing in the script suggests that the function needs to be run again once it has finished iterating through instances. Refer to this link to see how to Manage Trigger Programmatically.
2) var builder = ScriptApp.newTrigger('predict').timeBased().after(REASONABLE_TIME_TO_WAIT);
This line of your code falls under an if condition which stops the execution for 1 minute (value is 60000). Hence, adding 1 minute to the time since execution started. Nowhere are you resetting the startTime counter to the time after the waiting time since once the value of currTime - startTime has exceeded MAX_RUNNING_TIME, the function will keep calling the if loop for all iterations of the for loop after that. Simply put, if startTime was 9:35 and currTime was 9:40, after waiting for 1 minute the currTime is 9:41 which is still more than the MAX_RUNNING_TIME(5 minutes) because value of startTime still remains 9:35. Resetting it to 9:41 at this point should resolve your problem.
3) Loosing the break in the if loop would probably help fix that as well.
EDIT:
Add a function as shown in the link I mentioned above:
function callTrigger(){
ScriptApp.newTrigger('predict')
.timeBased()
.everyMinutes(30)
.create();
}
Run the function callTrigger once from your editor and you should be good to go. Remember, for minutes you can only pass values 1,5,15 or 30.

Timed script not working

I have a google script that I would like to run automatically every weekday at 8:11 am. I have set my time zones to make sure that everything is correct, but it never seems to work correctly. I am still weak at scripting. Does anyone see where I might have error in this script?
function myFunction(){
try {
var d = new Date();
if (d.getDay() == 6 || d.getDay() == 0) return;
if (d.getHours() != 08 && d.getMinutes() != 11) return; // This will stop the script from running unless it is 8:11am
} catch (e) {
MailApp.sendEmail("pthompson#ucc.on.ca", "Error report", e.message);
}
}
Thank you,
Paul
To run a function based on time, setup a project trigger (under Resources tab) with the Event "Time-driven". You can even setup a notification.
If you wanted to manage this trigger programmatically you could do something like this:
function createTimeTrigger() {
// Once a day at 8AM, near minute 11 for function 'writeSomething()'
var dailyHourNearMinute = ScriptApp.newTrigger("writeSomething")
.timeBased()
.everyDays(1)
.atHour(8)
.nearMinute(11)
.create();
}
You will notice that this creates a trigger for you that can be viewed under 'Current project's triggers'. It will say between 8AM-9AM. As far as google time events, they work well at getting it within the hour but not on the exact minute. The load in the system can also throw this off. So, the best you can really plan for is, between 8AM - 9AM.