How to create Work Shifts clock AppScript - google-apps-script

I am learning right now scripts functionally in Google Sheet, however, can't twist my head around constructing a very simple App script.
I have the following table (Snoopi Tab)
https://docs.google.com/spreadsheets/d/1l6nYBAqB1GWoMkIOwlykhiuMpaXdWHTo7UhgZdq6hT8/edit?usp=sharing
I want it to do this simple action:
EXAMPLE: If today is not Sunday or Saturday and the date is 14.2.14 and cell BF5 is
---> go down 3 rows and paste current time "Clocking in" working-shift
When button "IN" is clicked:
If (TODAYDATE = Value in cell in row 5) & (row 3 ==!"S") both true
Set current time in (same column just row 8)
Same with "OUT" button, but this I'll try to figure by myself.

The other answer is acceptable, but is very resource intensive and have a lot of loops to do resulting to very slow execution time especially when it gets later on the year since it will loop all those dates.
Also, the run you did on the other answer did finish successfully but didn't write anything due to it missing the actual date value. This might have been caused by a timezone issue, or by only modifying the actual date while getting the raw time of the cell value.
A better alternative would be to make use of the 4th row where it contains x value when the date is equal to the current date. By using that, you wouldn't need to loop thus resulting in faster execution time and wouldn't need to convert time thus making it safer. As long as row 4 is populated on all columns (which your problem is), there should be no issue of using this script.
Script:
function WorkClock() {
var currentDate = new Date();
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = SpreadsheetApp.getActiveSheet();
// you only need 3rd and 4th row of data
var data = sheet.getRange("E3:NE4").getValues();
// 4th row contains 'x' when today matches the column, find that index
var indexToday = data[1].indexOf('x');
// if that column's row 3 is not 'S'
if(data[0][indexToday] != 'S')
// write the time on row 8
sheet.getRange(8, indexToday + 5).setValue(Utilities.formatDate(currentDate, ss.getSpreadsheetTimeZone(), 'HH:mm'));
}
Output:
Note:
Timezone used is based on the spreadsheet's timezone which is GMT-8. Wherever the user is, it will use GMT-8, not its local time which should be helpful in some cases.
Performance difference between this and looping all dates would be vast if we are now dealing with the later months of the year (e.g. November, December)
For the OUT button, create another function by duplicating the current function. Then replace where you write the time. Instead of row 8 (Start), write it in row 10 (Finish).

function myFunction() {
var actualDate = new Date(new Date().setHours(0, 0, 0, 0)).getTime();
var sheet = SpreadsheetApp.getActiveSheet();
var data = sheet.getRange("E3:NE6").getValues();
for(var i = 0; i < data[0].length; i++) {
if (data[2][i].getTime() === actualDate) {
if (data[0][i] !== "S") {
sheet.getRange(8, (5+i)).setValue(new Date().getHours() + ":" + new Date().getMinutes());
}
break;
}
}
}

Related

Using for and if loops in Google Apps Script

Dear programming Community,
at first I need to state, that I am not quite experienced in VBA and programming in general.
What is my problem? I have created a topic list in google sheets in order to collect topics for our monthly meeting among members in a little dance club. That list has a few columns (A: date of creation of topic; B: topic; C: Name of creator; ...). Since it is hard to force all the people to use the same format for the date (column A; some use the year, others not, ...), I decided to lock the entire column A (read-only) and put a formular there in all cells that looks in the adjacent cell in column B and sets the current date, if someone types in a new topic (=if(B2="";"";Now()). Here the problem is, that google sheets (and excel) does then always update the date, when you open the file a few days later again. I tried to overcome this problem by using a circular reference, but that doesn't work either. So now I am thinking of creating a little function (macro) that gets triggered when the file is closed.
Every cell in Column B (Topic) in the range from row 2 to 1000 (row 1 is headline) shall be checked if someone created a new topic (whether or not its empty). If it is not empty, the Date in the adjacent cell (Column A) shall be copied and reinserted just as the value (to get rid of the formular in that cell). Since it also can happen, that someone has created a topic, but a few days later decides to delete it again, in that case the formular for the date shall be inserted again. I thought to solve this with an If-Then-Else loop (If B is not empty, then copy/paste A, else insert formula in A) in a For loop (checking rows 1 - 1000). This is what I have so far, but unfortunately does not work. Could someone help me out here?
Thanks in advance and best regards,
Harry
function NeuerTest () {
var ss=SpreadsheetApp.getActive();
var s=ss.getSheetByName('Themenspeicher');
var thema = s.getCell(i,2);
var datum = s.getCell(i,1);
for (i=2;i<=100;i++) {
if(thema.isBlank){
}
else {
datum.copyTo(spreadsheet.getActiveRange(), SpreadsheetApp.CopyPasteType.PASTE_VALUES, false);
}}
}
The suggested approach is to limit the calls to the Spreadsheet API, therefore instead of getting every cell, get all the data at once.
// this gets all the data in the Sheet
const allRows = s.getDataRange().getValues()
// here we will store what is written back into the sheet
const output = []
// now go through each row
allRows.forEach( (row, ind) => {
const currentRowNumber = ind+1
// check if column b is empty
if( !row[1] || row[1]= "" ){
// it is, therefore add a row with a formula
output.push( ["=YOUR_FORMULA_HERE"] )
} else {
// keep the existing value
output.push( [row[0]] )
}
})
Basically it could be something like this:
function myFunction() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Themenspeicher');
var range = sheet.getRange('A2:B1000');
var data = range.getValues(); // <---- or: range.getDisplayValues();
for (let row in data) {
var formula = '=if(B' + (+row+2) + '="";"";Now())';
if (data[row][1] == '') data[row][0] = formula;
}
range.setValues(data);
}
But actual answer depends on what exactly you have, how your formula looks like, etc. It would be better if you show a sample of your sheet (a couple of screenshots would be enough) 'before the script' and 'after the script'.

Sending Emails a day before a specified date based on a list of dates

I have a table in google sheets with several columns. I need to send an email reminder a day before dates specified in one of the columns.
My idea of how it should work:
Take date column
Loop through each date and subtract 1 day from it to get the reminder date.
Filter through by comparing "today's" date with the reminder date
Send emails with all rows that meet the filter criteria
I have tried the following:
function StatusAlert() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const ws = ss.getSheetByName("On hold procedure");
const lr = ws.getLastRow();
var data = ws.getRange(2, 2, lr, 8).getDisplayValues();
var today = new Date();
var exclusiondate = ws.getRange(2,8,lr).getDisplayValues();
for (var i = 0; i < data.length; i++) {
var alertdate = new Date(exclusiondate[I] - 1)
}
I'm struggling with referencing the date column and then applying the date math to it. Also, I am not quite sure that I needed the second getDisplayValues call (I was trying to reference the date column I need specifically).
Because Even if I manage to apply the change to every row based off of the second getDisplayValues call, I won't be able to go onto the next step of filtering the entire table and sending the relevant rows via email.
So ideally I would like to be able to reference the specific date column from the entire range, and apply date math to it.
Does anyone have any idea how I can best proceed on this?
Regards,
Michael
Try this:
function StatusAlert() {
const ss=SpreadsheetApp.getActive();
const ws=ss.getSheetByName("On hold procedure");
var data=ws.getRange(2,2,ws.getLastRow()-1,8).getValues();
var today=new Date();//add this `valueOf()` if you want to compare with another date value
var xd=ws.getRange(2,8,ws.getLastRow()-1).getValues();
for (var i=0;i<data.length;i++) {
var dt=new Date(xd[i][0]);
var alertdate=new Date(dt.getFullYear(),dt.getMonth(),dt.getDate()-1);//subtract off one day
}
}
If you want to compare today and alertdate then I would use valueOf() for both of them and then they are just numbers.
I would refer you to this post. I would use getValues instead of getDisplayValues because it returns a more reliable value. For example, if you change the formatting of your date in the spreadsheet, it will break the code.
Lastly, JavaScript is case sensitive do not capitalize "I" in the last line of code.

Writing to a cell based on todays date

I am learning coding in Sheets, and have already jigsawed together a couple of games (based off very limited coding knowledge. Very fun to see it come together
Next task:
I am logging my hours spent doing "Growth" things (like learning, exercise, work etc.) vs "Leisure" things (like TV, games, FB scrolling etc). Up until now I just scroll down to today's date and add in the numbers manually. Would be fun to have a button at the top of my sheet that just adds hours to the "Growth" or "Leisure" cells for today.
I have used formulae in the cell G371 to reveal the A1 notation of my target cell, hoping I can use this info in a script to write to that cell:
=substitute(Cell("address",vlookup(A1,A2:D367,3)),"$","")
Where cell A1 contains =today()
and A2:A367 are all the dates in the year.
and columns C and D hold numbers for growth and leisure respectively.
Then I tried this code to try to see how to write to a cell. I realise I am probably missing some very fundamental knowledge...
var ss = SpreadsheetApp.getActiveSpreadsheet();
var cellname = ss.getRange('G371') // makes cellname refer to G371
var input = cellname.getValue() // makes 'input' return the contents of G371, in today's case its 'C120'
var test = ss.getRange(input) // hopefully reads 'C120' instead of "input"
test.setValue(1) // setting value to 1 for now, as I'm just testing cell
// referencing. later I will work on figuring out the
// math for the buttons. I'm not there yet.
So it didn't work. apparently the problem is with ss.getRange(input) but I don't know what to do about that.
So my question is how to write to a cell that is named in another cell?
hope it makes sense
Thanks!
PS Here is a copy of the sheet
https://docs.google.com/spreadsheets/d/1KkoCb8kY1XICMeB9bx65HXsldCS-fa75xJCaU52WGg8/edit?usp=sharing
The formula in the cell might be messing with the value you retrieve. To make sure that does not happen, you can use getDisplayValue() instead of getValue().
Another way:
Also, if you just want to retrieve the cell where to write today's growth (or leisure), you don't need to follow this roundabout way of creating a formula in another cell to reference this one. You can do this instead (check comments):
function todayGrowthCell() {
var sheet = SpreadsheetApp.getActiveSheet();
var date = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "d/M"); // Format today's date as in spreadsheet, in order to compare
var firstRow = 2; // Row where days start
var column = 3; // Column C (Growth)
var dates = sheet.getRange(2, 1, 366).getDisplayValues().map(row => row[0]); // Retrieve columns with dates (its display value, in order to compare with today's date, which is formatted accordingly)
var row = dates.indexOf(date) + firstRow; // Today's row
var cell = sheet.getRange(row, column); // Today's growth cell
cell.setValue(1); // Set value to today's growth cell
}

Time Arithmetic Calculation Error

I have the following code on a gsheet -
var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var task = ss.getRange(1,2).getValue(); //ss.getRange("A2").getValue();
var date = new Date();
function onEdit(e) {
if (e.range.getA1Notation() == 'B2') {
if (/^\w+$/.test(e.value)) {
// console.log(e.value);
eval(e.value)();
e.range.clear();
}
}
}
function Start() {
var last = ss.getLastRow();
ss.getRange(last+1, 1).setValue(task);
ss.getRange(last+1, 2).setValue(date);
}
function End() {
var last = ss.getLastRow();
ss.getRange(last, 3).setValue(date);
var endTime = ss.getRange(last, 3).getValue();
var startTime = ss.getRange(last, 2).getValue();
ss.getRange(last, 4).setValue(endTime-startTime);
}
Whenever the cell in B2 is edited, it runs one of the validated names of the functions - either Start or End.
If 'start', it puts the value of cell b1 (validated list of task names) in first column, and current time in MM/DD/YYYY HH:MM:SS format.
If 'end', its the current time in MM/DD/YYYY HH:MM:SS format in the third column, and attempts to put the difference between the "start" time and "end" time in the fourth column, or a calculation.
Here's the error (or my lack of understanding how gsheets works)
The code produces the following:
Task| Start | End | Duration
DOS | 8/2/2017 16:44:28 | 8/2/2017 16:44:31 | 2,418.00
Question - what is the 2,418? The total duration should be 2 seconds, or 00:00:02. Given the above code, is it a code issue or a format of the cell issue?
when I put in the cell d2 = c2-b2, it works fine as long as column is formatted as a duration. But I'd rather not do it, as the inserting or start/end times is dependent upon the last row - so if I copy/paste the formula down to the bottom on the gsheet the data will not be continuous.
JavaScript timestamps are in milliseconds. Divide by 1000 to get seconds.
Also, using eval like you do seems a pretty bad idea. I would use a switch statement there, and display an error for the user if the entered function is not one of options. As is, your code silently fails if they enter "stat" instead of "start".

Google Sheets OnEdit script: copy value from specific columns

We are using Google sheets for tracking attendance. Previously, the teachers were entering P, T, or A (for present, tardy, absent) for each period. I would still like users to have the option to enter a value for each period in a week, however it would be a great time saver if they could enter one value for the whole day.
What I'd like is that if a value is entered into any one of the "0" periods (green columns) with a "P" or "A" (data validation limits those options) an OnEdit function would copy that same letter ("P" or "A") to the following 8 columns and then delete the original value. (without the deletion the totals on the far right columns will be off). I would not want the OnEdit to be activitated based on edits in any of the non-green columns.
I will eventually have several tabs, each one a different week, but each exactly the same... so I'm thinking the function should work within whatever the activesheet is.
https://docs.google.com/spreadsheets/d/1NKIdNY4k66r0zhJeFv8jYYoIwuTq0tCWlWin5GO_YtM/edit?usp=sharing
Thank you for your help,
I wrote some code to get you started with your project. (I am also a teacher) You will have to make some changes based on what you are going for and it can probably be optimised to run faster. Good luck!
function onEdit(e) {
//create an array of the columns that will be affected
var allColumns = [2, 10];
//get the number values of the column and row
var col = e.range.getColumn();
var row = e.range.getRow();
//get the A1 notation of the editted cell for clearing it out
var cell = e.range.getA1Notation();
//only run if the cell is in a column in the allColumns array
if(allColumns.indexOf(col) > -1) {
//run the for loop for the next 8 cells
for(var i = col + 1; i < col + 9; i++) {
SpreadsheetApp.getActiveSheet().getRange(row, i).setValue(e.value);
SpreadsheetApp.getActiveSheet().getRange(cell).setValue('');
}
}
}