Calculating tardies using google script - google-apps-script

I am trying to create an attendance system that calculates tardies based on certain criteria. First off, each student member has a row dedicated to them in the spreadsheet. (Ie, student1's attendance is found on row 3, student2's attendance is found on row 4, etc.). Each student has personal information in the first few columns. Then each student has an attendance record for the meeting. All attendance information comes in pairs with a sign in time and a sign out time.
A student receives a tardy if they show up after 6:30 PM. They also receive a tardy if they are present for less than 2 hours. So I wrote a code to calculate the number of tardies.
/**
each student has one row dedicated to their attendance. Sign in and sign out are always in pairs. I will ensure that the range
begins with range[0][0] being the first sign in time.
#param range the row for the student's attendance
#return the total tardies for each student
**/
function calculateTardies(range){
var search = range[0].length;
var tardies = 0;//initially set to 0
for(k = 0; k<search; k+2){ //go through the entire row of the student's sign in and sign out times (k+2 because they always come in pairs)
var timeIn = new Date(range[0][k]); //sign in time will always be at a K value
var date = Utilities.formatDate(timeIn, "EST", "EEE"); //get the day of week that time stamp is
var timeOut = new Date(range[0][k+1]); //sign out time is to the right of the sign in time
if(date == "Tue" || date == "Wed" || date == "Thu"){ //they only need to be there for 2 hours Tues-Thurs
var checkHour = Number(Utilities.formatDate(timeIn, "EST", "HH")); //The hour student arrived
var checkMin = Number(Utilities.formatDate(timeIn, "EST", "mm")); //The minute student arrived
var outHour = Number(Utilities.formatDate(timeOut, "EST", "HH")); //hour student left
var outHour = Number(Utilities.formatDate(timeOut, "EST", "HH")); // min student left
//If the student wasn't present for at least 2 hours, add a tardy
if((checkHour+2)*60+checkMin < (outHour*60)+outMin){
tardies = tardies+1;
}
//if the student left before 6:30 PM, add a tardy
if((checkHour*60)+checkMin > 1830){ //11100 is 6:30 PM in minutes 6PM=> 18 hours * 60 min = 1080 minutes + 30 additional minutes
tardies = tardies+1;
}
}
}
return tardies;
}
To check if they are there for two hours I had to change the time stamps to minutes. I found that I couldn't simply compare the times as is. The same applies for why I changed the arrival time to minutes before comparing it to 6:30 PM.
I am running into an error that states exceeded maximum execution time (line 0). Any advice on how I can change the code or fix this error would be greatly appreciated.

I am running into an error that states exceeded maximum execution time
You have an endless loop that makes your code run until it exceeds maximum execution time
This endless loop is created by the definition
for(k = 0; k<search; k+2)
depending on what you want to do, the correct syntax is either
for(k = 0; k<search; k=k+2)
(if you want to increase k by 2 after each iteration) or
for(k = 0; k<search; k=k++)
if you want to increase k by 1 after each iteration.
I found that I couldn't simply compare the times as is.
Dates are proceeded in Apps Script the same way like in Javascript.
The documentation provides many useful methods and examples of how to wokr with dates.
In a nutshell:
If you want to calculate the difference between two timestamps - the easiest way might be to convert both dates to ms with getTime(), substract them and convert the difference back to hours (1 h = 1000*60*60 ms)
If you want to know the weekday of a date, you need to use getDay()
For working with dates it is recommendable that they are formatted as such in your spreadsheet, rather than manually converting them from numbers inside your script.

Related

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.

Automatically copy values from cells every week at a set time after their value is changed manually by someone else

I run a google sheet to track attendance at a kids program I coordinate. Each row features the name, and then several columns of checkboxes to indicate whether they have brought the adequate materials for the program that week. There is a column (PTS TODAY) beside the checkbox columns that will calculate how many pts that child has earned that week based on the boxes they have checked.
So, every Wednesday at 6:30 pm, our check-in person at the desk manually clicks the checkboxes when the kids show up with whatever stuff they have, and by 6:45 all the points for that week are generated.
I would love to learn how to set up the sheet to perform an automated task weekly (Wednesday at 9 pm for example) that will either:
a) automatically copy the pts column value to an empty column in another sheet
b) automatically add the pts column value to another column that represents TOTAL PTS earned so far
and also
c) automatically resets the value of all the checkboxes to zero to reset the attendance sheet.
I have not tried doing any macros, as that is something I am not familiar with.
So far my efforts have included using formulas with an IF statement referencing a cell with a NOW function in order to create some sort of trigger at a certain time each week, but soon realized if that is even a possible tactic that it is beyond my somewhat limited Google-Sheets prowess.
Try this:
function thumb(){
var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("SHEET_NAME");
var data = ss.getDataRange().getValues();
var count;
for (var i = 1; i < data.length; i++){
count = 0;
for (var j = 1; j < data[0].length; j++){
if (j == data[0].length-1){
data[i][j] += count;
break;
}
if (data[i][j] == true){
count++;
data[i][j] = false;
}
}
}
ss.getDataRange().setValues(data);
}
I wrote this with a sheet that had a header row that was names, activities, total, the activities each had their checkbox button and in the total is where the points went in the end. The code checks which values are true and then adds to the count in the total column. In order to get the trigger set up for weekly running you have to follow these instructions, you'll be able to specify the day/time when you want it to be run.

Comparing a cell with a named range for conditional formatting

I'm importing hourly sensor data from 10 different sensors. In short, I want to be able to see when the hourly data exceeds the average usage for that day of the week and time of the day.
I've created sheets named each of the sensor names ("32022", for example) and each of those sheets is a 26 column matrix of the date, all the hours from 00:00 to 23:00, and a WEEKDAY() function to extract the day of the week from the date.
In a separate sheet ("Daily Usage"), I manually created 10 different matrices of the daily average usage by hour for each sensor, with column A being the day of the week (by name) and the following 24 columns being all the hours of the day. Each one of those matrices has been made into a Named Range called "averageusage_32022".
I am trying to iterate through all the data and identify which data is above average for that particular day/time, and change the background of that cell to red if it exceeds the average.
From what I've gathered, I cannot refer to a named ranged in conditional formatting, which is why I'm looking to solve this programmatically.
var currentsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("32033");
var usagerange = SpreadsheetApp.getActiveSpreadsheet().getRangeByName('averageusage_32033');
//how many rows there are in the sheet
var firstcolumn = currentsheet.getRange("A1:A").getValues();
var bottomrow = firstcolumn.filter(String).length;
//loop through every day, check day of week, iterate through each hour, compare cell to "averageusage" range
for (var i = 2; i < bottomrow; i++){
var day = currentsheet.getRange(i,26).getValue;
for (var j = 2; j < 26; j++){
if (currentsheet.getRange(i,j).getValue() > usagerange.getRange(day,j).getValue()){
currentsheet.getRange(i,j).setBackground("red");
}
}
}
I'm receiving an error of Cannot find function getRange in object Range. I'm assuming that this is because I cannot refer to the named range as I would any normal sheet.
Since getRangeByName(name) method returns Range instead of NamedRange, you need to remove both arguments and getRange() method from the one written into usagerange (please, note that day and j will be 0-based for values Array). Also, make sure to write values into a variable outside the loop to reduce service calls:
var usageVals = usagerange.getValues();
//...loop
if(currentsheet.getRange(i,j).getValue() > usageVals[day-1][j-1]) {
//do something;
}
//...loop

Calculating runtime minus Timestamp

I have a form which activates a procedure via an "On form submit" trigger. At the end of this routine I want to insert the difference in time between the form's Timestamp and the current time at the end of the routine (the difference of which is only a matter of a few seconds).
I've tried many things so far, but the result I typically receive is NaN.
I thought that my best bet would be to construct the runtime elements (H,M,S) and similarly deconstruct the time elements from the entire Timestamp, and then perform a bit of math on that:
var rt_ts = Math.abs(run_time - ts_time);
(btw, I got that formula from somewhere on this site, but I'm obviously grasping at anything at this point. I just can't seem to find a thread where my particular issue is addressed)
I've always found that dealing with dates and time in Javascript is tricky business (ex: the quirk that "month" start at zero while "date" starts at 1. That's unnecessarily mind-bending).
Would anyone care to lead me out of my current "grasping" mindset and guide me towards something resembling a logical approach?
You can simply add this at the top of your onFormSubmit routine :
UserProperties.setProperty('start',new Date().getTime().toString())
and this at the end that will show you the duration in millisecs.
var duration = new Date().getTime()-Number(UserProperties.getProperty('start'))
EDIT following your comment :
the time stamp coming from an onFormSubmit event is the first element of the array returned by e.values see docs here
so I don't really understand what problem you have ??
something like this below should work
var duration = new Date().getTime() - new Date(e.values[0]).getTime();//in millisecs
the value being a string I pass it it 'new Date' to make it a date object again. You can easily check that using the logger like this :
Logger.log(new Date(e.values[0]));//
It will return a complete date value in the form Fri Mar 12 15:00:00 GMT+01:00 2013
But the values will most probably be the same as in my first suggestion since the TimeStamp is the moment when the function is triggered...
I have a function which can show the times in a ss with timestamps in column A. It will also add the time of the script itself to the first timestamp (in row 3) and show this in the Log.
Notice that the google spreadsheet timestamp has a resolution in seconds and the script timestamp in milliseconds. So if you only add, say, 300 milliseconds to a spreadsheet timestamp, it might not show any difference at all if posted back to a spreadsheet. The script below only takes about 40 milliseconds to run, so I have added a Utilities.sleep(0) where you can change the value 0 to above 1000 to show a difference.
function testTime(){
var start = new Date().getTime();
var values = SpreadsheetApp.getActiveSheet().getDataRange().getValues();
for(var i = 2; i < values.length ; i++){
Logger.log(Utilities.formatDate(new Date(values[i][0]),Session.getTimeZone(),'d MMM yy hh:mm:ss' )); // this shows the date, in my case same as the ss timestamp.
Logger.log( new Date(values[i][0]).getTime() ); // this is the date in Milliseconds after 1 Jan 1970
}
Utilities.sleep(0); //you can vary this to see the effects
var endTime = new Date();
var msCumulative = (endTime.getTime() - start);
Logger.log(msCumulative);
var msTot = (msCumulative + new Date(values[2][0]).getTime());
Logger.log('script length in milliseconds ' + msTot );
var finalTime = Utilities.formatDate(new Date(msTot), Session.getTimeZone(), 'dd/MM/yyyy hh:mm:ss');
Logger.log ( finalTime); //Note that unless you change above to Utilities.sleep(1000) or greater number , this logged date/time is going to be identical to the first timestamp since the script runs so quickly, less than 1 second.
}

How do I parse this back to a mm/dd/yyyy format?

I have some dates in a Google Spreadsheet that I'm bringing in to a script like this:
var JCstartDateFix = Math.floor(Date.parse(JCstartDate) / 86400000) + 25570;
var todaysDateFix = Math.floor(Date.parse(todaysDate) / 86400000) + 25570;
How do I do the opposite of this at the end of the script to change it back into a mm/dd/yyyy formatted date?
Thanks in advance for any help on this.
Here's the whole script:
function projectedDate(JCstartDate, overallPercent, pace, todaysDate, HSstartDate, DaysInHS) {
//converts dates to a number of days
var JCstartDateFix = Math.floor(Date.parse(JCstartDate) / 86400000) + 25570;
var todaysDateFix = Math.floor(Date.parse(todaysDate) / 86400000) + 25570;
//This says that there's no projected date since the student hasn't started high school yet
if(HSstartDate == ""){
return "HS not started";
}
//This calculates grad date if the student's been here more than 8 months or if their percent is over 80.
else if(DaysInHS >= 200 || overallPercent >=80){
var percentPerDay = overallPercent/(DaysInHS);
var daysLeft = (100 - overallPercent) / percentPerDay;
if((todaysDateFix + daysLeft) > (JCstartDateFix +730)){
return "You are not on track to complete.";
}
else{
return (todaysDateFix + daysLeft);
}
}
//This calculates grad date if the student's been at JC less than 8 months
else{
if(JCstartDateFix + 600 - pace > JCstartDateFix + 730){
return "You are not on track to complete.";
}
else{
return (JCstartDateFix+600-pace);
}
}
}
I work in a school where students start at different times and work at their own pace. They have a 2 year limit to finish. So this script estimates their graduation date based on when they started and how fast they're going. It uses different formulas depending on how long they've been here. I'm happy with the dates I get on my spreadsheet, but if I format them from the spreadsheet, another script doesn't correctly pick up the text strings and gives a date in 1969 instead.
I think what I need to do is change the lines that return numbers so that those numbers are formatted as dates. I just don't know how. Thanks again!
The value you get with Date.parse() is in milliseconds, you divide it by the number of milliseconds in a day so I guess you obtain the number of days since the JS reference date, rounded to the lowest integer and then add a constant value of 25570.
What is the result supposed to be ?
It seems that it should be a number of day from the ref date but that's quite far in the future !! (about 70 years) Is this right ? could you clarify ?
Anyway, what you should do is to get a value in milliseconds again and use new Date(value in mSec) to get a date object. From there Utilities.formatDate will allow you to get any display format you want.
ref : http://www.w3schools.com/jsref/jsref_obj_date.asp
https://developers.google.com/apps-script/class_utilities#formatDate
As long as the value that you're setting in the spreadsheet is a Date object in apps script, it will appear as a date. The format will be under the control of the spreadsheet, of course, but it defaults to mm/dd/yyyy.
For example, you could just change your existing code to render Date objects. Then, when you call setValue() you will write a date out to the spreadsheet.
var JCstartDateFix = new Date(Math.floor((Date.parse(JCstartDate) / 86400000) + 25570)*86400000);
var todaysDateFix = new Date(Math.floor((Date.parse(todaysDate) / 86400000) + 25570)*86400000);