timekeeper

[Abandoned unfinished] CGI web application in C for time tracking. (My first, just a learning project)
Log | Files | Refs | README

commit dc30e93184466c41c694d2a6d0c6a65b890f0b77
parent 7c07002cc892dd88e1cb7cdaec54589fe15dcded
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Mon, 11 Mar 2024 17:31:01 -0700

Implement a stimesheet struct

Instead of just using a time_t array, use a struct to make things
more simple. The struct has a time_t array with four times (associated
with a time_field value) as well as the set member which is a bitmask
of time_flag values to determine which time values are set.

The struct is also a doubly linked list to make things easier.

Diffstat:
Mpages/export.c | 27++++++++++++---------------
Mpages/main.c | 115++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mstmt.c | 3+--
Mtimes.c | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtimes.h | 22+++++++++++++++++++++-
5 files changed, 180 insertions(+), 103 deletions(-)

diff --git a/pages/export.c b/pages/export.c @@ -27,10 +27,8 @@ enum kcgi_err pageexport(struct pagedata *pd) { enum kcgi_err status; - time_t *times; - size_t len; - size_t a, i; - int b; + struct timesheet *times, *ts; + int n; char *tmp; enum kcgi_err (*serialize)(struct pagedata *, time_t) = serialize_ustr; @@ -48,7 +46,7 @@ pageexport(struct pagedata *pd) serialize = serialize_epoch; } - times = gettimes(pd, pd->user->hash, &len); + times = gettimes(pd, pd->user->hash); if (!times) return errorpage(pd, KHTTP_500); @@ -63,28 +61,27 @@ pageexport(struct pagedata *pd) return status; } - for (a = 0; a < len / 4; ++a) { - for (b = 0; b < 4; ++b) { - if (b && (status = khttp_putc(&pd->req, '\t')) != KCGI_OK) { - free(times); + for (ts = times; ts; ts = ts->next) { + for (n = 0; n < 4; ++n) { + if (n && (status = khttp_putc(&pd->req, '\t')) != KCGI_OK) { + freetimesheet(times); return status; } - i = (a * 4) + b; - if (times[i]) { - status = serialize(pd, times[i]); + if (ts->set & timeflagmap[n]) { + status = serialize(pd, ts->times[n]); if (status != KCGI_OK) { - free(times); + freetimesheet(times); return status; } } } if ((status = khttp_putc(&pd->req, '\n')) != KCGI_OK) { - free(times); + freetimesheet(times); return status; } } - free(times); + freetimesheet(times); return KCGI_OK; } diff --git a/pages/main.c b/pages/main.c @@ -25,28 +25,16 @@ static char *datetime_fmt = "%FT%T+0000"; enum kcgi_err -printhours(struct pagedata *pd, time_t *times, int len) +printhours(struct pagedata *pd, struct timesheet *ts) { enum kcgi_err status; time_t elapsed; time_t hr, mn, sc; struct tm *tm; char datetime[13]; /* allows up to four digits for hours */ - time_t to, from, breaktime; + int final; - assert(len > 0); - from = times[StartTime]; - assert(from); - to = ((len - 1) >= EndTime) ? times[EndTime] : 0; - breaktime = 0; - - if ((len - 1) == BreakStartTime && times[BreakStartTime]) - to = times[BreakStartTime]; - else if ((len - 1) >= BreakEndTime && - times[BreakStartTime] && times[BreakEndTime]) - breaktime = times[BreakEndTime] - times[BreakStartTime]; - - elapsed = ((to ? to : time(NULL)) - from) - breaktime; + elapsed = getduration(ts); assert(elapsed >= 0); mn = elapsed / 60; @@ -54,7 +42,8 @@ printhours(struct pagedata *pd, time_t *times, int len) hr = mn / 60; mn = mn % 60; - tm = gmtime(&from); + assert(ts->set & StartTimeFlag); + tm = gmtime(&ts->times[0]); if (!tm) err(1, "gmtime"); @@ -63,7 +52,10 @@ printhours(struct pagedata *pd, time_t *times, int len) err(1, "snprintf (%lldh %lldm %llds)", hr, mn, sc); } - if (!to) + final = (ts->set & EndTimeFlag || + (ts->set & BreakStartTimeFlag && !(ts->set & BreakEndTimeFlag))); + + if (final) status = khtml_attr(&pd->html, KELEM_TIME, KATTR_ID, "counter", KATTR_DATETIME, datetime, KATTR__MAX); else @@ -75,7 +67,7 @@ printhours(struct pagedata *pd, time_t *times, int len) if ((status = khtml_printf(&pd->html, "%.2lld:%.2lld:%.2lld", hr, mn, sc)) != KCGI_OK) return status; - if (!to) { + if (!final) { if ((status = khtml_elem(&pd->html, KELEM_NOSCRIPT)) != KCGI_OK || (status = khtml_attr(&pd->html, KELEM_A, KATTR_HREF, pd->pages[PageMain], KATTR__MAX)) != KCGI_OK || @@ -117,7 +109,7 @@ printtimebutton(struct pagedata *pd, enum time_field tf) /* prints one row in times table */ enum kcgi_err -printtime(struct pagedata *pd, time_t *times, unsigned int len) +printtime(struct pagedata *pd, struct timesheet *ts) { unsigned int i; struct tm *tm; @@ -126,11 +118,8 @@ printtime(struct pagedata *pd, time_t *times, unsigned int len) char time[6]; /* %R */ enum kcgi_err status; - if (len > 4) - return KCGI_SYSTEM; - - if (len > 0) { - tm = gmtime(&times[0]); + if (ts->set & StartTimeFlag) { + tm = gmtime(&ts->times[0]); if (!tm) return KCGI_SYSTEM; if (strftime(date, sizeof(date), "%F", tm) >= sizeof(date)) @@ -155,55 +144,60 @@ printtime(struct pagedata *pd, time_t *times, unsigned int len) * with no break end time but a start break time mean the * break end time is the end time. */ - if ((len - 1) == EndTime && times[BreakStartTime] && !times[BreakEndTime]) - times[BreakEndTime] = times[EndTime]; + if (ts->set & BreakStartTimeFlag && + !(ts->set & BreakEndTimeFlag) && + ts->set & EndTimeFlag) + timesheet_set(ts, BreakEndTime, ts->times[EndTime]); for (i = 0; i < 4; ++i) { - if (i >= len || !times[i]) { + if (!(ts->set & timeflagmap[i])) { if ((status = khtml_elem(&pd->html, KELEM_TD)) != KCGI_OK) return status; - if (len == StartTime && i == StartTime) + /* This does not validate input */ + if (i == StartTime) status = printtimebutton(pd, StartTime); - else if (len == BreakStartTime && (i == BreakStartTime || i == EndTime)) + else if (ts->set & StartTimeFlag && + i == BreakStartTime && !(ts->set & EndTimeFlag)) status = printtimebutton(pd, i); - else if (len > BreakStartTime && ((times[BreakStartTime] && i == BreakEndTime) || i == EndTime)) + else if (i == BreakEndTime && (ts->set & BreakStartTimeFlag)) + status = printtimebutton(pd, i); + else if (ts->set & StartTimeFlag && i == EndTime) status = printtimebutton(pd, i); else status = khtml_putc(&pd->html, '-'); if (status != KCGI_OK || (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) return status; - continue; - } - - if (i) { - tm = gmtime(&times[i]); - if (!tm) + } else { + if (i) { + tm = gmtime(&ts->times[i]); + if (!tm) + return KCGI_SYSTEM; + } + + if (strftime(datetime, sizeof(datetime), datetime_fmt, tm) >= sizeof(datetime) || + strftime(time, sizeof(time), "%R", tm) >= sizeof(time)) return KCGI_SYSTEM; - } - if (strftime(datetime, sizeof(datetime), datetime_fmt, tm) >= sizeof(datetime) || - strftime(time, sizeof(time), "%R", tm) >= sizeof(time)) - return KCGI_SYSTEM; - - if ((status = khtml_elem(&pd->html, KELEM_TD)) != KCGI_OK || - (status = khtml_attr(&pd->html, KELEM_TIME, - KATTR_DATETIME, datetime, KATTR__MAX)) != KCGI_OK || - (status = khtml_puts(&pd->html, time)) != KCGI_OK || - (status = khtml_closeelem(&pd->html, 2)) != KCGI_OK) - return status; + if ((status = khtml_elem(&pd->html, KELEM_TD)) != KCGI_OK || + (status = khtml_attr(&pd->html, KELEM_TIME, + KATTR_DATETIME, datetime, KATTR__MAX)) != KCGI_OK || + (status = khtml_puts(&pd->html, time)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 2)) != KCGI_OK) + return status; + } } if ((status = khtml_elem(&pd->html, KELEM_TD)) != KCGI_OK || - (len && (status = printhours(pd, times, len)) != KCGI_OK) || + (ts->set & StartTimeFlag && (status = printhours(pd, ts)) != KCGI_OK) || (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) return status; return KCGI_OK; } enum kcgi_err -printtimes(struct pagedata *pd, time_t *times, size_t len) +printtimes(struct pagedata *pd, struct timesheet *times) { unsigned int i; enum kcgi_err status; @@ -231,9 +225,9 @@ printtimes(struct pagedata *pd, time_t *times, size_t len) (status = khtml_elem(&pd->html, KELEM_TBODY)) != KCGI_OK) return status; - for (i = 0; i <= len; i += 4) { + for (; times; times = times->next) { if ((status = khtml_elem(&pd->html, KELEM_TR)) != KCGI_OK || - (status = printtime(pd, &times[i], Min(len - i, 4))) != KCGI_OK || + (status = printtime(pd, times)) != KCGI_OK || (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) return status; } @@ -258,8 +252,7 @@ pagemain(struct pagedata *pd) { enum kcgi_err status; enum time_field tf; - time_t *times; - size_t ntimes; + struct timesheet *times, *end; if (!pd->user) return errorpage(pd, KHTTP_401); @@ -278,19 +271,27 @@ pagemain(struct pagedata *pd) if (status != KCGI_OK) return status; - times = gettimes(pd, pd->user->hash, &ntimes); + times = gettimes(pd, pd->user->hash); if (!times) err(1, "gettimes"); + for (end = times; end->next; end = end->next) + ; + if (end->set & EndTimeFlag) { + end->next = newtimesheet(); + if (!end->next) + freetimesheet(times); + } + if ((status = htmlwithin(pd, KELEM_H1, "Timekeeper")) != KCGI_OK || - (status = printtimes(pd, times, ntimes)) != KCGI_OK || + (status = printtimes(pd, times)) != KCGI_OK || (status = khtml_attr(&pd->html, KELEM_SCRIPT, KATTR_SRC, "scripts/localize.js", KATTR__MAX)) != KCGI_OK || (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) { - free(times); + freetimesheet(times); return status; } - free(times); + freetimesheet(times); if ((status = htmlwithin(pd, KELEM_H2, "Export")) != KCGI_OK || (status = khtml_attr(&pd->html, KELEM_FORM, diff --git a/stmt.c b/stmt.c @@ -26,6 +26,5 @@ struct sqlbox_pstmt pstmts[] = { */ [StmtSetTime] = { .stmt = "UPDATE userdata SET times =\n" "json_replace(times, '$[#-1][' || ? || ']', ?) WHERE id IS ? AND json_extract(times, '$[#-1][' || ? || ']') IS null" }, - [StmtGetTimes] = { .stmt = "SELECT json_array_length(times) * 4 FROM userdata UNION ALL\n" - "SELECT value FROM userdata, json_tree(times) WHERE userdata.id IS ? AND type IS NOT 'array'" } + [StmtGetTimes] = { .stmt = "SELECT value FROM userdata, json_tree(times) WHERE userdata.id IS ? AND type IS NOT 'array'" } }; diff --git a/times.c b/times.c @@ -3,10 +3,11 @@ #define const #include <sys/types.h> +#include <err.h> #include <stdarg.h> #include <stdint.h> #include <stdlib.h> -#include <err.h> +#include <time.h> #include <kcgi.h> #include <kcgihtml.h> @@ -25,6 +26,13 @@ char *timefields[] = { [EndTime] = "end" }; +enum time_flag timeflagmap[] = { + StartTimeFlag, + BreakStartTimeFlag, + BreakEndTimeFlag, + EndTimeFlag +}; + enum sqlbox_code newrow(struct pagedata *pd, char *hash) { @@ -52,47 +60,73 @@ settime(struct pagedata *pd, char *hash, enum time_field f, time_t time) Len(ps), ps, 0) != SQLBOX_CODE_OK; } -time_t * -gettimes(struct pagedata *pd, char *hash, size_t *relem) +void +freetimesheet(struct timesheet *ts) +{ + if (!ts) + return; + + freetimesheet(ts->next); + free(ts); +} + +struct timesheet * +newtimesheet(void) +{ + return calloc(1, sizeof(struct timesheet)); +} +void +timesheet_set(struct timesheet *ts, enum time_field f, time_t v) +{ + assert(f >= StartTime && f <= EndTime); + ts->times[f] = v; + ts->set |= timeflagmap[f]; +} + +struct timesheet * +gettimes(struct pagedata *pd, char *hash) { size_t stmtid; struct sqlbox_parm p = { .sparm = hash, .type = SQLBOX_PARM_STRING }; struct sqlbox_parmset *r; - time_t *times; - size_t i; - - assert(relem); + struct timesheet *times, *oldtime, *time; + unsigned int n; stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, StmtGetTimes, 1, &p, 0); if (stmtid == 0) err(1, "prepare bind"); - r = sqlbox_step(pd->db, stmtid); - if (!r || r->code != SQLBOX_CODE_OK) - err(1, "step"); - - assert(r->psz == 1 && r->ps[0].type == SQLBOX_PARM_INT); - assert(r->ps[0].iparm >= 0); + times = oldtime = NULL; + for (n = 0; (r = sqlbox_step(pd->db, stmtid)) && + !(!r->psz && r->code == SQLBOX_CODE_OK); n = (n + 1) % 4) { + if (n % 4 == 0) { + oldtime = time; + time = newtimesheet(); + if (!time) { + freetimesheet(times); + return NULL; + } + if (!times) + times = time; + else if (oldtime) { + oldtime->next = time; + time->prev = oldtime; + } + } - *relem = r->ps[0].iparm; - kutil_info(NULL,NULL,"Number of times: %zu", *relem); - times = calloc(*relem, sizeof(*times)); - if (!times) - err(1, "calloc"); - - for (i = 0; i < *relem && (r = sqlbox_step(pd->db, stmtid)) && - !(!r->psz && r->code == SQLBOX_CODE_OK); ++i) { assert(r->psz == 1); - assert(r->ps[0].type == SQLBOX_PARM_INT || - r->ps[0].type == SQLBOX_PARM_NULL); - times[i] = (r->ps[0].type == SQLBOX_PARM_NULL) ? 0 : r->ps[0].iparm; + switch (r->ps[0].type) { + case SQLBOX_PARM_NULL: + break; + case SQLBOX_PARM_INT: + timesheet_set(time, n, r->ps[0].iparm); + break; + default: + err(1, "This is a stmt.c bug"); + } } - assert(r); - - while (*relem > 0 && !times[*relem - 1]) - --*relem; if (!sqlbox_finalise(pd->db, stmtid)) { free(times); @@ -101,3 +135,29 @@ gettimes(struct pagedata *pd, char *hash, size_t *relem) return times; } + +time_t +getduration(struct timesheet *ts) +{ + time_t s, e, o; + + assert(ts->set & StartTimeFlag); + + o = 0; + s = ts->times[StartTime]; + if (ts->set & EndTimeFlag) + e = ts->times[EndTime]; + else + e = time(NULL); + + if (ts->set & BreakStartTimeFlag) { + if (ts->set & BreakEndTimeFlag) { + o = ts->times[BreakEndTime] - ts->times[BreakStartTime]; + } else { + assert(!(ts->set & EndTimeFlag)); + e = ts->times[BreakStartTime]; + } + } + + return e - s - o; +} diff --git a/times.h b/times.h @@ -5,8 +5,28 @@ enum time_field { EndTime }; +enum time_flag { + StartTimeFlag = 1, + BreakStartTimeFlag = 2, + BreakEndTimeFlag = 4, + EndTimeFlag = 8 +}; + +struct timesheet; + +struct timesheet { + time_t times[4]; /* raw time data */ + enum time_flag set; /* which times are set */ + struct timesheet *prev, *next; /* previous and next entry */ +}; + extern char *timefields[]; +extern enum time_flag timeflagmap[]; enum sqlbox_code newrow(struct pagedata *pd, char *hash); enum sqlbox_code settime(struct pagedata *pd, char *hash, enum time_field f, time_t time); -time_t *gettimes(struct pagedata *pd, char *hash, size_t *relem); +void freetimesheet(struct timesheet *ts); +struct timesheet *newtimesheet(void); +void timesheet_set(struct timesheet *ts, enum time_field f, time_t v); +struct timesheet *gettimes(struct pagedata *pd, char *hash); +time_t getduration(struct timesheet *ts);