Skip to content

Instantly share code, notes, and snippets.

@dayt0n
Created September 1, 2018 04:28
Show Gist options
  • Select an option

  • Save dayt0n/3586008a25cb44bdb65c8bf5572c7b0f to your computer and use it in GitHub Desktop.

Select an option

Save dayt0n/3586008a25cb44bdb65c8bf5572c7b0f to your computer and use it in GitHub Desktop.
fill out a time sheet from information collected with an IFTTT csv log
/*
* TimeSheet.cpp - fill out a time sheet from information collected with an IFTTT csv log
*
* (c)dayt0n 2018
*
* build: g++ main.cpp -o timesheet -lcurl `Magick++-config --cxxflags --cppflags` `Magick++-config --ldflags --libs` -Wall -DNAME='"YOUR NAME"' -DSIG -std=c++11
*
*/
#include <string>
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <curl/curl.h>
#include <ctime>
#include <vector>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <Magick++.h>
#include <math.h>
#include <algorithm>
using namespace std;
using namespace Magick;
// this can be changed at a per-user basis
#ifndef NAME
#define NAME "YOUR NAME"
#endif
#ifndef LOG_ID
#define LOG_ID "SHEET_ID"
#endif
#define NUM_DAYS 14 // two weeks
#define ACTIVATION_DAY 2 // monday == 0, tuesday == 1, etc
#define WIGGLE_ROOM 8 // 15 - WIGGLE_ROOM = how many minutes should pass before making the decision to advance to the next quarter mark in an hour
#define LUNCH_DURATION .25 // time in hours of a predetermined lunch break period (.25 = 15 min, .5 = 30 min, etc)
#define LUNCH_TIME "1:00"
#define LUNCH_MERIDIEM "PM"
#define OVERTIME 40 // how many hours must you work before you qualify for overtime
#ifdef SIG
#define ADD_SIGNATURE true
#ifndef SIGNATURE_URL
#define SIGNATURE_URL "LINK TO SIG PNG" // link to URL with signature .png file
#endif
#else
#define ADD_SIGNATURE false
#define SIGNATURE_URL ""
#endif
#define YEAR "2018"
struct activity {
int event; // 0 for entered, 1 for exit
string day;
string time;
string meridiem;
};
size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) {
size_t written = fwrite(ptr, size, nmemb, stream);
return written;
}
int downloadFile(string url,string filename) {
CURL *curl;
FILE* fp;
CURLcode res;
const char* URL = url.c_str();
const char* outfile = filename.c_str();
curl = curl_easy_init();
if(curl) {
fp = fopen(outfile,"wb");
curl_easy_setopt(curl,CURLOPT_URL,URL);
curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,write_data);
curl_easy_setopt(curl,CURLOPT_WRITEDATA,fp);
curl_easy_setopt(curl,CURLOPT_FOLLOWLOCATION, 1);
res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
fclose(fp);
} else
return -1;
return 0;
}
vector <string> getDays(time_t now) {
int dayLen = 60*60*24;
vector <string> days;
time_t tempDay = 0;
string dateLabel;
for(int i = NUM_DAYS+1; i > 1; i--) {
tempDay = now - (i * dayLen);
tm* realTime = localtime(&tempDay); /*
if (i == NUM_DAYS && realTime->tm_wday == ACTIVATION_DAY) {
printf("Excellent\n"); // check if first day is wednesday
} else {
printf("%d\n",realTime->tm_wday);
}*/
if (realTime->tm_mday == 31) {// go to next month to prevent overflow to 32
realTime->tm_mday = 0;
realTime->tm_mon += 1;
}
dateLabel = to_string(realTime->tm_mon + 1) + "/" + to_string(realTime->tm_mday + 1); // have to add one because starts at 0
days.push_back(dateLabel);
}
return days;
}
vector <string> getDates(time_t now,vector <string> days) {
string months[] = {"January","February","March","April","May","June","July","August","September","October","November","December"};
vector <string> dateStrs;
string monthNum,dayNum,month;
size_t pos;
for(unsigned int i = 0; i < days.size(); i++) {
pos = days[i].find("/");
monthNum = days[i].substr(0,pos);
dayNum = days[i].substr(pos+1);
if (dayNum.length() < 2) {
dayNum = "0" + dayNum;
}
month = months[stoi(monthNum) - 1];
dateStrs.push_back(month + " " + dayNum + ", " + YEAR);
}
return dateStrs;
}
vector <string> getUsefulLinesFromCSV(string csv,vector <string> dates) {
vector <string> usefulLines;
ifstream file;
file.open(csv);
bool didGetUsefulLines = false;
for(string line; getline(file,line);) {
for (unsigned int i = 0; i < dates.size(); i++) {
if (line.find(dates[i]) != string::npos) {
didGetUsefulLines = true;
usefulLines.push_back(line);
}
}
}
if (!didGetUsefulLines) {
printf("There is no data for the past two weeks. Exiting...\n");
exit(-1);
}
return usefulLines;
}
vector <activity> getEvents(string csv, vector <string> dates) {
vector <string> rawLines = getUsefulLinesFromCSV("sheet.csv",dates);
struct activity eventAtTime;
vector <activity> events;
size_t pos1,pos2,pos3,pos4,pos5;
string lastPart,datePart,timePart,day,time;
for(unsigned int i = 0; i < rawLines.size(); i++) {
if (rawLines[i].find("entered") != string::npos)
eventAtTime.event = 0;
else
eventAtTime.event = 1;
if(rawLines[i].find("PM") != string::npos)
eventAtTime.meridiem = "PM";
else
eventAtTime.meridiem = "AM";
pos1 = rawLines[i].find("\"");
lastPart = rawLines[i].substr(pos1);
pos2 = lastPart.find(",");
datePart = lastPart.substr(0,pos2);
timePart = lastPart.substr(pos2+1);
pos3 = datePart.find(" ");
day = datePart.substr(pos3+1);
pos4 = timePart.find("at ");
time = timePart.substr(pos4+3);
if (eventAtTime.meridiem == "PM") {
pos5 = time.find("PM");
time = time.substr(0,pos5);
} else {
pos5 = time.find("AM");
time = time.substr(0,pos5);
}
eventAtTime.day = day;
eventAtTime.time = time;
events.push_back(eventAtTime);
}
return events;
}
vector <activity> roundTimes(vector <activity> events) {
string time;
int minute, hour;
string minuteStr, hourStr;
size_t colonPos;
for(unsigned int i = 0; i < events.size(); i++) {
time = events[i].time;
colonPos = time.find(":");
hour = stoi(time.substr(0,colonPos));
minute = stoi(time.substr(colonPos+1));
if ((60 - minute) < WIGGLE_ROOM) { // if the minute it past 52
minuteStr = "00";
if (hour != 12) {
hourStr = to_string(hour+1);
} else {
hourStr = "1";
}
} else if(abs(45 - minute) < WIGGLE_ROOM) {
minuteStr = "45";
hourStr = to_string(hour);
} else if(abs(30 - minute) < WIGGLE_ROOM) {
minuteStr = "30";
hourStr = to_string(hour);
} else if(abs(15 - minute) < WIGGLE_ROOM) {
minuteStr = "15";
hourStr = to_string(hour);
} else if(minute < WIGGLE_ROOM) {
minuteStr = "00";
hourStr = to_string(hour);
}
if (hourStr.length() < 2)
hourStr = "0" + hourStr;
time = hourStr + ":" + minuteStr;
events[i].time = time;
}
return events;
}
vector <string> getDurations(vector <activity> events) {
vector <string> durations;
string day,duration;
int minuteExit,hourExit,minuteEnter,hourEnter,totalMinuteDifference;
double durationDouble,minuteDurationInHours;
string enterTime,exitTime,enterMeridiem,exitMeridiem;
bool inOneMeridiem;
size_t pos1,pos2,pos3;
stringstream durationStream;
string PM = "PM";
if(events[events.size()-1].event == 0) // if we end on an "entered" log, forget about it. it was never there...
events.erase(events.begin()+(events.size()-1));
for(unsigned int i = 0; i < events.size(); i++) {
day = events[i].day;
for(unsigned int j = i+1; j < events.size(); j++) {
if (events[j].day == day) {
// do stuff with the two events (events[i] and events[j]) here to calculate time
// then remove element
if (events[j].meridiem == events[i].meridiem)
inOneMeridiem = true;
else {
inOneMeridiem = false;
}
if (events[j].event == 0) { // events[j] is the one in which we entered
enterTime = events[j].time;
exitTime = events[i].time;
if (inOneMeridiem == false) {
enterMeridiem = events[j].meridiem;
exitMeridiem = events[i].meridiem;
}
} else { // events[j] is the one in which we exited
enterTime = events[i].time;
exitTime = events[j].time;
if (inOneMeridiem == false) {
enterMeridiem = events[i].meridiem;
exitMeridiem = events[j].meridiem;
}
}
// now calculate difference
pos1 = enterTime.find(":");
pos2 = exitTime.find(":");
hourEnter = stoi(enterTime.substr(0,pos1));
hourExit = stoi(exitTime.substr(0,pos2));
minuteEnter = stoi(enterTime.substr(pos1+1));
minuteExit = stoi(exitTime.substr(pos2+1));
pos3 = string(LUNCH_TIME).find(":");
int lunchHour = stoi(string(LUNCH_TIME).substr(0,pos3));
int lunchMinute = stoi(string(LUNCH_TIME).substr(pos3+1));
if (inOneMeridiem) {
totalMinuteDifference = (60 - minuteEnter) + minuteExit;
minuteDurationInHours = totalMinuteDifference / 60.0; // if it was 75, it should give us 1.25; if it was 45, it should give us .75
if (hourEnter == 12)
hourEnter = 0;
durationDouble = (hourExit - (hourEnter + 1)) + minuteDurationInHours;
if (events[i].meridiem == LUNCH_MERIDIEM) {
if (hourEnter < lunchHour) {
durationDouble -= LUNCH_DURATION;
} else if (hourEnter == lunchHour && minuteEnter < lunchMinute) {
durationDouble -= LUNCH_DURATION;
} else {
durationDouble += .07;
}
}
} else {
totalMinuteDifference = (60 - minuteEnter) + minuteExit;
minuteDurationInHours = totalMinuteDifference / 60.0;
if(enterMeridiem == "AM") { // coming into work in the morning
durationDouble = ((12 - hourEnter) + (hourExit - 1)) + minuteDurationInHours;
} else { // if you are going to work in the PM something is obviously wrong and you should be paid
durationDouble = ((hourEnter - 1) + (12 - hourExit)) + minuteDurationInHours;
}
if(enterMeridiem == LUNCH_MERIDIEM) {
if (hourEnter < lunchHour) {
durationDouble -= LUNCH_DURATION;
} else if (hourEnter == lunchHour && minuteEnter < lunchHour) {
durationDouble -= LUNCH_DURATION;
} else {
durationDouble += .07;
}
} else {
if (LUNCH_MERIDIEM == PM) {
// enterMeridiem would be AM, LUNCH IN PM
if (lunchHour < hourEnter) {
durationDouble -= LUNCH_DURATION;
} else {
durationDouble += .07;
}
} else {
// enterMeridiem would be PM, LUNCH in AM
durationDouble += .07;
// 24+ hour days are not acceptable. go home and get some sleep. and eat lunch. eat lunch too.
}
}
}
durationStream.str(""); // must clear the stream
durationStream << fixed << setprecision(2) << durationDouble;
duration = durationStream.str();
events.erase(events.begin()+j);
}
}
// put into vector
durations.push_back(duration);
}
return durations;
}
vector <double> getTotals(vector <string> durations,vector <string> days,vector <activity> events) {
vector <double> totals;
double weekTotal = 0.0;
double secondWeekTotal = 0.0;
string currentDay;
size_t pos1;
for(unsigned int i = 0; i < days.size(); i++) {
if(!days.empty()) {
pos1 = days[i].find("/");
currentDay = days[i].substr(pos1+1);
if (currentDay.length() < 2)
currentDay = "0" + currentDay;
if (!events.empty()) {
if (currentDay == events[0].day) {
events.erase(events.begin());
events.erase(events.begin());
if (i < 7) {
weekTotal = weekTotal + stod(durations[0]);
}
else {
secondWeekTotal = secondWeekTotal + stod(durations[0]);
}
durations.erase(durations.begin());
}
}
}
}
if (fmod(weekTotal,.25) != 0.0) {
weekTotal -= .07;
}
if (fmod(secondWeekTotal,.25) != 0.0) {
secondWeekTotal -= .07;
}
totals.push_back(weekTotal);
totals.push_back(secondWeekTotal);
return totals;
}
int writeToPDF(string infile, string outfile,time_t now) {
vector <string> days = getDays(now);
vector <string> datesToFind = getDates(now,days);
vector <activity> events = getEvents("sheet.csv",datesToFind);
events = roundTimes(events);
vector <string> durations = getDurations(events);
vector <double> totals = getTotals(durations,days,events);
int lunchMinute;
string tempDate,lunchHour;
size_t pos1,posLunch;
double totalTime = 0.0;
double overTime = 0.0;
int durationCount = 0;
int didFind = 0;
double durationToUse = 0.0;
string totalTimeString,overTimeString,durationString;
vector <string> totalsString;
stringstream tempTotalStream,doubleStream;
for(unsigned int i = 0; i < totals.size(); i++) {
tempTotalStream.str("");
tempTotalStream << fixed << setprecision(2) << totals[i];
totalsString.push_back(tempTotalStream.str());
if (totals[i] > OVERTIME) {
overTime += totals[i] - OVERTIME;
totals[i] = 40;
}
totalTime += totals[i];
}
if(totalTime >= OVERTIME*(NUM_DAYS/7)) {
totalTime = OVERTIME*(NUM_DAYS/7);
}
tempTotalStream.str("");
tempTotalStream << fixed << setprecision(2) << overTime;
overTimeString = tempTotalStream.str();
tempTotalStream.str("");
tempTotalStream << fixed << setprecision(2) << totalTime;
totalTimeString = tempTotalStream.str();
posLunch = string(LUNCH_TIME).find(":");
lunchHour = string(LUNCH_TIME).substr(0,posLunch);
lunchMinute = 60 * LUNCH_DURATION;
string lunchOut = lunchHour + ":" + to_string(lunchMinute);
// now we do image "magic"
Image image;
printf("Writing to %s...\n", outfile.c_str());
int dateXCords[14] = {405,465,520,575,630,690,745,1255,1315,1370,1425,1480,1540,1600};
try {
image.read(infile);
image.fontPointsize(28);
image.annotate(NAME,Geometry(10,10,300,255));
for(int i = 0; i < NUM_DAYS; i++) {
didFind = 0;
image.annotate(days[i],Geometry(10,10,370,dateXCords[i]));
pos1 = days[i].find("/");
tempDate = days[i].substr(pos1+1);
if (tempDate.length() < 2)
tempDate = "0" + tempDate;
for(unsigned int j = 0; j < events.size(); j++) {
if (tempDate == events[j].day) {
// we got one
if(events[j].event == 0) { // 0 == enter
durationToUse = stod(durations[durationCount]);
image.annotate(events[j].time,Geometry(10,10,465,dateXCords[i])); // enter column
image.annotate(events[j].meridiem,Geometry(10,10,545,dateXCords[i]));
image.annotate(events[j+1].time,Geometry(10,10,595,dateXCords[i])); // exit column
image.annotate(events[j+1].meridiem,Geometry(10,10,675,dateXCords[i]));
if (fmod(durationToUse,.25) == 0.0) {
// lunch happened
image.annotate(LUNCH_TIME,Geometry(10,10,740,dateXCords[i]));
image.annotate(lunchOut,Geometry(10,10,870,dateXCords[i]));
} else {
image.annotate("--",Geometry(10,10,740,dateXCords[i]));
image.annotate("--",Geometry(10,10,870,dateXCords[i]));
durationToUse -= .07;
}
doubleStream.str("");
doubleStream << fixed << setprecision(2) << durationToUse;
durationString = doubleStream.str();
image.annotate(durationString,Geometry(10,10,1020,dateXCords[i]));
events.erase(events.begin()+j);
durationCount += 1;
}
didFind = 1;
}
}
if (didFind == 0) {
image.annotate("--",Geometry(10,10,480,dateXCords[i]));
image.annotate("--",Geometry(10,10,610,dateXCords[i]));
image.annotate("--",Geometry(10,10,740,dateXCords[i]));
image.annotate("--",Geometry(10,10,870,dateXCords[i]));
image.annotate("--",Geometry(10,10,1020,dateXCords[i]));
}
}
image.annotate(totalsString[0],Geometry(10,10,1020,805)); // FIX ISSUE WITH COUNTING TOTALS IN A WEEK IMMEDIATELY
image.annotate(totalsString[1],Geometry(10,10,1020,1655));
image.annotate(totalTimeString,Geometry(10,10,490,1830));
image.annotate(overTimeString,Geometry(10,10,870,1830));
if (ADD_SIGNATURE) {
Image signature;
downloadFile(SIGNATURE_URL,"sig.png");
printf("Adding signature...\n");
try {
signature.read("sig.png");
image.composite(signature,Geometry(10,10,450,1670),OverCompositeOp);
image.composite(signature,Geometry(10,10,450,815),OverCompositeOp);
} catch(Exception &error_) {
printf("Caught exception: %s\n", error_.what());
return -1;
}
}
image.quality(100);
image.write(outfile);
} catch(Exception &error_) {
printf("Caught exception: %s\n", error_.what());
return -1;
}
return 0;
}
int main(int argc, char* argv[]) {
if(argc < 2) {
printf("usage: %s [timesheet.pdf]\n",argv[0]);
return -1;
}
time_t now = time(0);
tm* todaysDate = localtime(&now);
/*if (todaysDate->tm_wday != ACTIVATION_DAY) { // if it is not tuesday, do not run this program
printf("It is not time to turn in the sheet, not running.\n");
return -1;
}*/
printf("Grabbing logs...\n");
if(downloadFile("https://docs.google.com/spreadsheets/d/" + string(LOG_ID) + "/export?gid=0&format=csv","sheet.csv") != 0) {
printf("[Error]: Unable to download time logs\n");
return -1;
}
// now we modify timesheet with all the data we have processed
string timesheet = string(argv[1]);
string currentMonth = to_string(todaysDate->tm_mon + 1);
string currentDay = to_string(todaysDate->tm_mday);
if(currentMonth.length() < 2)
currentMonth = "0" + currentMonth;
if(currentDay.length() < 2)
currentDay = "0" + currentDay;
string newName = string(NAME);
replace(newName.begin(),newName.end(),' ','_');
string pdfOutfile = "Timesheet_" + currentMonth + "_" + currentDay + "_" + newName + ".pdf";
string run = string(argv[0]);
if (count(run.begin(),run.end(),'/') > 1) {
string prefix;
// the program is being run from elsewhere, meaning that directory may not be writeable. write to the directory the program is run from instead.
if (run.front() == '.') {
run.erase(run[0]);
}
size_t pos = run.find_last_of('/');
prefix = run.substr(0,pos+1);
pdfOutfile = prefix + pdfOutfile;
}
if(writeToPDF(timesheet,pdfOutfile,now) != 0) {
printf("Error writing to timesheet file. Are you sure this is the correct one?\n");
return -1;
}
remove("sheet.csv");
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment