diff --git a/src/grammar/expenses.y b/src/grammar/expenses.y index 0cb32d3..f2b4a69 100644 --- a/src/grammar/expenses.y +++ b/src/grammar/expenses.y @@ -55,7 +55,11 @@ | error '\n' { yyerrok; } ; -msg: update value TOK_USER TOK_STR { printf("update: %d %f %s %s\n", $1, $2, $3->str, $4->str); $$ = 0;} +msg: update value TOK_USER TOK_STR + { + write_bot_expense_entry(stdout, + create_bot_expense_entry(NULL, $1, $2, $3, $4)); + printf("update: %d %f %s %s\n", $1, $2, $3->str, $4->str); $$ = 0;} | TOK_CMD_DEL TOK_INT { printf("delete: %d\n", $2); $$ = 0; } | TOK_CMD_REPORT ts { printf("report: %d\n", $2); $$ = 0; } @@ -63,7 +67,7 @@ | TOK_DOUBLE ; -update: TOK_CMD_IN +update: TOK_CMD_IN | TOK_CMD_OUT ; diff --git a/src/grammar/types.c b/src/grammar/types.c index c4db6b3..41cd83b 100644 --- a/src/grammar/types.c +++ b/src/grammar/types.c @@ -2,6 +2,7 @@ */ #include "types.h" +/* Parse a string with Flex+Bison instead of a file / stdout */ int grammar_parse_string(const GString *buf) { int res; yy_scan_string(buf->str); @@ -9,3 +10,135 @@ yylex_destroy(); return res; } + +/* Create a BotExpenseEntry with data fed from the parser. If 'previous' is null, initialize as first expense and 'totalAmount' will be 0 + * Returns NULL if error getting local time. + */ +BotExpenseEntry *create_bot_expense_entry +( + const BotExpenseEntry *previous, + const BotCommand cmd, + const double value, + GString* user, + GString* notes +) +{ + unsigned int id; + double totalAmount; + double signedValue = value; + BotExpenseEntry *entry = malloc(sizeof *entry); // TODO this should be freed once it's written to registry + struct tm *lt = malloc(sizeof *lt); + time_t t = time(NULL); + + /* get the local time */ + localtime_r(&t, lt); + if(lt == NULL) { + fprintf(stderr, "Unable to get localtime.\n"); + return NULL; + } + + if(cmd == BOT_CMD_OUT) { + signedValue = -value; + } + + if(previous == NULL) { + id = 1; + totalAmount = signedValue; + } else { + id = previous->id + 1; + totalAmount = previous->totalAmount + signedValue; /* value can be negative */ + } + + entry->id = id; + entry->time = *lt; + entry->value = signedValue; + entry->totalAmount = totalAmount; + entry->user = user; /* we are setting the pointer here, not copying! */ + entry->notes = notes; + entry->_serialized = false; + + return entry; +} + +/* FP should not be null. This assumes that it has been initialized with an header line as well + * Returns NULL in case 'entry' is uninitialized. + */ +GString *serialize_bot_expense_entry_csv +( + BotExpenseEntry *entry +) +{ + /* will be automatically expanded by g_string_printf */ + GString *tstr = g_string_new(NULL); + GString *result = g_string_new(NULL); + + if(entry == NULL) { + fprintf(stderr, "BotExpenseEntry 'entry' is NULL. Initialize it before calling 'serialize_bot_expense_entry_csv'\n"); + return NULL; + } + + /* Time in ISO 8061 */ + g_string_printf(tstr, + "%d-%02d-%02d %02d:%02d:%02d", + entry->time.tm_year + 1900, + entry->time.tm_mon + 1, + entry->time.tm_mday, + entry->time.tm_hour, + entry->time.tm_min, + entry->time.tm_sec); + + g_string_printf(result, + "%u,%s,%.2f,%.2f,%s,%s\n", + entry->id, + tstr->str, + entry->value, + entry->totalAmount, + entry->user->str, + entry->notes->str + ); + + entry->_serialized = true; + + return result; +} + +/* returns the number of bytes written, following the convention of fprintf. + * Returns negative in case of error + */ +int write_bot_expense_entry +( + FILE *fp, + BotExpenseEntry *entry +) +{ + GString *line = serialize_bot_expense_entry_csv(entry); + + if(fp == NULL) { + fprintf(stderr, "File pointer to CSV is NULL. Initialize it before calling 'serialize_bot_expense_entry_csv'\n"); + return -1; + } + + if(line == NULL) { + return -1; + } + + /* line is terminated with '\n' */ + return fprintf(fp, "%s", line->str); +} + +/* To be called after it has been serialized */ +void free_bot_expense_entry +( + BotExpenseEntry *entry +) +{ + if(!entry->_serialized) { + /* TODO could fail here instead */ + fprintf(stderr, "WARNING: freeing an entry that wasn't serialized!\n"); + } + + /* Also free underlying string, we don't need it anymore */ + g_string_free(entry->user, TRUE); + g_string_free(entry->notes, TRUE); + free(entry); +} diff --git a/src/grammar/types.h b/src/grammar/types.h index 12d9e15..83afabf 100644 --- a/src/grammar/types.h +++ b/src/grammar/types.h @@ -2,6 +2,7 @@ */ #pragma once #include +#include #include typedef enum BotCommand { @@ -18,10 +19,45 @@ BOT_TS_YEARLY } BotReportTimespan; +typedef struct BotExpenseEntry { + unsigned int id; + struct tm time; /* number of seconds since the epoch, needs `localtime()` + timezone to localize: https://stackoverflow.com/questions/1442116/how-can-i-get-the-date-and-time-values-in-a-c-program */ + double value; /* positive or negative according to in or out */ + double totalAmount; /* keep track of the total amount at this point */ + bool _serialized; /* utility to check if entry has been serialized before freeing */ + GString *user; + GString *notes; +} BotExpenseEntry; + +/* Parser definitions */ int yylex(); int yyparse(); int yylex_destroy(); typedef struct yy_buffer_state * YY_BUFFER_STATE; YY_BUFFER_STATE yy_scan_string(const char * str); +/* Parse a string with Flex+Bison instead of a file / stdout */ int grammar_parse_string(const GString *buf); + +/* Create a BotExpenseEntry with data fed from the parser. If 'previous' is null, initialize as first expense and 'totalAmount' will be 0 */ +BotExpenseEntry *create_bot_expense_entry +( + const BotExpenseEntry *previous, + const BotCommand cmd, + const double value, + GString* user, + GString* notes +); + +/* FP should not be null */ +GString *serialize_bot_expense_entry_csv +( + BotExpenseEntry *data +); + +/* returns the number of bytes written, following the convention of fprintf */ +int write_bot_expense_entry +( + FILE *fp, + BotExpenseEntry *entry +);