Created
September 1, 2018 04:28
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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